Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions core/dev/experience/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Experience library (#874 Step 2)

Dated lessons keyed by repository and tech stack. This is a Harness²-style **library only**. It does not rewrite Lite, Medium, or Heavy while a task runs, and it does not call a model. Root [LESSONS.md](../../../LESSONS.md) stays the global agent log. This tree does not replace it, and a harness tier does not inject either one.

## Layout

```
core/dev/experience/
repos/<owner>/<name>/<id>.json
stacks/<techStack>/<id>.json
```

A file under `repos/` must say `"repo": "<owner>/<name>"`. It may list `techStacks`. Those tags stay inside that repo.

A file under `stacks/` must say `"repo": null` and `"techStacks": ["<techStack>"]`. That is the only shared scope. Another repo's file is never opened for a lookup that did not name it.

Each entry has `id`, `date` (`YYYY-MM-DD`), `trigger`, `wrong`, and `right`. `seen` and `refs` (`#123`) are optional.

## Read

`lookupExperience({ repo, techStack, query, root })` in `core/src/host/experience.js`:

| Call | Result |
| --- | --- |
| `repo` only | That repo's files |
| `techStack` only | Shared files under `stacks/<techStack>/` |
| both | That repo's entries tagged with the stack, plus the shared stack entries |
| neither | No entries |

`query` filters `id`, `trigger`, `wrong`, and `right` after the scope is fixed. Results sort by `date` descending, then `id` ascending.

`harnessExperience` on the host harness is that read. `resolveHarness` does not attach lessons. `applied` stays false.

The npm package does not ship `core/dev/`. A packaged install's default read returns no entries.

## Write

Add or edit a JSON file and commit it.

```sh
node core/dev/experience/check.mjs
node core/dev/experience/check.mjs add --repo ThinkFlowLab/FlyBot --id <id> \
--date YYYY-MM-DD --trigger '...' --wrong '...' --right '...'
node core/dev/experience/check.mjs add --tech-stack node-host --id <id> \
--date YYYY-MM-DD --trigger '...' --wrong '...' --right '...'
```

`add` writes one new file and refuses to overwrite. It does not edit harness configs. `check` exits non-zero when a path and its fields disagree.
92 changes: 92 additions & 0 deletions core/dev/experience/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Commit-reviewed writer and checker for the experience library.
// Adding a lesson writes one JSON file under this directory. It does not edit
// harness tiers. Review the file in the pull request that adds it.
//
// node core/dev/experience/check.mjs [--root dir]
// node core/dev/experience/check.mjs [--root dir] add --id <id> --date YYYY-MM-DD \
// --trigger <text> --wrong <text> --right <text> [--repo owner/name] [--tech-stack id]
// [--seen N] [--ref #N]
//
// --repo alone writes repos/<owner>/<name>/<id>.json.
// --tech-stack alone writes stacks/<id>/<id>.json with repo null.
// Both write a repo entry tagged with that stack.
// Passing neither --repo nor --tech-stack is an error.

import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, resolve, sep } from 'node:path'
import { checkExperience, DEFAULT_EXPERIENCE_ROOT, prepareExperienceEntry } from '../../src/host/experience.js'

const argv = process.argv.slice(2)
const positional = []
const flags = new Map()
const refs = []

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index]
if (!arg.startsWith('--')) {
positional.push(arg)
continue
}
const key = arg.slice(2)
const value = argv[index + 1]
if (value === undefined || value.startsWith('--')) {
console.error(`missing value for --${key}`)
process.exit(2)
}
index += 1
if (key === 'ref') refs.push(value)
else if (flags.has(key)) {
console.error(`duplicate --${key}`)
process.exit(2)
} else flags.set(key, value)
}

const root = flags.get('root') ?? DEFAULT_EXPERIENCE_ROOT
const command = positional[0] ?? 'check'
if (positional.length > 1 || (command !== 'check' && command !== 'add')) {
console.error('usage: check.mjs [--root dir] [add --id ... --date ... --trigger ... --wrong ... --right ...]')
process.exit(2)
}

if (command === 'check') {
const report = checkExperience(root)
if (!report.ok) {
for (const line of report.failures) console.error(line)
process.exit(1)
}
console.log(`ok ${root}`)
process.exit(0)
}

try {
const stack = flags.get('tech-stack')
const fields = {
id: flags.get('id'),
repo: flags.has('repo') ? flags.get('repo') : null,
techStacks: stack === undefined ? [] : [stack],
date: flags.get('date'),
trigger: flags.get('trigger'),
wrong: flags.get('wrong'),
right: flags.get('right'),
}
if (flags.has('seen')) {
if (!/^[1-9]\d*$/.test(flags.get('seen'))) throw new Error('seen')
fields.seen = Number(flags.get('seen'))
}
if (refs.length > 0) fields.refs = refs
const planned = prepareExperienceEntry(fields)
const base = resolve(root)
const file = resolve(base, planned.relativePath)
if (file !== base && !file.startsWith(base + sep)) throw new Error('experience path escapes root')
if (existsSync(file)) {
console.error(`exists ${file}`)
process.exit(1)
}
mkdirSync(dirname(file), { recursive: true })
writeFileSync(file, planned.body, { flag: 'wx' })
console.log(file)
} catch (error) {
console.error(error.message)
process.exit(1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "one-conclusion",
"repo": "ThinkFlowLab/FlyBot",
"techStacks": [],
"date": "2026-10-24",
"trigger": "A tip already carries a Verdict.",
"wrong": "Posting a second Verdict at the same SHA, or editing the first when the answer changed.",
"right": "One Verdict per tip. A new commit voids the old one. A correction carries no Verdict line."
}
11 changes: 11 additions & 0 deletions core/dev/experience/repos/ThinkFlowLab/FlyBot/stale-base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "stale-base",
"repo": "ThinkFlowLab/FlyBot",
"techStacks": [],
"date": "2026-10-24",
"seen": 4,
"trigger": "Branching from a local main that this run did not fetch, then opening a pull request.",
"wrong": "Reading a green pull request page as proof the branch is current. Ghost files in the diff are the branch restoring an old main.",
"right": "git diff origin/main...HEAD --stat must list only files you changed. Anything else means start again from the fetched tip.",
"refs": ["#864", "#876", "#887"]
}
11 changes: 11 additions & 0 deletions core/dev/experience/stacks/node-host/generated-gates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "generated-gates",
"repo": null,
"techStacks": ["node-host"],
"date": "2026-10-15",
"seen": 3,
"trigger": "An edit under core/ after the last patch generate, or before arch-docs runs.",
"wrong": "Trusting an earlier green check. arch-docs reports no changed files while the edit is still uncommitted.",
"right": "After the last core/ edit, build the client when src/client changed, regenerate ds-bot patches, then verify.",
"refs": ["#94", "#99", "#177"]
}
Loading
Loading