diff --git a/README.md b/README.md index d926794..5f93527 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Less context to repeat, clear task tracking, and decisions preserved between sessions. Choose only the modules you need and adapt them to your project. -**Claude Code · Codex · Copilot · 8 optional modules · Customizable** +**Claude Code · Codex · Copilot · 9 optional modules · Customizable** ## Supported coding agents @@ -128,6 +128,7 @@ project's `.gitignore` is not touched. Hooks stay silent in clones that do not h | Reporting (`reporting`) | Weekly goals and progress reports | | Exploration (`exploration`) | Help clarifying ideas before development | | Learning (`learning`) | Explanations and guidance to help you learn | +| Knowledge (`knowledge`) | Indexes of your app's flows and domain facts, read one section at a time | For a custom first installation: diff --git a/docs/modules.md b/docs/modules.md index bc37b5e..62b79cc 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -17,7 +17,7 @@ or execute anything. `--module DIRECTORY` selects that module; `--modules id,id` Repeat either source option to supply more directories. Required dependencies are resolved from installed modules and available catalogs and printed in the plan. No network download occurs. Use `--profile minimal|continuity|complete` during initial installation; Complete has a fixed list -of eight official modules and never automatically activates external discoveries. +of nine official modules and never automatically activates external discoveries. ## Manifest version 1 @@ -86,6 +86,14 @@ field, and `links` requires every relative Markdown link to name an existing fil reported by the doctor without failing it, and installation, add and update skip it. It checks no external services and makes no claim about the agent following instructions. +## Knowledge module + +`knowledge` installs two editable indexes: `{{TASKS_DIR}}/APP.md` routes code flows +(flow → `doc/app/.md` → keywords) and `{{TASKS_DIR}}/DOC.md` routes verified domain facts +(theme → `doc/.md` → when to read). They hold project-specific knowledge the model cannot +know, never generic stack documentation. The doctor warns when an index exceeds 8 KiB, a theme +exceeds 16 KiB or an index links to a missing file. + ## Preservation and lifecycle The installed `{{KIT_DIR}}/kit.json` records modules, versions, selected IDs, settings, file owners and diff --git a/modules/knowledge/files/APP.md b/modules/knowledge/files/APP.md new file mode 100644 index 0000000..d780532 --- /dev/null +++ b/modules/knowledge/files/APP.md @@ -0,0 +1,12 @@ +# Application flows — index + +Code navigation map, separate from the domain facts in [DOC.md](DOC.md). One file per flow in +`doc/app/`. Find a keyword under "When to read", then open only that flow and its matching `## ` +section. Do not load every flow. A flow links to its implementation files; rules and invariants +stay in their owning documents. + +| Flow | File | When to read (keywords) | +|---|---|---| + +Row format: flow name, a relative link to `doc/app/.md`, then the symbols, files and +symptoms that should lead a reader to that flow. diff --git a/modules/knowledge/files/DOC.md b/modules/knowledge/files/DOC.md new file mode 100644 index 0000000..1567ddb --- /dev/null +++ b/modules/knowledge/files/DOC.md @@ -0,0 +1,15 @@ +# Domain docs — index + +Verified domain facts (source code, specifications, runtime observations), one file per theme in +`doc/`. This index routes to a file; it does not describe its content. Read only the matching +theme, never this whole index plus every theme. + +1. Find the row by responsibility under "When to read". +2. Exact symbol not listed → `grep -ril '' {{TASKS_DIR}}/doc`; theme bodies are the only + source of symbols, the index does not copy them. +3. Large theme → `grep -n '^## '` on the file, then read only the matching section's line range. + +| Theme | File | When to read | +|---|---|---| + +Row format: theme name, a relative link to `doc/.md`, then the questions it answers. diff --git a/modules/knowledge/module.json b/modules/knowledge/module.json new file mode 100644 index 0000000..bc45707 --- /dev/null +++ b/modules/knowledge/module.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "id": "knowledge", + "version": "1.0.0", + "description": "Project knowledge indexes: application flows and domain facts", + "requires": {}, + "skills": [], + "files": [ + { + "source": "files/APP.md", + "target": "{{TASKS_DIR}}/APP.md", + "editable": true + }, + { + "source": "files/DOC.md", + "target": "{{TASKS_DIR}}/DOC.md", + "editable": true + } + ], + "rules": [ + { + "id": "main", + "source": "rules/main.md", + "point": "session-start" + } + ], + "hooks": [], + "checks": [ + { + "path": "{{TASKS_DIR}}/APP.md", + "type": "file", + "maxBytes": 8192, + "level": "warn" + }, + { + "path": "{{TASKS_DIR}}/DOC.md", + "type": "file", + "maxBytes": 8192, + "level": "warn" + }, + { + "pattern": "{{TASKS_DIR}}/doc/app/*.md", + "type": "file", + "maxBytes": 16384, + "level": "warn" + }, + { + "pattern": "{{TASKS_DIR}}/doc/*.md", + "type": "file", + "maxBytes": 16384, + "level": "warn" + }, + { + "path": "{{TASKS_DIR}}/APP.md", + "type": "links", + "level": "warn" + }, + { + "path": "{{TASKS_DIR}}/DOC.md", + "type": "links", + "level": "warn" + } + ] +} diff --git a/modules/knowledge/rules/main.md b/modules/knowledge/rules/main.md new file mode 100644 index 0000000..a4da95f --- /dev/null +++ b/modules/knowledge/rules/main.md @@ -0,0 +1,4 @@ +For code paths start at `${TASKS_DIR}/APP.md`, for domain facts at `${TASKS_DIR}/DOC.md`: read the +matching index row, then only the matching `## ` section of that file, never every theme. +Write a flow doc in `doc/app/` and its index row only when the same flow is investigated a second +time. Never copy index or theme content into CLAUDE.md or AGENTS.md. diff --git a/templates/project/.workflow/runtime/catalog.mjs b/templates/project/.workflow/runtime/catalog.mjs index 55349de..7406ca6 100644 --- a/templates/project/.workflow/runtime/catalog.mjs +++ b/templates/project/.workflow/runtime/catalog.mjs @@ -2,7 +2,7 @@ import { existsSync, readdirSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { assertKeys, readJson, safePath } from './paths.mjs'; -export const officialModules = ['tasks', 'memory', 'continuity', 'quality', 'bugs', 'reporting', 'exploration', 'learning']; +export const officialModules = ['tasks', 'memory', 'continuity', 'quality', 'bugs', 'reporting', 'exploration', 'learning', 'knowledge']; const moduleIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/; const versionPattern = /^\d+\.\d+\.\d+$/; const dependencyPattern = /^(?:\*|\^?\d+\.\d+\.\d+)$/; diff --git a/tests/modules.test.mjs b/tests/modules.test.mjs index 1302b6f..3749d8c 100644 --- a/tests/modules.test.mjs +++ b/tests/modules.test.mjs @@ -156,6 +156,27 @@ test('doctor warns, without failing, on index links to missing files', context = assert.match(output, /0 failure\(s\), 2 warning\(s\)/); }); +test('knowledge: indexes installed editable, preserved on repeat add, themes and links warn', context => { + const setup = fixture(context); install(setup); + add(setup, ['--modules', 'knowledge']); + const tasks = join(setup.project, 'planning notes/tasks'); + assert.match(read(setup, 'planning notes/tasks/APP.md'), /\| Flow \| File \| When to read/); + assert.match(read(setup, 'planning notes/tasks/DOC.md'), /grep -ril '' planning notes\/tasks\/doc/); + assert.match(read(setup, `${kit}/PROJECT_RULES.md`), /\nFor code paths start at `\$\{TASKS_DIR\}\/APP.md`/); + assert.doesNotMatch(read(setup, 'CLAUDE.md'), /APP\.md/); + assert.match(command('bash', [join(setup.project, kit, 'scripts/workflow-doctor.sh')]), /0 failure\(s\), 0 warning\(s\)/); + mkdirSync(join(tasks, 'doc/app'), { recursive: true }); + writeFileSync(join(tasks, 'doc/app/login.md'), 'x'.repeat(16385)); + writeFileSync(join(tasks, 'APP.md'), '| Login | [login.md](doc/app/login.md) | `signIn` |\n| Cart | [cart.md](doc/app/cart.md) | `addItem` |\n'); + const edited = snapshot(setup.project); + add(setup, ['--modules', 'knowledge']); + assert.deepEqual(snapshot(setup.project), edited); + const output = command('bash', [join(setup.project, kit, 'scripts/workflow-doctor.sh')]); + assert.match(output, /warn Size limit exceeded: planning notes\/tasks\/doc\/app\/login.md/); + assert.match(output, /warn Broken link in planning notes\/tasks\/APP.md: doc\/app\/cart.md/); + assert.match(output, /0 failure\(s\), 2 warning\(s\)/); +}); + test('external module: added after install, settings, dependencies, mirrors, editable notes and exact repeat', context => { const setup = fixture(context); install(setup); const source = join(repository, 'examples/modules/project-notes'); @@ -190,7 +211,7 @@ test('complete profile, conditional reporting integration, reporting add and bra const output = command('bash', [join(setup.project, kit, 'hooks/show-handoff.sh')]); assert.match(output, /todo\/dev-example\/README.md/); assert.match(output, /memory\/dev-example.md/); assert.match(output, /handoff\/dev-example.md/); assert.match(output, /Reporting/); add(setup, ['--modules', officialModules.join(',')]); - assert.equal(state(setup).modules.length, 8); + assert.equal(state(setup).modules.length, 9); assert.match(command('bash', [join(setup.project, kit, 'scripts/workflow-doctor.sh')]), /0 failure/); }); @@ -371,12 +392,12 @@ test('all official modules have a unique skill owner; all profiles and independe 'wayfinder', 'weekly-report', 'write-task', ]; assert.deepEqual([...owners].sort(), expectedSkills.sort()); - for (const moduleId of ['memory', 'tasks', 'bugs', 'reporting', 'learning', 'exploration']) { + for (const moduleId of ['memory', 'tasks', 'bugs', 'reporting', 'learning', 'exploration', 'knowledge']) { const setup = fixture(context); install(setup, ['--modules', moduleId]); assert.match(command('bash', [join(setup.project, kit, 'scripts/workflow-doctor.sh')]), /0 failure/); } const complete = fixture(context); install(complete, ['--profile', 'complete']); - assert.equal(state(complete).modules.length, 8); + assert.equal(state(complete).modules.length, 9); }); test('installed skills ship the files their SKILL.md links to', context => { @@ -468,6 +489,7 @@ for (const [profile, extra, configuration] of [ ['continuity', ['--profile', 'continuity'], ''], ['quality', ['--modules', 'tasks,memory,quality'], ''], ['quality with exports', ['--modules', 'tasks,memory,quality'], 'INSTALL_GLOBAL_SKILLS=true\n'], + ['knowledge', ['--modules', 'tasks,memory,knowledge'], ''], ]) test(`update ${profile}: real source and module diff, preserved local data, mirrors, doctor and idempotence`, context => { const setup = updateFixture(context, extra, configuration); const initial = state(setup); @@ -481,6 +503,7 @@ for (const [profile, extra, configuration] of [ initial.configuration.tasksDirectory + '/todo/local-effort/01-task.md', initial.configuration.tasksDirectory + '/handoff/dev-local.md', ]; + if (initial.modules.some(module => module.id === 'knowledge')) localArtifacts.push(initial.configuration.tasksDirectory + '/APP.md'); for (const path of localArtifacts) { mkdirSync(resolve(setup.project, path, '..'), { recursive: true }); writeFileSync(join(setup.project, path), 'Local user evidence: do not reset.\n');