fdeops 3.15.1 → 3.16.0

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.
Files changed (56) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +132 -93
  3. package/adapters/AGENTS.md +2 -2
  4. package/adapters/GEMINI.md +2 -2
  5. package/adapters/LOCAL-LLM.md +3 -3
  6. package/adapters/README.md +1 -1
  7. package/adapters/copilot-instructions.md +2 -2
  8. package/adapters/cursor.fde.mdc +3 -3
  9. package/bin/check.js +66 -14
  10. package/bin/fde.js +16 -16
  11. package/bin/lib/memory.js +1 -1
  12. package/bin/lib/render.js +1 -1
  13. package/bin/lib/trust.js +1 -1
  14. package/mcp/README.md +3 -3
  15. package/mcp/fdeops-ingest/README.md +8 -8
  16. package/mcp/fdeops-ingest/package.json +1 -1
  17. package/mcp/fdeops-ingest/server.js +1 -1
  18. package/mcp/recipes/README.md +3 -3
  19. package/mcp/recipes/file.md +1 -1
  20. package/mcp/recipes/granola.md +5 -5
  21. package/mcp/recipes/notion.md +2 -2
  22. package/mcp/recipes/slack.md +5 -5
  23. package/package.json +1 -1
  24. package/plugin.json +1 -1
  25. package/skills/fde/SKILL.md +52 -39
  26. package/skills/fde/references/ai.md +9 -9
  27. package/skills/fde/references/{exec-narrative.md → board-memo.md} +1 -1
  28. package/skills/fde/references/business-case.md +5 -5
  29. package/skills/fde/references/close.md +5 -5
  30. package/skills/fde/references/{ingest-connect.md → connect.md} +7 -7
  31. package/skills/fde/references/debrief.md +2 -2
  32. package/skills/fde/references/discover.md +8 -8
  33. package/skills/fde/references/{trust-engineering.md → earn-trust.md} +2 -2
  34. package/skills/fde/references/{pattern-extract.md → encode-pattern.md} +1 -1
  35. package/skills/fde/references/eval-pack.md +6 -6
  36. package/skills/fde/references/{scope-defense.md → hold-scope.md} +4 -4
  37. package/skills/fde/references/ingest.md +16 -16
  38. package/skills/fde/references/land.md +10 -10
  39. package/skills/fde/references/{initiative-triage.md → pick-three.md} +7 -7
  40. package/skills/fde/references/plan.md +9 -9
  41. package/skills/fde/references/{sketch.md → poc.md} +4 -4
  42. package/skills/fde/references/{status.md → readout.md} +10 -10
  43. package/skills/fde/references/red-team.md +2 -2
  44. package/skills/fde/references/rescue.md +1 -1
  45. package/skills/fde/references/review.md +7 -7
  46. package/skills/fde/references/{rollback-drill.md → rollback.md} +1 -1
  47. package/skills/fde/references/{handoff-engineering.md → runbook.md} +5 -5
  48. package/skills/fde/references/{use-case-scoring.md → score-use-cases.md} +4 -4
  49. package/skills/fde/references/ship.md +16 -16
  50. package/skills/fde/references/{multi-customer-ops.md → switch-clients.md} +2 -2
  51. package/skills/fde/references/{assumption-audit.md → test-assumptions.md} +3 -3
  52. package/skills/fde/references/{incremental-build.md → thin-slices.md} +22 -13
  53. package/skills/fde/references/{options-analysis.md → three-options.md} +3 -3
  54. package/skills/fde/references/{blast-radius.md → what-breaks.md} +4 -4
  55. package/skills/fde/references/{stakeholder-radar.md → who-decides.md} +5 -5
  56. package/templates/.fde/assumptions.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 the recorded session is session.gif`)
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,14 +57,14 @@ 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 unrouted methods are dead code')
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)
64
64
  const requiredReferences = [
65
65
  'land.md', 'discover.md', 'audit.md', 'plan.md', 'review.md',
66
- 'rescue.md', 'ship.md', 'sketch.md', 'close.md', 'dashboard.md',
67
- 'debrief.md', 'status.md', 'demo-prep.md',
66
+ 'rescue.md', 'ship.md', 'poc.md', 'close.md', 'dashboard.md',
67
+ 'debrief.md', 'readout.md', 'demo-prep.md',
68
68
  'healthcare.md', 'fintech.md', 'gov.md',
69
69
  'ai.md', 'eval-pack.md',
70
70
  ]
@@ -84,8 +84,8 @@ ok('phase references')
84
84
  // file the work lands in. Prose-only guidance drifts into advice nobody can apply.
85
85
  const exampleReferences = [
86
86
  'land.md', 'discover.md', 'plan.md', 'ship.md', 'close.md',
87
- 'status.md', 'stakeholder-radar.md', 'options-analysis.md', 'business-case.md',
88
- 'assumption-audit.md', 'scope-defense.md',
87
+ 'readout.md', 'who-decides.md', 'three-options.md', 'business-case.md',
88
+ 'test-assumptions.md', 'hold-scope.md',
89
89
  ]
90
90
  for (const f of exampleReferences) {
91
91
  const p = path.join(root, 'skills', 'fde', 'references', f)
@@ -115,7 +115,7 @@ for (const refFile of mentioned) {
115
115
  ok(`router dispatch (${mentioned.length} reference targets verified) + memory contract`)
116
116
 
117
117
  // Public claims must match the router. The docs advertise a method count and a
118
- // per-domain list; both drifted from SKILL.md once (ingest / ingest-connect
118
+ // per-domain list; both drifted from SKILL.md once (ingest / connect
119
119
  // routed but undocumented), and a number nobody can verify is worse than none.
120
120
  {
121
121
  // Every routing row must parse. A row this misses is a method that could go
@@ -186,7 +186,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
186
186
  }
187
187
  for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) {
188
188
  const body = read(rel)
189
- // `-` is a word boundary, so \bingest\b matches inside `ingest-connect`:
189
+ // `-` is a word boundary, so \bscore\b matches inside `score-use-cases`:
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
192
  if (absent.length) fail(`${rel} does not list skill(s): ${absent.join(', ')}`)
@@ -201,7 +201,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
201
201
 
202
202
  const refDir = path.join(root, 'skills', 'fde', 'references')
203
203
  const extra = fs.readdirSync(refDir).filter(f => f.endsWith('.md') && !mentioned.includes(f))
204
- if (extra.length) fail(`unrouted reference file(s) dead method: ${extra.join(', ')}`)
204
+ if (extra.length) fail(`unrouted reference file(s) - dead method: ${extra.join(', ')}`)
205
205
  else ok('no unrouted reference files')
206
206
  }
207
207
 
@@ -222,7 +222,7 @@ if (read('package.json').includes('postinstall')) {
222
222
 
223
223
  const readme = read('README.md')
224
224
  if (/session\.gif|demo\.gif|<img /i.test(readme)) {
225
- fail('README must not embed images front door is text; the recording lives in docs/USAGE.md')
225
+ fail('README must not embed images - front door is text; the recording lives in docs/USAGE.md')
226
226
  } else ok('README is text (no gif)')
227
227
 
228
228
  const usage = read('docs/USAGE.md')
@@ -262,9 +262,10 @@ if (!readme.includes('AI coding agent')) {
262
262
  }
263
263
  ok('README clarity sections')
264
264
 
265
- for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/got', '/close', '/debrief', '/prep', '/quiet', '/agreed', '/status']) {
265
+ for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/outcome', '/close', '/debrief', '/prep', '/trust', '/receipts', '/readout']) {
266
266
  if (!readme.includes(cmd)) fail(`README must document slash command ${cmd}`)
267
267
  }
268
+ if (/(^|[^\w/])\/got\b/.test(readme)) fail('README must use /outcome, not /got')
268
269
  ok('README slash commands documented')
269
270
 
270
271
  // Front-door map is the embed left-to-right (LAND → CLOSE), not a pile of situations.
@@ -535,7 +536,7 @@ if (pkg.version !== plugin.version) {
535
536
  if (plugin.commands !== './.claude/commands' || plugin.skills !== './skills') {
536
537
  fail('.claude-plugin/plugin.json must declare skills and commands')
537
538
  } else {
538
- for (const cmd of ['brief', 'discover', 'plan', 'ship', 'got', 'close', 'debrief', 'prep', 'quiet', 'agreed', 'status']) {
539
+ for (const cmd of ['brief', 'discover', 'plan', 'ship', 'outcome', 'close', 'debrief', 'prep', 'trust', 'receipts', 'readout']) {
539
540
  const rel = `.claude/commands/${cmd}.md`
540
541
  if (!fs.existsSync(path.join(root, rel))) fail(`${rel} missing`)
541
542
  else if (!read(rel).includes('@fde')) fail(`${rel} must load @fde`)
@@ -549,8 +550,8 @@ if (!fs.existsSync(path.join(root, 'mcp', 'fdeops-ingest', 'server.js'))) {
549
550
  fail('ingest MCP must expose ingest_stage')
550
551
  } else if (!read('skills/fde/references/ingest.md').includes('stage')) {
551
552
  fail('skills/fde/references/ingest.md missing stage contract')
552
- } else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', 'ingest-connect.md'))) {
553
- fail('skills/fde/references/ingest-connect.md missing')
553
+ } else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', 'connect.md'))) {
554
+ fail('skills/fde/references/connect.md missing')
554
555
  } else {
555
556
  for (const recipe of ['file.md', 'granola.md', 'slack.md', 'notion.md']) {
556
557
  if (!fs.existsSync(path.join(root, 'mcp', 'recipes', recipe))) fail(`mcp/recipes/${recipe} missing`)
@@ -647,4 +648,55 @@ if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'
647
648
  fail('GitHub issue template missing')
648
649
  } else ok('issue templates')
649
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
+
650
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 not day-1 scaffold.
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 the agent rewrites .debrief-propose with prefixes; --apply commits.
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…" capitalized subject + field verb.
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 only staging + the existing confirm gate.
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/ confirm via propose → apply)')
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) required before green ship/close`
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,}|…|-+|—+|–+)(?:[^\w].*)?$/i
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 delivery/decisions/trust evidence only.
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 need a real verdict/run/result.
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) ${top}${issues[0].length > 72 ? '…' : ''}`,
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 ${label}`)
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 ? ` ${p.role}` : ''}${p.note ? ` · ${p.note.slice(0, 60)}` : ''}`))
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 NOT reversible until ledger is repaired)')
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 NOT reversible)')
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 tidy apply cannot create a reversible commit until the ledger exists.'
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 ? '…' : ''}") keep first, retire echoes`,
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 existsSync alone lied.
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)} &middot; ${total} engagement${total === 1 ? '' : 's'} &middot; ${attentionCount} need you &middot; ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
440
440
  <div class="fb-block">
441
- <div class="fb-sec">Queue next action per client, worst first</div>
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 first-match would report "(none set)".
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 it only shells out to the local `fde` CLI.
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` nothing writes `.fde/` unreviewed.
31
+ 4. Agent applies via `ingest_apply` - nothing writes `.fde/` unreviewed.
32
32
 
33
33
  ## Packages
34
34
 
@@ -38,7 +38,7 @@ Source MCP(s) fdeops-ingest MCP fde CLI
38
38
 
39
39
  ## Recipes (copy-paste connect)
40
40
 
41
- See [`recipes/`](./recipes/) for file, Granola-shaped, and Notion-shaped setup. In chat: `@fde I want to connect Granola` → skill `ingest-connect` walks the FDE through config + reload + verify.
41
+ See [`recipes/`](./recipes/) for file, Granola-shaped, and Notion-shaped setup. In chat: `@fde I want to connect Granola` → skill `connect` walks the FDE through config + reload + verify.
42
42
 
43
43
  ## Adding a source MCP
44
44
 
@@ -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** you add those in your own `mcp.json`.
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 then pass `engagement` (path to `.fde/` from `fde resume --bind`) on every tool call.
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** 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.
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.15.1",
3
+ "version": "3.16.0",
4
4
  "private": true,
5
5
  "description": "Thin stdio MCP sink for FDEOps ingest (stage → propose → apply). Zero runtime dependencies.",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  'use strict'
3
3
 
4
4
  /**
5
- * fdeops-ingest MCP thin stdio sink for FDEOps ingest.
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).
@@ -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 only if you are not using the CLI from a bound workspace.
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 never post |
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
- **Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/ingest-connect.md`.
16
+ **Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/connect.md`.
@@ -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 or you paste into chat. This is the default FDE path.
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
 
@@ -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 you add whichever MCP/export path you trust.
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?` notes-source tools should appear.
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 notes-source tools present?
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 do not dump the raw transcript into `.fde/`.
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 they save + reload |
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 |
@@ -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 use a Notion MCP (or export markdown). We do not write back to Notion.
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 Notion read tools present?
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
 
@@ -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?` 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.
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 Slack **read** tools present? If not → this recipe, then stop.
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 do not dump the thread into `.fde/`.
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 they save + reload; we cannot silent-install |
59
- | Missing channel | Token/scopes cannot read that workspace their Slack admin, not FDEOps |
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.1",
3
+ "version": "3.16.0",
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.1",
4
+ "version": "3.16.0",
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",