fdeops 3.15.0 → 3.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +131 -90
- package/adapters/AGENTS.md +2 -2
- package/adapters/GEMINI.md +2 -2
- package/adapters/LOCAL-LLM.md +1 -1
- package/adapters/README.md +2 -2
- package/adapters/copilot-instructions.md +2 -2
- package/adapters/cursor.fde.mdc +3 -3
- package/bin/check.js +91 -32
- package/bin/fde.js +16 -16
- package/bin/lib/memory.js +1 -1
- package/bin/lib/render.js +1 -1
- package/bin/lib/trust.js +1 -1
- package/mcp/README.md +2 -2
- package/mcp/fdeops-ingest/README.md +8 -8
- package/mcp/fdeops-ingest/package.json +1 -1
- package/mcp/fdeops-ingest/server.js +1 -1
- package/mcp/recipes/README.md +2 -2
- package/mcp/recipes/file.md +1 -1
- package/mcp/recipes/granola.md +5 -5
- package/mcp/recipes/notion.md +2 -2
- package/mcp/recipes/slack.md +5 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +36 -23
- package/skills/fde/references/ai.md +8 -8
- package/skills/fde/references/assumption-audit.md +2 -2
- package/skills/fde/references/blast-radius.md +1 -1
- package/skills/fde/references/business-case.md +3 -3
- package/skills/fde/references/close.md +5 -5
- package/skills/fde/references/debrief.md +2 -2
- package/skills/fde/references/discover.md +7 -7
- package/skills/fde/references/eval-pack.md +6 -6
- package/skills/fde/references/handoff-engineering.md +2 -2
- package/skills/fde/references/incremental-build.md +20 -11
- package/skills/fde/references/ingest-connect.md +6 -6
- package/skills/fde/references/ingest.md +14 -14
- package/skills/fde/references/initiative-triage.md +6 -6
- package/skills/fde/references/land.md +8 -8
- package/skills/fde/references/multi-customer-ops.md +1 -1
- package/skills/fde/references/options-analysis.md +2 -2
- package/skills/fde/references/plan.md +6 -6
- package/skills/fde/references/red-team.md +2 -2
- package/skills/fde/references/review.md +7 -7
- package/skills/fde/references/scope-defense.md +3 -3
- package/skills/fde/references/ship.md +16 -16
- package/skills/fde/references/sketch.md +3 -3
- package/skills/fde/references/stakeholder-radar.md +4 -4
- package/skills/fde/references/status.md +8 -8
- package/skills/fde/references/trust-engineering.md +1 -1
- package/skills/fde/references/use-case-scoring.md +1 -1
package/bin/check.js
CHANGED
|
@@ -43,7 +43,7 @@ for (const f of requiredTemplates) {
|
|
|
43
43
|
|
|
44
44
|
const deadMedia = ['demo.gif', 'demo.sh', 'demo.tape', 'terminal-demo.svg', 'fieldbook-dashboard.png', 'fieldbook-detail.png', 'fieldbook-walkthrough.gif']
|
|
45
45
|
for (const name of deadMedia) {
|
|
46
|
-
if (fs.existsSync(path.join(root, 'media', name))) fail(`dead media/${name} must not ship
|
|
46
|
+
if (fs.existsSync(path.join(root, 'media', name))) fail(`dead media/${name} must not ship - the recorded session is session.gif`)
|
|
47
47
|
}
|
|
48
48
|
ok('no staged mock media')
|
|
49
49
|
|
|
@@ -57,7 +57,7 @@ for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
|
|
|
57
57
|
ok('skills structure')
|
|
58
58
|
|
|
59
59
|
if (fs.existsSync(path.join(root, 'skills', 'fde', 'archive'))) {
|
|
60
|
-
fail('skills/fde/archive must not exist
|
|
60
|
+
fail('skills/fde/archive must not exist - unrouted methods are dead code')
|
|
61
61
|
} else ok('no archived skill dump')
|
|
62
62
|
|
|
63
63
|
// v3: one skill + phase references (progressive disclosure)
|
|
@@ -182,28 +182,26 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
|
|
|
182
182
|
// the agent can never reach, advertised anyway.
|
|
183
183
|
const unrouted = [...documented].filter(name => !routed.has(name))
|
|
184
184
|
if (unrouted.length) {
|
|
185
|
-
fail(`docs/skills-reference.md documents
|
|
185
|
+
fail(`docs/skills-reference.md documents skill(s) SKILL.md never routes to: ${unrouted.join(', ')}`)
|
|
186
186
|
}
|
|
187
187
|
for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) {
|
|
188
188
|
const body = read(rel)
|
|
189
189
|
// `-` is a word boundary, so \bingest\b matches inside `ingest-connect`:
|
|
190
190
|
// a method could disappear from the docs behind a hyphenated sibling.
|
|
191
191
|
const absent = [...documented].filter(name => !new RegExp(`(?<![\\w-])${name}(?![\\w-])`).test(body))
|
|
192
|
-
if (absent.length) fail(`${rel} does not list
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const claims = [...body.matchAll(/(\d+)\s+methods/g)].map(m => Number(m[1]))
|
|
196
|
-
if (!claims.length) fail(`${rel} must state how many methods it documents`)
|
|
192
|
+
if (absent.length) fail(`${rel} does not list skill(s): ${absent.join(', ')}`)
|
|
193
|
+
const claims = [...body.matchAll(/(\d+)\s+skills/g)].map(m => Number(m[1]))
|
|
194
|
+
if (!claims.length) fail(`${rel} must state how many skills it documents`)
|
|
197
195
|
const wrong = [...new Set(claims.filter(n => n !== documented.size))]
|
|
198
196
|
if (wrong.length) {
|
|
199
|
-
fail(`${rel} claims ${wrong.join('/')}
|
|
197
|
+
fail(`${rel} claims ${wrong.join('/')} skills; ${documented.size} are documented`)
|
|
200
198
|
}
|
|
201
199
|
}
|
|
202
|
-
ok(`public
|
|
200
|
+
ok(`public skill count is verifiable (${documented.size} documented, ${routed.size} routed)`)
|
|
203
201
|
|
|
204
202
|
const refDir = path.join(root, 'skills', 'fde', 'references')
|
|
205
203
|
const extra = fs.readdirSync(refDir).filter(f => f.endsWith('.md') && !mentioned.includes(f))
|
|
206
|
-
if (extra.length) fail(`unrouted reference file(s)
|
|
204
|
+
if (extra.length) fail(`unrouted reference file(s) - dead method: ${extra.join(', ')}`)
|
|
207
205
|
else ok('no unrouted reference files')
|
|
208
206
|
}
|
|
209
207
|
|
|
@@ -224,7 +222,7 @@ if (read('package.json').includes('postinstall')) {
|
|
|
224
222
|
|
|
225
223
|
const readme = read('README.md')
|
|
226
224
|
if (/session\.gif|demo\.gif|<img /i.test(readme)) {
|
|
227
|
-
fail('README must not embed images
|
|
225
|
+
fail('README must not embed images - front door is text; the recording lives in docs/USAGE.md')
|
|
228
226
|
} else ok('README is text (no gif)')
|
|
229
227
|
|
|
230
228
|
const usage = read('docs/USAGE.md')
|
|
@@ -250,7 +248,7 @@ if (brokenLinks.length) fail(`README links to missing paths: ${brokenLinks.join(
|
|
|
250
248
|
else ok('README links all resolve')
|
|
251
249
|
|
|
252
250
|
for (const section of [
|
|
253
|
-
'How
|
|
251
|
+
'How Skills Work',
|
|
254
252
|
'Quick Start',
|
|
255
253
|
'Engagement memory',
|
|
256
254
|
'Who this is for',
|
|
@@ -264,9 +262,10 @@ if (!readme.includes('AI coding agent')) {
|
|
|
264
262
|
}
|
|
265
263
|
ok('README clarity sections')
|
|
266
264
|
|
|
267
|
-
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/
|
|
265
|
+
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/outcome', '/close', '/debrief', '/prep', '/quiet', '/agreed', '/status']) {
|
|
268
266
|
if (!readme.includes(cmd)) fail(`README must document slash command ${cmd}`)
|
|
269
267
|
}
|
|
268
|
+
if (/(^|[^\w/])\/got\b/.test(readme)) fail('README must use /outcome, not /got')
|
|
270
269
|
ok('README slash commands documented')
|
|
271
270
|
|
|
272
271
|
// Front-door map is the embed left-to-right (LAND → CLOSE), not a pile of situations.
|
|
@@ -282,9 +281,8 @@ if (!readme.includes('fde-engagements') || !/fdeops.*init.*engagement/i.test(rea
|
|
|
282
281
|
fail('README must document fde-engagements + init flow')
|
|
283
282
|
} else ok('README engagement path')
|
|
284
283
|
|
|
285
|
-
// The
|
|
286
|
-
//
|
|
287
|
-
// a field user wants.
|
|
284
|
+
// The advertised command must name the one skill a field user wants.
|
|
285
|
+
// Contributor CLI attack notes live in evals/testing-fieldbook.md, not as a skill.
|
|
288
286
|
for (const m of readme.match(/^.*npx skills add .*$/gm) || []) {
|
|
289
287
|
if (!m.includes('--skill fde')) {
|
|
290
288
|
fail(`README skills-add command must pin --skill fde: ${m.trim()}`)
|
|
@@ -310,19 +308,7 @@ if (!read('skills/fde/SKILL.md').includes('npx --yes fdeops')) {
|
|
|
310
308
|
fail(`SKILL.md description needs several "Use when …" triggers so it fires on intent (found ${triggers.length})`)
|
|
311
309
|
} else if (/Use when the human says @fde or/.test(desc)) {
|
|
312
310
|
fail('SKILL.md description must not gate on @fde - name the client-work intents first')
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Six invented words (fieldbook, terrain, reality, trust signal, receipts,
|
|
317
|
-
// vault) carry the method. They were never defined in one place, so a stranger
|
|
318
|
-
// met them scattered through the docs and guessed.
|
|
319
|
-
{
|
|
320
|
-
const gloss = /Words used here[\s\S]{0,1200}/.exec(readme)
|
|
321
|
-
const missing = ['fieldbook', 'reality', 'terrain', 'trust signal', 'receipts', 'vault']
|
|
322
|
-
.filter(w => !gloss || !new RegExp(w, 'i').test(gloss[0]))
|
|
323
|
-
if (!gloss) fail('README must define its invented words once (a "Words used here" line)')
|
|
324
|
-
else if (missing.length) fail(`README glossary is missing: ${missing.join(', ')}`)
|
|
325
|
-
else ok('README defines its own vocabulary')
|
|
311
|
+
} else ok('SKILL.md description triggers on intent')
|
|
326
312
|
}
|
|
327
313
|
|
|
328
314
|
if (!fs.existsSync(path.join(root, 'docs', 'USAGE.md'))) {
|
|
@@ -550,7 +536,7 @@ if (pkg.version !== plugin.version) {
|
|
|
550
536
|
if (plugin.commands !== './.claude/commands' || plugin.skills !== './skills') {
|
|
551
537
|
fail('.claude-plugin/plugin.json must declare skills and commands')
|
|
552
538
|
} else {
|
|
553
|
-
for (const cmd of ['brief', 'discover', 'plan', 'ship', '
|
|
539
|
+
for (const cmd of ['brief', 'discover', 'plan', 'ship', 'outcome', 'close', 'debrief', 'prep', 'quiet', 'agreed', 'status']) {
|
|
554
540
|
const rel = `.claude/commands/${cmd}.md`
|
|
555
541
|
if (!fs.existsSync(path.join(root, rel))) fail(`${rel} missing`)
|
|
556
542
|
else if (!read(rel).includes('@fde')) fail(`${rel} must load @fde`)
|
|
@@ -629,15 +615,88 @@ for (const manifest of ['plugin.json', 'mcp.json']) {
|
|
|
629
615
|
else ok(`${manifest} published to npm`)
|
|
630
616
|
}
|
|
631
617
|
|
|
618
|
+
const skillDirs = []
|
|
632
619
|
for (const entry of fs.readdirSync(path.join(root, 'skills'))) {
|
|
633
620
|
const dir = path.join(root, 'skills', entry)
|
|
634
621
|
if (!fs.statSync(dir).isDirectory()) continue
|
|
635
622
|
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) fail(`skills/${entry}/ has no SKILL.md - clients skip it`)
|
|
636
|
-
else
|
|
623
|
+
else {
|
|
624
|
+
skillDirs.push(entry)
|
|
625
|
+
ok(`skill ${entry} discoverable`)
|
|
626
|
+
}
|
|
637
627
|
}
|
|
628
|
+
if (skillDirs.length !== 1 || skillDirs[0] !== 'fde') {
|
|
629
|
+
fail(`public tree ships one skill (skills/fde); found: ${skillDirs.join(', ') || '(none)'}`)
|
|
630
|
+
} else ok('one public skill')
|
|
631
|
+
|
|
632
|
+
function findSkillMd(dir, acc = []) {
|
|
633
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
634
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '.agents') continue
|
|
635
|
+
const p = path.join(dir, e.name)
|
|
636
|
+
if (e.isDirectory()) findSkillMd(p, acc)
|
|
637
|
+
else if (e.name === 'SKILL.md') acc.push(path.relative(root, p))
|
|
638
|
+
}
|
|
639
|
+
return acc
|
|
640
|
+
}
|
|
641
|
+
const skillFiles = findSkillMd(root)
|
|
642
|
+
const allowedSkill = path.join('skills', 'fde', 'SKILL.md')
|
|
643
|
+
if (skillFiles.length !== 1 || skillFiles[0] !== allowedSkill) {
|
|
644
|
+
fail(`only ${allowedSkill} may exist; found: ${skillFiles.join(', ') || '(none)'}`)
|
|
645
|
+
} else ok('one SKILL.md')
|
|
638
646
|
|
|
639
647
|
if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'))) {
|
|
640
648
|
fail('GitHub issue template missing')
|
|
641
649
|
} else ok('issue templates')
|
|
642
650
|
|
|
651
|
+
function findUnicodeDashes(dir, acc = []) {
|
|
652
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
653
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '.agents') continue
|
|
654
|
+
const p = path.join(dir, e.name)
|
|
655
|
+
if (e.isDirectory()) findUnicodeDashes(p, acc)
|
|
656
|
+
else if (e.name === 'session.cast' || e.name === 'session.gif') continue
|
|
657
|
+
else {
|
|
658
|
+
let t
|
|
659
|
+
try { t = fs.readFileSync(p, 'utf8') } catch { continue }
|
|
660
|
+
if (t.includes('\u2014') || t.includes('\u2013')) acc.push(path.relative(root, p))
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return acc
|
|
664
|
+
}
|
|
665
|
+
{
|
|
666
|
+
const dashed = findUnicodeDashes(root)
|
|
667
|
+
if (dashed.length) fail(`em/en dashes must be ASCII hyphen: ${dashed.join(', ')}`)
|
|
668
|
+
else ok('no em/en dashes')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!fs.existsSync(path.join(root, 'CODE_OF_CONDUCT.md'))) {
|
|
672
|
+
fail('CODE_OF_CONDUCT.md missing - GitHub community profile needs it')
|
|
673
|
+
} else ok('CODE_OF_CONDUCT.md')
|
|
674
|
+
|
|
675
|
+
if (!fs.existsSync(path.join(root, '.github', 'PULL_REQUEST_TEMPLATE.md'))) {
|
|
676
|
+
fail('.github/PULL_REQUEST_TEMPLATE.md missing - GitHub community profile needs it')
|
|
677
|
+
} else ok('pull request template')
|
|
678
|
+
|
|
679
|
+
if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'question.md'))) {
|
|
680
|
+
fail('.github/ISSUE_TEMPLATE/question.md missing - community profile needs a markdown template with name/about')
|
|
681
|
+
} else {
|
|
682
|
+
const q = read('.github/ISSUE_TEMPLATE/question.md')
|
|
683
|
+
if (!/^---[\s\S]*\nname:/m.test(q) || !/^---[\s\S]*\nabout:/m.test(q)) {
|
|
684
|
+
fail('question.md must have YAML name: and about: so GitHub ticks issue templates')
|
|
685
|
+
} else ok('markdown issue template (name + about)')
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
{
|
|
689
|
+
const mktPath = path.join(root, '.claude-plugin', 'marketplace.json')
|
|
690
|
+
if (!fs.existsSync(mktPath)) fail('.claude-plugin/marketplace.json missing - /plugin marketplace add needs it')
|
|
691
|
+
else {
|
|
692
|
+
const mkt = JSON.parse(read('.claude-plugin/marketplace.json'))
|
|
693
|
+
const plug = (mkt.plugins || [])[0]
|
|
694
|
+
if (mkt.name !== 'fdeops' || !plug || plug.source !== './') {
|
|
695
|
+
fail('marketplace.json must name fdeops and list source ./')
|
|
696
|
+
} else if (!mkt.description && !(mkt.metadata && mkt.metadata.description)) {
|
|
697
|
+
fail('marketplace.json needs a description for the plugin directory')
|
|
698
|
+
} else ok('claude marketplace.json')
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
643
702
|
process.exit(failed)
|
package/bin/fde.js
CHANGED
|
@@ -1134,7 +1134,7 @@ function cmdResume(args) {
|
|
|
1134
1134
|
const fdeDir = path.join(engRoot, '.fde')
|
|
1135
1135
|
const existed = fs.existsSync(fdeDir)
|
|
1136
1136
|
|
|
1137
|
-
// Optional stubs (AI eval pack, …) stay in templates/ for copy-on-use
|
|
1137
|
+
// Optional stubs (AI eval pack, …) stay in templates/ for copy-on-use - not day-1 scaffold.
|
|
1138
1138
|
const SKIP_INIT_TEMPLATES = new Set(['evals.md'])
|
|
1139
1139
|
const fillTemplates = (destFde) => {
|
|
1140
1140
|
for (const f of fs.readdirSync(tpl)) {
|
|
@@ -1381,7 +1381,7 @@ function setContextPhase(eng, phase) {
|
|
|
1381
1381
|
// as one dated debrief block. contact: lines may carry an inline [signal:x]
|
|
1382
1382
|
// token anywhere in the text - preserved verbatim so computeSignals can trust it.
|
|
1383
1383
|
// --smart: thin heuristic propose (existing prefixes + light keywords). Not a
|
|
1384
|
-
// brain
|
|
1384
|
+
// brain - the agent rewrites .debrief-propose with prefixes; --apply commits.
|
|
1385
1385
|
// --dry-run prints the routing without writing anything.
|
|
1386
1386
|
function inferContactSignal(text) {
|
|
1387
1387
|
const t = String(text)
|
|
@@ -1392,7 +1392,7 @@ function inferContactSignal(text) {
|
|
|
1392
1392
|
}
|
|
1393
1393
|
|
|
1394
1394
|
function looksLikePersonLine(text) {
|
|
1395
|
-
// "Denise …" / "Randy opened…"
|
|
1395
|
+
// "Denise …" / "Randy opened…" - capitalized subject + field verb.
|
|
1396
1396
|
return /^[A-Z][a-z]{1,20}\b/.test(text) &&
|
|
1397
1397
|
/\b(helping|quiet|skipped|said|will|opened|resistant|champion|warm|cold|unresponsive|demo|sheet|slack)\b/i.test(text)
|
|
1398
1398
|
}
|
|
@@ -1688,7 +1688,7 @@ function cmdDebrief(args) {
|
|
|
1688
1688
|
|
|
1689
1689
|
// Pull sink: stage raw artifacts outside the memory ledger, then reuse debrief
|
|
1690
1690
|
// propose/apply. Never writes .fde/ until the FDE confirms apply. Source SaaS
|
|
1691
|
-
// (Granola/Gmail/…) is not here
|
|
1691
|
+
// (Granola/Gmail/…) is not here - only staging + the existing confirm gate.
|
|
1692
1692
|
function inboxDir(eng) {
|
|
1693
1693
|
return path.join(path.dirname(eng), '.inbox')
|
|
1694
1694
|
}
|
|
@@ -1863,7 +1863,7 @@ function cmdIngest(args) {
|
|
|
1863
1863
|
console.log(`staged → ${dest}`)
|
|
1864
1864
|
console.log(`id: ${id}`)
|
|
1865
1865
|
console.log('next: fde ingest propose ' + id)
|
|
1866
|
-
console.log('(does not write .fde/
|
|
1866
|
+
console.log('(does not write .fde/ - confirm via propose → apply)')
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
1869
|
function cmdReceipts(args) {
|
|
@@ -2157,7 +2157,7 @@ function collectDoctorIssues(eng) {
|
|
|
2157
2157
|
}
|
|
2158
2158
|
if (engagementTouchesAI(eng) && !hasEvalReceipt(eng)) {
|
|
2159
2159
|
issues.push(
|
|
2160
|
-
`phase is ${s.phase} with AI in scope but no eval receipt (evals.md Verdict or delivery Eval / Ship receipts)
|
|
2160
|
+
`phase is ${s.phase} with AI in scope but no eval receipt (evals.md Verdict or delivery Eval / Ship receipts) - required before green ship/close`
|
|
2161
2161
|
)
|
|
2162
2162
|
}
|
|
2163
2163
|
}
|
|
@@ -2302,7 +2302,7 @@ function hasValueBucket(eng) {
|
|
|
2302
2302
|
// "pending review", "TBD.", "n/a (blocked)" and "..." are all the same thing an
|
|
2303
2303
|
// FDE means by an empty cell - nagging about them teaches people to ignore doctor.
|
|
2304
2304
|
const PENDING_CELL_RE =
|
|
2305
|
-
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not measured|\?+|\.{2,}
|
|
2305
|
+
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not measured|\?+|\.{2,}|…|-+|-+|-+)(?:[^\w].*)?$/i
|
|
2306
2306
|
|
|
2307
2307
|
function parseValueLedger(eng) {
|
|
2308
2308
|
const ledger = stripTemplateNoise(sectionBody(readClean(eng, 'delivery.md'), 'Value ledger') || '')
|
|
@@ -2359,7 +2359,7 @@ function valueLedgerStatusLines(eng, opts = {}) {
|
|
|
2359
2359
|
return lines
|
|
2360
2360
|
}
|
|
2361
2361
|
|
|
2362
|
-
// AI in scope for ship/close hygiene
|
|
2362
|
+
// AI in scope for ship/close hygiene - delivery/decisions/trust evidence only.
|
|
2363
2363
|
// Do not scan terrain.md: its template headers mention LLM and would false-positive every ship.
|
|
2364
2364
|
function engagementTouchesAI(eng) {
|
|
2365
2365
|
const trust = readClean(eng, 'trust-profile.md')
|
|
@@ -2376,7 +2376,7 @@ function hasEvalReceipt(eng) {
|
|
|
2376
2376
|
const evalsPath = path.join(eng, 'evals.md')
|
|
2377
2377
|
if (fs.existsSync(evalsPath)) {
|
|
2378
2378
|
const e = stripTemplateNoise(readClean(eng, 'evals.md'))
|
|
2379
|
-
// Empty G1 stub + "Pass / fail" heading is not a receipt
|
|
2379
|
+
// Empty G1 stub + "Pass / fail" heading is not a receipt - need a real verdict/run/result.
|
|
2380
2380
|
if (/\*\*Verdict:\*\*\s*SHIP\b/i.test(e) || /(?:^|\n)\s*-\s*\*\*Verdict:\*\*\s*SHIP\b/i.test(e)) return true
|
|
2381
2381
|
if (/\bLast run:\s*\d{4}-\d{2}-\d{2}/i.test(e)) return true
|
|
2382
2382
|
if (/\|\s*G\d+\s*\|[^|\n]+\|[^|\n]+\|[^|\n]+\|[^|\n]+\|\s*pass\s*\|/i.test(e)) return true
|
|
@@ -2397,7 +2397,7 @@ function hygieneTriageLines(eng) {
|
|
|
2397
2397
|
if (!issues.length) return []
|
|
2398
2398
|
const top = issues[0].replace(/\s+/g, ' ').trim().slice(0, 72)
|
|
2399
2399
|
return [
|
|
2400
|
-
` hygiene: ${issues.length} issue(s)
|
|
2400
|
+
` hygiene: ${issues.length} issue(s) - ${top}${issues[0].length > 72 ? '…' : ''}`,
|
|
2401
2401
|
' → say "@fde clean up the fieldbook" when ready (agent runs fde doctor; nothing auto-rewrites)',
|
|
2402
2402
|
]
|
|
2403
2403
|
}
|
|
@@ -2494,7 +2494,7 @@ function cmdPrep(args) {
|
|
|
2494
2494
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
2495
2495
|
const label = args.join(' ').trim() || 'next meeting'
|
|
2496
2496
|
// Grounded brief: only text already in .fde/. No invention (Rowboat meeting-prep rule).
|
|
2497
|
-
console.log(`MEETING PREP
|
|
2497
|
+
console.log(`MEETING PREP - ${label}`)
|
|
2498
2498
|
console.log('(grounded in local .fde/ only - if a fact is missing, it is missing)\n')
|
|
2499
2499
|
console.log(resumeTriage(eng))
|
|
2500
2500
|
const owner = readOwner(eng)
|
|
@@ -2504,7 +2504,7 @@ function cmdPrep(args) {
|
|
|
2504
2504
|
const people = extractStakeholders(eng).slice(0, 8)
|
|
2505
2505
|
console.log('\nStakeholders (table + signal history)')
|
|
2506
2506
|
if (!people.length) console.log(' (none yet - log contacts with --signal)')
|
|
2507
|
-
else people.forEach(p => console.log(` [${p.signal}] ${p.name}${p.role ? `
|
|
2507
|
+
else people.forEach(p => console.log(` [${p.signal}] ${p.name}${p.role ? ` - ${p.role}` : ''}${p.note ? ` · ${p.note.slice(0, 60)}` : ''}`))
|
|
2508
2508
|
|
|
2509
2509
|
const risks = extractRisks(eng).slice(0, 5)
|
|
2510
2510
|
console.log('\nOpen risks (table + dated bullets)')
|
|
@@ -2537,16 +2537,16 @@ function cmdGarden(args) {
|
|
|
2537
2537
|
if (gitHealth.ok) {
|
|
2538
2538
|
console.log('TIDY (contract: no new facts · no deleted substance · reversible via memory git)')
|
|
2539
2539
|
} else if (gitHealth.reason === 'broken') {
|
|
2540
|
-
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory git BROKEN
|
|
2540
|
+
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory git BROKEN - NOT reversible until ledger is repaired)')
|
|
2541
2541
|
} else {
|
|
2542
|
-
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory not git-versioned
|
|
2542
|
+
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory not git-versioned - NOT reversible)')
|
|
2543
2543
|
}
|
|
2544
2544
|
console.log(resumeTriage(eng))
|
|
2545
2545
|
if (!gitHealth.ok) {
|
|
2546
2546
|
console.log(
|
|
2547
2547
|
gitHealth.reason === 'broken'
|
|
2548
2548
|
? '\n⚠ ledger is UNVERSIONED (corrupt .git). Repair before trusting tidy apply: mv .fde/.git .fde/.git.broken && run any fde write to re-init.'
|
|
2549
|
-
: '\n⚠ no memory git
|
|
2549
|
+
: '\n⚠ no memory git - tidy apply cannot create a reversible commit until the ledger exists.'
|
|
2550
2550
|
)
|
|
2551
2551
|
}
|
|
2552
2552
|
const proposals = []
|
|
@@ -2564,7 +2564,7 @@ function cmdGarden(args) {
|
|
|
2564
2564
|
proposals.push({
|
|
2565
2565
|
id: 'dedupe-risks',
|
|
2566
2566
|
kind: 'apply',
|
|
2567
|
-
text: `Consolidate ${dupes.length} duplicate open-risk cluster(s) (e.g. "${sample}${sample.length >= 50 ? '…' : ''}")
|
|
2567
|
+
text: `Consolidate ${dupes.length} duplicate open-risk cluster(s) (e.g. "${sample}${sample.length >= 50 ? '…' : ''}") - keep first, retire echoes`,
|
|
2568
2568
|
clusters: dupes,
|
|
2569
2569
|
})
|
|
2570
2570
|
}
|
package/bin/lib/memory.js
CHANGED
|
@@ -152,7 +152,7 @@ function createMemoryApi(deps) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// True only when .git exists AND can resolve HEAD. A corrupt ledger (broken
|
|
155
|
-
// HEAD / missing objects) still has a .git directory
|
|
155
|
+
// HEAD / missing objects) still has a .git directory - existsSync alone lied.
|
|
156
156
|
function memoryGitHealthy(eng) {
|
|
157
157
|
if (!eng) return { ok: false, reason: 'missing' }
|
|
158
158
|
if (!fs.existsSync(path.join(eng, '.git'))) return { ok: false, reason: 'missing' }
|
package/bin/lib/render.js
CHANGED
|
@@ -438,7 +438,7 @@ function todayViewHtml({ today, total, attentionCount, highRiskTotal, todayQueue
|
|
|
438
438
|
<h1 class="fb-h1">Today</h1>
|
|
439
439
|
<div class="fb-meta-line">${escapeHtml(today)} · ${total} engagement${total === 1 ? '' : 's'} · ${attentionCount} need you · ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
|
|
440
440
|
<div class="fb-block">
|
|
441
|
-
<div class="fb-sec">Queue
|
|
441
|
+
<div class="fb-sec">Queue - next action per client, worst first</div>
|
|
442
442
|
${todayQueue}
|
|
443
443
|
</div>
|
|
444
444
|
${flagsHtml ? `<div class="fb-block">
|
package/bin/lib/trust.js
CHANGED
|
@@ -95,7 +95,7 @@ function createTrustApi(deps) {
|
|
|
95
95
|
|
|
96
96
|
function nextActionLine(ctx) {
|
|
97
97
|
// lastNonEmpty: template ships an empty ## Next action; agents often append a
|
|
98
|
-
// second heading with the real bullet
|
|
98
|
+
// second heading with the real bullet - first-match would report "(none set)".
|
|
99
99
|
const body = sectionBody(ctx, 'Next action', { lastNonEmpty: true })
|
|
100
100
|
for (const raw of body.split('\n')) {
|
|
101
101
|
const t = raw.trim().replace(/^[-*]\s+/, '')
|
package/mcp/README.md
CHANGED
|
@@ -9,7 +9,7 @@ FDEOps MCP servers follow a **pluggable source model**: core owns the **sink**,
|
|
|
9
9
|
| **Source** | FDE configures separately | Granola, Slack, Notion, Gmail, file |
|
|
10
10
|
| **Sink** | FDEOps (`fdeops-ingest`) | stage → propose → apply into engagement memory |
|
|
11
11
|
|
|
12
|
-
Source MCPs fetch raw text from SaaS APIs using credentials the FDE manages. The ingest MCP never stores OAuth tokens or calls external services
|
|
12
|
+
Source MCPs fetch raw text from SaaS APIs using credentials the FDE manages. The ingest MCP never stores OAuth tokens or calls external services - it only shells out to the local `fde` CLI.
|
|
13
13
|
|
|
14
14
|
## Ground loop
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ Source MCP(s) fdeops-ingest MCP fde CLI
|
|
|
28
28
|
1. Agent pulls from whichever source MCPs are configured.
|
|
29
29
|
2. Agent stages raw content via `ingest_stage` (with `source` provenance).
|
|
30
30
|
3. Agent proposes routing via `ingest_propose`; FDE confirms.
|
|
31
|
-
4. Agent applies via `ingest_apply`
|
|
31
|
+
4. Agent applies via `ingest_apply` - nothing writes `.fde/` unreviewed.
|
|
32
32
|
|
|
33
33
|
## Packages
|
|
34
34
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Thin stdio MCP server for the FDEOps **ingest sink** only: **stage → propose → apply**.
|
|
4
4
|
|
|
5
|
-
This package shells out to the local `fde` CLI. It never calls SaaS APIs. Source MCPs (Granola, Slack, Notion, etc.) are **separate**
|
|
5
|
+
This package shells out to the local `fde` CLI. It never calls SaaS APIs. Source MCPs (Granola, Slack, Notion, etc.) are **separate** - you add those in your own `mcp.json`.
|
|
6
6
|
|
|
7
|
-
**Prefer the CLI when this workspace is bound:** `fde ingest stage|list|propose|apply`. Use this MCP when the host did not start in a bound workspace
|
|
7
|
+
**Prefer the CLI when this workspace is bound:** `fde ingest stage|list|propose|apply`. Use this MCP when the host did not start in a bound workspace - then pass `engagement` (path to `.fde/` from `fde resume --bind`) on every tool call.
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
@@ -70,12 +70,12 @@ Or after `npm link` in this directory:
|
|
|
70
70
|
|
|
71
71
|
## Daily loop (with separate source MCPs)
|
|
72
72
|
|
|
73
|
-
1. **Fetch**
|
|
74
|
-
2. **Stage**
|
|
75
|
-
3. **List**
|
|
76
|
-
4. **Propose**
|
|
77
|
-
5. **Confirm**
|
|
78
|
-
6. **Apply**
|
|
73
|
+
1. **Fetch** - Use your source MCP (e.g. Granola, Gmail) to pull raw text. FDEOps does not bundle these.
|
|
74
|
+
2. **Stage** - `ingest_stage` with `content`, `source` (e.g. `"granola"`), optional `title`.
|
|
75
|
+
3. **List** - `ingest_list` to see staged items in `.inbox/`.
|
|
76
|
+
4. **Propose** - `ingest_propose` with the staged `id`; agent reviews `.debrief-propose`.
|
|
77
|
+
5. **Confirm** - FDE approves the proposal in chat.
|
|
78
|
+
6. **Apply** - `ingest_apply` writes dated facts into `.fde/` with provenance.
|
|
79
79
|
|
|
80
80
|
Raw artifacts stay in `.inbox/`; the system of record (`.fde/`) stays thin and reviewed.
|
|
81
81
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* fdeops-ingest MCP
|
|
5
|
+
* fdeops-ingest MCP - thin stdio sink for FDEOps ingest.
|
|
6
6
|
* Shells out to local `fde` CLI only. Never calls SaaS.
|
|
7
7
|
* MCP stdio transport: newline-delimited JSON-RPC 2.0
|
|
8
8
|
* (Content-Length frames are accepted on input).
|
package/mcp/recipes/README.md
CHANGED
|
@@ -4,13 +4,13 @@ FDEOps does **not** bundle Granola / Slack / Notion OAuth and does **not** push
|
|
|
4
4
|
|
|
5
5
|
**Daily (no MCP):** paste notes to `@fde debrief`, or drop a file ([file.md](./file.md)).
|
|
6
6
|
|
|
7
|
-
**Pull (optional):** you add a **source** MCP. The agent fetches text, then runs `fde ingest` in this bound workspace (stage → propose → you confirm → apply). The `fdeops-ingest` MCP is optional
|
|
7
|
+
**Pull (optional):** you add a **source** MCP. The agent fetches text, then runs `fde ingest` in this bound workspace (stage → propose → you confirm → apply). The `fdeops-ingest` MCP is optional - only if you are not using the CLI from a bound workspace.
|
|
8
8
|
|
|
9
9
|
| Recipe | When |
|
|
10
10
|
|--------|------|
|
|
11
11
|
| [file.md](./file.md) | Transcript / export already on disk, or paste |
|
|
12
12
|
| [granola.md](./granola.md) | Meeting transcripts via a notes MCP (or export) |
|
|
13
|
-
| [slack.md](./slack.md) | Pull a thread/channel as text
|
|
13
|
+
| [slack.md](./slack.md) | Pull a thread/channel as text - never post |
|
|
14
14
|
| [notion.md](./notion.md) | Read a Notion page (or export markdown) |
|
|
15
15
|
|
|
16
16
|
**Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/ingest-connect.md`.
|
package/mcp/recipes/file.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: local file / paste (no source MCP)
|
|
2
2
|
|
|
3
|
-
**Use when:** you already have a transcript, `.eml`, or export on disk
|
|
3
|
+
**Use when:** you already have a transcript, `.eml`, or export on disk - or you paste into chat. This is the default FDE path.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
package/mcp/recipes/granola.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: Granola-shaped meeting transcripts
|
|
2
2
|
|
|
3
|
-
**Use when:** meeting notes live in Granola (or a similar notes MCP). FDEOps does not ship a Granola server
|
|
3
|
+
**Use when:** meeting notes live in Granola (or a similar notes MCP). FDEOps does not ship a Granola server - you add whichever MCP/export path you trust.
|
|
4
4
|
|
|
5
5
|
Daily path if the notes are already in chat or on disk: paste to `@fde debrief` or [file.md](./file.md). This recipe is only for **pull**.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Daily path if the notes are already in chat or on disk: paste to `@fde debrief`
|
|
|
8
8
|
|
|
9
9
|
1. Enable a **Granola (or notes) MCP** in Cursor/Claude per that product’s docs.
|
|
10
10
|
2. Reload MCP / restart the host.
|
|
11
|
-
3. Test: `@fde what can you pull?`
|
|
11
|
+
3. Test: `@fde what can you pull?` - notes-source tools should appear.
|
|
12
12
|
|
|
13
13
|
The sink is **`fde ingest` in this bound workspace.** You do not need `fdeops-ingest` MCP for daily pull.
|
|
14
14
|
|
|
@@ -40,15 +40,15 @@ Replace command/args with whatever the real Granola MCP documents. FDEOps only n
|
|
|
40
40
|
|
|
41
41
|
## Agent steps
|
|
42
42
|
|
|
43
|
-
1. Capability check
|
|
43
|
+
1. Capability check - notes-source tools present?
|
|
44
44
|
2. Fetch transcript text (ask which meeting if ambiguous).
|
|
45
45
|
3. `fde ingest stage --source granola --title "<short>"` then propose → confirm → apply.
|
|
46
|
-
4. Extract decisions/risks/next
|
|
46
|
+
4. Extract decisions/risks/next - do not dump the raw transcript into `.fde/`.
|
|
47
47
|
|
|
48
48
|
## Common fails
|
|
49
49
|
|
|
50
50
|
| Symptom | Fix |
|
|
51
51
|
|---------|-----|
|
|
52
|
-
| No Granola tools | Source MCP not loaded
|
|
52
|
+
| No Granola tools | Source MCP not loaded - they save + reload |
|
|
53
53
|
| Wrong meeting | One clarifying question, then fetch |
|
|
54
54
|
| Wrong engagement | `fde resume` in this workspace before staging |
|
package/mcp/recipes/notion.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: Notion docs / meeting notes
|
|
2
2
|
|
|
3
|
-
**Use when:** useful engagement notes live in Notion. FDEOps does not ship a Notion server
|
|
3
|
+
**Use when:** useful engagement notes live in Notion. FDEOps does not ship a Notion server - use a Notion MCP (or export markdown). We do not write back to Notion.
|
|
4
4
|
|
|
5
5
|
## Setup (once)
|
|
6
6
|
|
|
@@ -36,7 +36,7 @@ The sink is **`fde ingest` in this bound workspace.** `fdeops-ingest` MCP is opt
|
|
|
36
36
|
|
|
37
37
|
## Agent steps
|
|
38
38
|
|
|
39
|
-
1. Capability check
|
|
39
|
+
1. Capability check - Notion read tools present?
|
|
40
40
|
2. Fetch page/block text (ask which page if ambiguous).
|
|
41
41
|
3. `fde ingest stage --source notion --title "<short>"` → propose → confirm → apply.
|
|
42
42
|
|
package/mcp/recipes/slack.md
CHANGED
|
@@ -8,7 +8,7 @@ You add whatever Slack MCP your host already supports. We only accept **text you
|
|
|
8
8
|
|
|
9
9
|
1. Enable a **Slack MCP** (official or community) with read access to the threads you need.
|
|
10
10
|
2. Reload MCP / restart the host.
|
|
11
|
-
3. Test: `@fde what can you pull?`
|
|
11
|
+
3. Test: `@fde what can you pull?` - Slack fetch tools should appear. The FDEOps **CLI** (`fde ingest`) is the sink if this workspace is bound; you do not need `fdeops-ingest` MCP for daily use.
|
|
12
12
|
|
|
13
13
|
### Example mcp.json shape (illustrative)
|
|
14
14
|
|
|
@@ -40,10 +40,10 @@ Optional sink MCP (only if you are not running `fde ingest` from this workspace)
|
|
|
40
40
|
|
|
41
41
|
## Agent steps
|
|
42
42
|
|
|
43
|
-
1. Capability check
|
|
43
|
+
1. Capability check - Slack **read** tools present? If not → this recipe, then stop.
|
|
44
44
|
2. Fetch the thread/channel as **text** (ask which channel/thread if ambiguous).
|
|
45
45
|
3. `fde ingest stage --source slack --title "<short>"` (CLI in this bound workspace).
|
|
46
|
-
4. Propose → FDE confirms → apply. Extract decisions/risks/asks
|
|
46
|
+
4. Propose → FDE confirms → apply. Extract decisions/risks/asks - do not dump the thread into `.fde/`.
|
|
47
47
|
|
|
48
48
|
## Never
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ Optional sink MCP (only if you are not running `fde ingest` from this workspace)
|
|
|
55
55
|
|
|
56
56
|
| Symptom | Fix |
|
|
57
57
|
|---------|-----|
|
|
58
|
-
| No Slack tools | Source MCP not loaded
|
|
59
|
-
| Missing channel | Token/scopes cannot read that workspace
|
|
58
|
+
| No Slack tools | Source MCP not loaded - they save + reload; we cannot silent-install |
|
|
59
|
+
| Missing channel | Token/scopes cannot read that workspace - their Slack admin, not FDEOps |
|
|
60
60
|
| Huge dump | Stage full text in `.inbox/`; apply only short dated facts |
|
|
61
61
|
| Wrong client | Bind this workspace (`fde resume`) before staging |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.2",
|
|
4
4
|
"description": "Forward deployed engineering skills for AI coding agents. Your agent forgets the client every morning - the sponsor, the promise, who signed off. FDEOps keeps that as dated markdown on your laptop: one @fde skill, a deterministic local CLI, and hooks that make it automatic. Claude Code plugin and any agent that loads skills.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fdeops": "bin/install.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "fdeops",
|
|
4
|
-
"version": "3.15.
|
|
4
|
+
"version": "3.15.2",
|
|
5
5
|
"description": "Forward deployed engineering skills for AI coding agents: per-client memory in local .fde/ files, one @fde skill. Local-only, no network.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|