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.
Files changed (51) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +131 -90
  3. package/adapters/AGENTS.md +2 -2
  4. package/adapters/GEMINI.md +2 -2
  5. package/adapters/LOCAL-LLM.md +1 -1
  6. package/adapters/README.md +2 -2
  7. package/adapters/copilot-instructions.md +2 -2
  8. package/adapters/cursor.fde.mdc +3 -3
  9. package/bin/check.js +91 -32
  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 +2 -2
  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 +2 -2
  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 +36 -23
  26. package/skills/fde/references/ai.md +8 -8
  27. package/skills/fde/references/assumption-audit.md +2 -2
  28. package/skills/fde/references/blast-radius.md +1 -1
  29. package/skills/fde/references/business-case.md +3 -3
  30. package/skills/fde/references/close.md +5 -5
  31. package/skills/fde/references/debrief.md +2 -2
  32. package/skills/fde/references/discover.md +7 -7
  33. package/skills/fde/references/eval-pack.md +6 -6
  34. package/skills/fde/references/handoff-engineering.md +2 -2
  35. package/skills/fde/references/incremental-build.md +20 -11
  36. package/skills/fde/references/ingest-connect.md +6 -6
  37. package/skills/fde/references/ingest.md +14 -14
  38. package/skills/fde/references/initiative-triage.md +6 -6
  39. package/skills/fde/references/land.md +8 -8
  40. package/skills/fde/references/multi-customer-ops.md +1 -1
  41. package/skills/fde/references/options-analysis.md +2 -2
  42. package/skills/fde/references/plan.md +6 -6
  43. package/skills/fde/references/red-team.md +2 -2
  44. package/skills/fde/references/review.md +7 -7
  45. package/skills/fde/references/scope-defense.md +3 -3
  46. package/skills/fde/references/ship.md +16 -16
  47. package/skills/fde/references/sketch.md +3 -3
  48. package/skills/fde/references/stakeholder-radar.md +4 -4
  49. package/skills/fde/references/status.md +8 -8
  50. package/skills/fde/references/trust-engineering.md +1 -1
  51. 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 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,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 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)
@@ -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 method(s) SKILL.md never routes to: ${unrouted.join(', ')}`)
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 method(s): ${absent.join(', ')}`)
193
- // every count claim, not just the first: an earlier sentence must not shadow
194
- // a stale headline (or the reverse).
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('/')} methods; ${documented.size} are documented`)
197
+ fail(`${rel} claims ${wrong.join('/')} skills; ${documented.size} are documented`)
200
198
  }
201
199
  }
202
- ok(`public method count is verifiable (${documented.size} documented, ${routed.size} routed)`)
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) dead method: ${extra.join(', ')}`)
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 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')
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 it works',
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', '/got', '/close', '/debrief', '/prep', '/quiet', '/agreed', '/status']) {
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 contributor-only testing skill lives in .agents/skills/, so a bare
286
- // `skills add` installs it too. The advertised command must name the one skill
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
- } else ok('SKILL.md description triggers on intent')
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', 'got', 'close', 'debrief', 'prep', 'quiet', 'agreed', 'status']) {
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 ok(`skill ${entry} discoverable`)
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 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
 
@@ -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.0",
3
+ "version": "3.15.2",
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
16
  **Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/ingest-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.0",
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.0",
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",