From b2bf5d4495f1d2fbc56781a6d1dca1a6f2b1f00f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:20:11 +0000 Subject: [PATCH 1/4] docs: document ingest/ingest-connect and gate the advertised method count Co-Authored-By: Subash Natarajan --- adapters/LOCAL-LLM.md | 2 +- adapters/README.md | 2 +- bin/check.js | 37 +++++++++++++++++++++++++++++++++++++ docs/REPO_LAYOUT.md | 2 +- docs/skills-reference.md | 4 +++- docs/skills.md | 4 ++-- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/adapters/LOCAL-LLM.md b/adapters/LOCAL-LLM.md index db9d8d9..7e800dd 100644 --- a/adapters/LOCAL-LLM.md +++ b/adapters/LOCAL-LLM.md @@ -62,7 +62,7 @@ The model reads SKILL.md, routes to the right skill, and produces artifacts in y ## Model size recommendations -The methodology is detailed (35 skills, routing logic, evidence format, memory contract). Larger models handle it better: +The methodology is detailed (37 methods, routing logic, evidence format, memory contract). Larger models handle it better: | Model class | Experience | |-------------|-----------| diff --git a/adapters/README.md b/adapters/README.md index 7750833..ea52979 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -31,4 +31,4 @@ Defaults to the current directory if no path is given. Existing files are never ## The principle -The adapter only tells the tool **where the brain is and how to behave**. All the method - the 35 skills, the overlays, the memory contract - lives once in `skills/fde/SKILL.md`. Update the brain, every platform gets it. That's why fdeops feels native in whatever the FDE already uses, without five things to keep in sync. +The adapter only tells the tool **where the brain is and how to behave**. All the method - the 37 methods, the overlays, the memory contract - lives once in `skills/fde/SKILL.md`. Update the brain, every platform gets it. That's why fdeops feels native in whatever the FDE already uses, without five things to keep in sync. diff --git a/bin/check.js b/bin/check.js index d6db136..41728fd 100644 --- a/bin/check.js +++ b/bin/check.js @@ -88,6 +88,43 @@ for (const refFile of mentioned) { } ok(`router dispatch (${mentioned.length} reference targets verified) + memory contract`) +// Public claims must match the router. The docs advertise a method count and a +// per-domain list; both drifted from SKILL.md once (ingest / ingest-connect +// routed but undocumented), and a number nobody can verify is worse than none. +{ + const routed = new Set() + const routing = router.split('## Routing - 6 domains')[1] || '' + for (const line of routing.split('\n')) { + const m = line.match(/^\|.*?\|\s*([a-z0-9-]+)(?:\s*\([^)]*\))?\s*\|\s*`references/) + if (m) routed.add(m[1]) + } + if (!routed.size) fail('check.js could not parse the SKILL.md routing table') + + // docs/skills-reference.md is the canonical per-method list: one row per + // method inside the six domain tables, ending at the Overlays section. + const reference = read('docs/skills-reference.md') + const documented = new Set() + for (const line of reference.split('### Overlays')[0].split('\n')) { + const m = line.match(/^\|\s*\[([a-z0-9-]+)\]\(\.\.\/skills\/fde\/references\//) + if (m) documented.add(m[1]) + } + const undocumented = [...routed].filter(name => !documented.has(name)) + if (undocumented.length) { + fail(`SKILL.md routes skill(s) missing from docs/skills-reference.md: ${undocumented.join(', ')}`) + } + for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) { + const body = read(rel) + const absent = [...documented].filter(name => !new RegExp(`\\b${name}\\b`).test(body)) + if (absent.length) fail(`${rel} does not list method(s): ${absent.join(', ')}`) + const claim = body.match(/(\d+)\s+methods/) + if (!claim) fail(`${rel} must state how many methods it documents`) + else if (Number(claim[1]) !== documented.size) { + fail(`${rel} claims ${claim[1]} methods; ${documented.size} are documented`) + } + } + ok(`public method count is verifiable (${documented.size} documented, ${routed.size} routed)`) +} + const install = read('bin/install.js') if (install.includes('scaffoldFdeInProject(process.cwd())')) { fail('install.js must not auto-scaffold .fde in customer cwd') diff --git a/docs/REPO_LAYOUT.md b/docs/REPO_LAYOUT.md index 8b22be7..9f1bb1f 100644 --- a/docs/REPO_LAYOUT.md +++ b/docs/REPO_LAYOUT.md @@ -2,7 +2,7 @@ | Path | Purpose | |------|---------| -| `skills/fde/` | **The one skill** - router (`SKILL.md`) + 35 methods, 5 overlays, and AI companion `eval-pack` under `references/` - installed to `~/.claude/skills/` | +| `skills/fde/` | **The one skill** - router (`SKILL.md`) + 37 methods, 5 overlays, and AI companion `eval-pack` under `references/` - installed to `~/.claude/skills/` | | `adapters/` | Thin per-tool pointers (Codex/`AGENTS.md`, Gemini, Cursor, Copilot, local LLMs) - `node bin/install.js adapters ` | | `templates/.fde/` | Core memory templates for `fde resume --init` (phase artifacts are created by phases on demand; `evals.md` is optional) | | `examples/` | Fictional walkthroughs with sample `.fde/` files | diff --git a/docs/skills-reference.md b/docs/skills-reference.md index 6928f8e..b5d8cee 100644 --- a/docs/skills-reference.md +++ b/docs/skills-reference.md @@ -1,4 +1,4 @@ -# fdeops Reference - one skill, 35 methods across 6 domains +# fdeops Reference - one skill, 37 methods across 6 domains v3 ships **one skill**: `@fde` ([skills/fde/SKILL.md](../skills/fde/SKILL.md)). You describe the situation; it routes to a phase and follows that phase's method from [skills/fde/references/](../skills/fde/references/). Engagement memory lives in `~/fde-engagements//.fde/` (one folder per customer). @@ -78,6 +78,8 @@ Each reference is a **method, not advice**: the thinking the agent does, the art | [handoff-engineering](../skills/fde/references/handoff-engineering.md) | Operations runbook, knowledge transfer, confidence scoring | Engagement ending, team needs to operate without you | | [pattern-extract](../skills/fde/references/pattern-extract.md) | If you did it twice, encode it; patterns are compound interest | Something worked well and will apply to future engagements | | [red-team](../skills/fde/references/red-team.md) | Stress-test a plan, handoff, or narrative before someone else does | "Red-team this," "stress-test my plan," poke holes, what am I missing | +| [ingest](../skills/fde/references/ingest.md) | Pull raw text from any source MCP into `.inbox/`, propose, you confirm | "Pull today's transcript," "bring in the Notion page" | +| [ingest-connect](../skills/fde/references/ingest-connect.md) | Guided config for a source MCP you already trust, plus a reusable recipe | "Connect Granola," "wire up Drive" | ### Overlays (activate on signal, alongside whatever skill is running) diff --git a/docs/skills.md b/docs/skills.md index 2d4ae1e..e6fdd48 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -8,7 +8,7 @@ One skill (`@fde`) routes by situation - you never pick a method by name. Three The full map is below; per-method details live in [skills-reference.md](./skills-reference.md). -## Engagement methods (35 across 6 domains) +## Engagement methods (37 methods across 6 domains) | Domain | Skills | What it covers | |--------|--------|---------------| @@ -17,7 +17,7 @@ The full map is below; per-method details live in [skills-reference.md](./skills | **Plan & Align** | plan, business-case, options-analysis, initiative-triage | Sequencing work, getting sponsor alignment | | **Build & Guard** | build, incremental-build, test-on-legacy, blast-radius, debug, rescue, security-audit, observability | Building safely on their codebase | | **Ship & Verify** | ship, review, rollback-drill, qa-live | Getting to production without surprises | -| **Operate & Close** | status, demo-prep, debrief, exec-narrative, dashboard, multi-customer-ops, close, handoff-engineering, pattern-extract, red-team | Running and ending the engagement well | +| **Operate & Close** | status, demo-prep, debrief, exec-narrative, dashboard, multi-customer-ops, close, handoff-engineering, pattern-extract, red-team, ingest, ingest-connect | Running and ending the engagement well; pulling from source MCPs | Each skill is a **method, not advice**: the thinking the agent does, the artifact it drafts into `.fde/` under `~/fde-engagements/`, and the checkpoint with the human FDE. From b49db80da66a6abd9f085f59dc2529f261d1a41e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:02:13 +0000 Subject: [PATCH 2/4] fix: claims gate reads every routing row and requires the two sets to match Co-Authored-By: Subash Natarajan --- bin/check.js | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/bin/check.js b/bin/check.js index 41728fd..19ef1c0 100644 --- a/bin/check.js +++ b/bin/check.js @@ -92,11 +92,19 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co // per-domain list; both drifted from SKILL.md once (ingest / ingest-connect // routed but undocumented), and a number nobody can verify is worse than none. { + // Every routing row must parse. A row this misses is a method that could go + // undocumented for free, so an unparsed row is a hard failure, not a silent skip. const routed = new Set() - const routing = router.split('## Routing - 6 domains')[1] || '' + const routing = (router.split('## Routing - 6 domains')[1] || '').split('**Overlays')[0] for (const line of routing.split('\n')) { - const m = line.match(/^\|.*?\|\s*([a-z0-9-]+)(?:\s*\([^)]*\))?\s*\|\s*`references/) - if (m) routed.add(m[1]) + if (!/^\|/.test(line) || !/`references\/[a-z0-9-]+\.md`/.test(line)) continue + // | You hear | | | + const method = (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '') + if (!/^[a-z0-9-]+$/.test(method)) { + fail(`check.js cannot read the method name in a SKILL.md routing row: ${line.trim().slice(0, 80)}`) + continue + } + routed.add(method) } if (!routed.size) fail('check.js could not parse the SKILL.md routing table') @@ -104,22 +112,37 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co // method inside the six domain tables, ending at the Overlays section. const reference = read('docs/skills-reference.md') const documented = new Set() + let documentedRows = 0 for (const line of reference.split('### Overlays')[0].split('\n')) { const m = line.match(/^\|\s*\[([a-z0-9-]+)\]\(\.\.\/skills\/fde\/references\//) - if (m) documented.add(m[1]) + if (!m) continue + documentedRows++ + documented.add(m[1]) + } + if (documented.size !== documentedRows) { + fail(`docs/skills-reference.md lists ${documentedRows} method rows for ${documented.size} methods - a duplicate row inflates the count`) } const undocumented = [...routed].filter(name => !documented.has(name)) if (undocumented.length) { fail(`SKILL.md routes skill(s) missing from docs/skills-reference.md: ${undocumented.join(', ')}`) } + // and the other direction: a documented method nothing routes to is a method + // the agent can never reach, advertised anyway. + const unrouted = [...documented].filter(name => !routed.has(name)) + if (unrouted.length) { + fail(`docs/skills-reference.md documents method(s) SKILL.md never routes to: ${unrouted.join(', ')}`) + } for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) { const body = read(rel) const absent = [...documented].filter(name => !new RegExp(`\\b${name}\\b`).test(body)) if (absent.length) fail(`${rel} does not list method(s): ${absent.join(', ')}`) - const claim = body.match(/(\d+)\s+methods/) - if (!claim) fail(`${rel} must state how many methods it documents`) - else if (Number(claim[1]) !== documented.size) { - fail(`${rel} claims ${claim[1]} methods; ${documented.size} are documented`) + // every count claim, not just the first: an earlier sentence must not shadow + // a stale headline (or the reverse). + const claims = [...body.matchAll(/(\d+)\s+methods/g)].map(m => Number(m[1])) + if (!claims.length) fail(`${rel} must state how many methods it documents`) + const wrong = [...new Set(claims.filter(n => n !== documented.size))] + if (wrong.length) { + fail(`${rel} claims ${wrong.join('/')} methods; ${documented.size} are documented`) } } ok(`public method count is verifiable (${documented.size} documented, ${routed.size} routed)`) From 51fa9fcf41f48c42e6d2bc6ebdfcdf9f650428c1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:24:28 +0000 Subject: [PATCH 3/4] fix: gate hard-fails an unreadable reference cell and validates doc link targets Co-Authored-By: Subash Natarajan --- bin/check.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bin/check.js b/bin/check.js index 19ef1c0..e93809a 100644 --- a/bin/check.js +++ b/bin/check.js @@ -97,7 +97,14 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co const routed = new Set() const routing = (router.split('## Routing - 6 domains')[1] || '').split('**Overlays')[0] for (const line of routing.split('\n')) { - if (!/^\|/.test(line) || !/`references\/[a-z0-9-]+\.md`/.test(line)) continue + // Candidate rows are selected on the reference name in ANY form, then the + // shape is enforced - a row this cannot read must fail, never be skipped, + // or a method could go undocumented by being written unusually. + if (!/^\|/.test(line) || !/references\/[a-z0-9-]+\.md/.test(line)) continue + if (!/`references\/[a-z0-9-]+\.md`/.test(line)) { + fail(`SKILL.md routing row must name its reference as \`references/.md\`: ${line.trim().slice(0, 80)}`) + continue + } // | You hear | | | const method = (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '') if (!/^[a-z0-9-]+$/.test(method)) { @@ -114,10 +121,17 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co const documented = new Set() let documentedRows = 0 for (const line of reference.split('### Overlays')[0].split('\n')) { - const m = line.match(/^\|\s*\[([a-z0-9-]+)\]\(\.\.\/skills\/fde\/references\//) + const m = line.match(/^\|\s*\[([a-z0-9-]+)\]\(\.\.\/skills\/fde\/references\/([a-z0-9-]+\.md)\)/) if (!m) continue documentedRows++ documented.add(m[1]) + // A link nobody followed is the same unverifiable claim this gate exists for: + // the target must exist, and it must be the method the text names. + if (m[2] !== `${m[1]}.md`) { + fail(`docs/skills-reference.md links [${m[1]}] at references/${m[2]}`) + } else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', m[2]))) { + fail(`docs/skills-reference.md links references/${m[2]}, which does not exist`) + } } if (documented.size !== documentedRows) { fail(`docs/skills-reference.md lists ${documentedRows} method rows for ${documented.size} methods - a duplicate row inflates the count`) From e7cec39100d71d4e12d8992930072edf4f37cd4c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:28:33 +0000 Subject: [PATCH 4/4] fix: gate catches a method routed without a reference and a name masked by a hyphenated sibling Co-Authored-By: Subash Natarajan --- bin/check.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/bin/check.js b/bin/check.js index e93809a..952f1cc 100644 --- a/bin/check.js +++ b/bin/check.js @@ -96,17 +96,29 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co // undocumented for free, so an unparsed row is a hard failure, not a silent skip. const routed = new Set() const routing = (router.split('## Routing - 6 domains')[1] || '').split('**Overlays')[0] + const methodCell = line => (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '') for (const line of routing.split('\n')) { + if (!/^\|/.test(line)) continue + // A row that names a method but no reference would route that method while + // nothing requires anyone to document it - shape checks can only police rows + // they recognise, so name a method here and you must name its reference. + if (!/references\/[a-z0-9-]+\.md/.test(line)) { + const orphan = methodCell(line) + // a method name, not a `-` placeholder (CLI-only rows) or a `---` separator + if (/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(orphan)) { + fail(`SKILL.md routes '${orphan}' without naming a reference: ${line.trim().slice(0, 80)}`) + } + continue + } // Candidate rows are selected on the reference name in ANY form, then the // shape is enforced - a row this cannot read must fail, never be skipped, // or a method could go undocumented by being written unusually. - if (!/^\|/.test(line) || !/references\/[a-z0-9-]+\.md/.test(line)) continue if (!/`references\/[a-z0-9-]+\.md`/.test(line)) { fail(`SKILL.md routing row must name its reference as \`references/.md\`: ${line.trim().slice(0, 80)}`) continue } // | You hear | | | - const method = (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '') + const method = methodCell(line) if (!/^[a-z0-9-]+$/.test(method)) { fail(`check.js cannot read the method name in a SKILL.md routing row: ${line.trim().slice(0, 80)}`) continue @@ -148,7 +160,9 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co } for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) { const body = read(rel) - const absent = [...documented].filter(name => !new RegExp(`\\b${name}\\b`).test(body)) + // `-` is a word boundary, so \bingest\b matches inside `ingest-connect`: + // a method could disappear from the docs behind a hyphenated sibling. + const absent = [...documented].filter(name => !new RegExp(`(?