fdeops 3.15.2 → 3.17.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 (46) hide show
  1. package/README.md +61 -69
  2. package/adapters/AGENTS.md +1 -1
  3. package/adapters/GEMINI.md +1 -1
  4. package/adapters/LOCAL-LLM.md +4 -4
  5. package/adapters/README.md +1 -1
  6. package/adapters/copilot-instructions.md +1 -1
  7. package/adapters/cursor.fde.mdc +1 -1
  8. package/bin/check.js +27 -15
  9. package/bin/fde.js +150 -13
  10. package/bin/lib/render.js +2 -2
  11. package/bin/lib/trust.js +38 -3
  12. package/mcp/README.md +1 -1
  13. package/mcp/fdeops-ingest/package.json +1 -1
  14. package/mcp/recipes/README.md +1 -1
  15. package/package.json +1 -1
  16. package/plugin.json +1 -1
  17. package/skills/fde/SKILL.md +30 -31
  18. package/skills/fde/references/ai.md +1 -1
  19. package/skills/fde/references/{exec-narrative.md → board-memo.md} +1 -1
  20. package/skills/fde/references/business-case.md +2 -2
  21. package/skills/fde/references/{ingest-connect.md → connect.md} +1 -1
  22. package/skills/fde/references/discover.md +33 -14
  23. package/skills/fde/references/{trust-engineering.md → earn-trust.md} +1 -1
  24. package/skills/fde/references/{pattern-extract.md → encode-pattern.md} +1 -1
  25. package/skills/fde/references/eval-pack.md +1 -1
  26. package/skills/fde/references/{scope-defense.md → hold-scope.md} +2 -2
  27. package/skills/fde/references/ingest.md +3 -3
  28. package/skills/fde/references/land.md +2 -2
  29. package/skills/fde/references/{initiative-triage.md → pick-three.md} +1 -1
  30. package/skills/fde/references/plan.md +10 -7
  31. package/skills/fde/references/{sketch.md → poc.md} +1 -1
  32. package/skills/fde/references/{status.md → readout.md} +2 -2
  33. package/skills/fde/references/rescue.md +1 -1
  34. package/skills/fde/references/review.md +1 -1
  35. package/skills/fde/references/{rollback-drill.md → rollback.md} +1 -1
  36. package/skills/fde/references/{handoff-engineering.md → runbook.md} +3 -3
  37. package/skills/fde/references/{use-case-scoring.md → score-use-cases.md} +3 -3
  38. package/skills/fde/references/ship.md +121 -18
  39. package/skills/fde/references/{multi-customer-ops.md → switch-clients.md} +1 -1
  40. package/skills/fde/references/{assumption-audit.md → test-assumptions.md} +2 -2
  41. package/skills/fde/references/{options-analysis.md → three-options.md} +1 -1
  42. package/skills/fde/references/{blast-radius.md → what-breaks.md} +3 -3
  43. package/skills/fde/references/{stakeholder-radar.md → who-decides.md} +1 -1
  44. package/templates/.fde/assumptions.md +2 -2
  45. package/templates/.fde/reality.md +6 -1
  46. package/skills/fde/references/incremental-build.md +0 -100
package/bin/fde.js CHANGED
@@ -408,6 +408,8 @@ const SECRET_PATTERNS = [
408
408
  { name: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
409
409
  { name: 'PEM private key', re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
410
410
  { name: 'Bearer token', re: /\bBearer\s+[A-Za-z0-9._\-]{20,}\b/ },
411
+ { name: 'database URL', re: /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:]+:[^/\s@]+@/i },
412
+ { name: 'api key assignment', re: /\b(?:api[_-]?key|secret|password)\s*=\s*\S{8,}/i },
411
413
  ]
412
414
 
413
415
  function findSecretHit(text) {
@@ -769,6 +771,89 @@ function firstLine(md, maxLen) {
769
771
  return ''
770
772
  }
771
773
 
774
+ // reality.md is the one panel whose job is "not the brief". If the file does
775
+ // not carry Working theory / Evidence / Differs from brief, do not scrape a
776
+ // first line that might be the inherited brief and label it truth.
777
+ function parseReality(md, maxLen) {
778
+ const theory = (md.match(/\*\*Working theory:\*\*\s*(.*)/i) || [])[1]
779
+ const hasSchema = /\*\*(Working theory|Evidence|Differs from brief how):\*\*/i.test(md)
780
+ const theoryText = (theory || '').trim()
781
+ if (theoryText) {
782
+ const line = theoryText.length > maxLen ? theoryText.slice(0, maxLen - 1).trim() + '…' : theoryText
783
+ return { line, missing: '' }
784
+ }
785
+ if (hasSchema) return { line: '', missing: '' }
786
+ const prose = firstLine(md, maxLen)
787
+ if (prose) {
788
+ return {
789
+ line: '',
790
+ missing: 'UNREADABLE - reality.md does not match the schema (Working theory / Evidence / Differs from brief). Not showing the brief as truth.',
791
+ }
792
+ }
793
+ return { line: '', missing: '' }
794
+ }
795
+
796
+ function appendValueLedgerRow(eng, cells) {
797
+ ensureMemoryGit(eng)
798
+ const p = path.join(eng, 'delivery.md')
799
+ let md = readEng(eng, 'delivery.md')
800
+ if (!md) md = '# Delivery log\n\n## Value ledger\n\n'
801
+ const date = new Date().toISOString().slice(0, 10)
802
+ const cols = []
803
+ for (let i = 0; i < 7; i++) cols.push((cells[i] || '').replace(/\|/g, '\\|').trim() || ' ')
804
+ const row = `| ${date} | ${cols.join(' | ')} |`
805
+ const lines = md.split('\n')
806
+ let inLedger = false
807
+ let lastTableLine = -1
808
+ for (let i = 0; i < lines.length; i++) {
809
+ if (/^##\s+Value ledger\b/i.test(lines[i])) { inLedger = true; continue }
810
+ if (inLedger && /^##\s+/.test(lines[i])) break
811
+ if (inLedger && /^\|/.test(lines[i].trim())) lastTableLine = i
812
+ }
813
+ if (lastTableLine === -1) md = appendUnderSection(md, 'Value ledger', row)
814
+ else {
815
+ lines.splice(lastTableLine + 1, 0, row)
816
+ md = lines.join('\n')
817
+ }
818
+ withFileLock(p, () => { atomicWriteFile(p, md.endsWith('\n') ? md : md + '\n') })
819
+ recordLastWrite(eng, 'delivery.md', row)
820
+ commitMemory(eng, 'log delivery', { files: ['delivery.md'] })
821
+ }
822
+
823
+ function retireOpenRisks(eng, needle) {
824
+ const n = String(needle || '').toLowerCase()
825
+ if (!n) return 0
826
+ const p = path.join(eng, 'risks.md')
827
+ const md = readEng(eng, 'risks.md')
828
+ if (!md) return 0
829
+ const retired = []
830
+ const kept = []
831
+ let inRetired = false
832
+ for (const raw of md.split('\n')) {
833
+ const t = raw.trim()
834
+ if (/^#{1,6}\s+Retired\b/i.test(t)) { inRetired = true; kept.push(raw); continue }
835
+ if (!inRetired) {
836
+ const m = t.match(/^-\s*\[\d{4}-\d{2}-\d{2}\]\s*(?:\[@[^\]]+\]\s*)?(.*)$/)
837
+ if (m && m[1].toLowerCase().includes(n)) {
838
+ retired.push(raw)
839
+ continue
840
+ }
841
+ }
842
+ kept.push(raw)
843
+ }
844
+ if (!retired.length) return 0
845
+ let out = kept.join('\n')
846
+ if (!/^#{1,6}\s+Retired\b/im.test(out)) out = out.replace(/\n*$/, '\n\n## Retired\n')
847
+ const stamp = new Date().toISOString().slice(0, 10)
848
+ const block = retired.map(l => {
849
+ const body = l.trim().replace(/^-\s*/, '')
850
+ return `- [${stamp}] (retired) ${body}`
851
+ }).join('\n')
852
+ out = appendUnderSection(out, 'Retired', block)
853
+ withFileLock(p, () => { atomicWriteFile(p, out.endsWith('\n') ? out : out + '\n') })
854
+ return retired.length
855
+ }
856
+
772
857
  // Engagement age from the .fde/ directory's own birth time - hidden (not
773
858
  // fabricated as 0) on filesystems that do not report birthtime.
774
859
  function daysElapsed(eng) {
@@ -817,6 +902,7 @@ function parseMdTable(md) {
817
902
  function colIndex(headers, rx) { return headers.findIndex(h => rx.test(h)) }
818
903
 
819
904
  const {
905
+ personFromSignalText,
820
906
  signalSubjectKey,
821
907
  nextActionLine,
822
908
  computeSignals,
@@ -856,6 +942,8 @@ function parseSignalHistoryEntries(eng) {
856
942
  }
857
943
 
858
944
  function displayNameFromSignalText(text) {
945
+ const person = personFromSignalText(text)
946
+ if (person) return person
859
947
  const t = String(text).trim()
860
948
  const proper = t.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/)
861
949
  if (proper) return proper[1]
@@ -885,11 +973,11 @@ function extractStakeholders(eng) {
885
973
  const stance = stanceIdx !== -1 ? (cs[stanceIdx] || '').trim() : ''
886
974
  const note = notesIdx !== -1 ? (cs[notesIdx] || '').trim() : ''
887
975
  const words = name.replace(/\([^)]*\)/g, '').split(/\s+/).filter(w => w && !/^(dr|mr|mrs|ms)\.?$/i.test(w))
888
- const frag = (words[0] || '').replace(/[^a-z0-9]/gi, '')
976
+ const nameKey = signalSubjectKey(name)
889
977
  let signal = null, matchedDate = null
890
- if (frag.length >= 3) {
978
+ if (nameKey) {
891
979
  for (const h of history) {
892
- if (h.text.trim().toLowerCase().startsWith(frag.toLowerCase()) && (!matchedDate || h.date >= matchedDate)) {
980
+ if (signalSubjectKey(h.text) === nameKey && (!matchedDate || h.date >= matchedDate)) {
893
981
  signal = h.signal; matchedDate = h.date
894
982
  }
895
983
  }
@@ -1317,6 +1405,9 @@ function cmdLog(args) {
1317
1405
  if (!['red', 'amber', 'green'].includes(signal)) { console.error('usage: fde log contact <text> --signal red|amber|green'); process.exit(1) }
1318
1406
  args.splice(sigIdx, 2)
1319
1407
  }
1408
+ let retire = false
1409
+ const retireIdx = args.indexOf('--retire')
1410
+ if (retireIdx !== -1) { retire = true; args.splice(retireIdx, 1) }
1320
1411
  const type = args[0]; const text = args.slice(1).join(' ')
1321
1412
  const eng = resolveEngagement({ forWrite: true })
1322
1413
  if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
@@ -1333,11 +1424,26 @@ function cmdLog(args) {
1333
1424
  return
1334
1425
  }
1335
1426
 
1336
- if (!LOG_FILES[type] || !text) { console.error(`usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green] [--force]\n fde log phase <${PHASES.join('|')}>\n fde log --undo`); process.exit(1) }
1427
+ if (!LOG_FILES[type] || !text) { console.error(`usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green] [--force]\n fde log risk --retire <text>\n fde log phase <${PHASES.join('|')}>\n fde log --undo`); process.exit(1) }
1337
1428
  if (signal && type !== 'contact') { console.error('--signal only applies to: fde log contact'); process.exit(1) }
1429
+ if (retire && type !== 'risk') { console.error('--retire only applies to: fde log risk'); process.exit(1) }
1338
1430
  const hit = findSecretHit(text)
1339
1431
  if (hit && !force) { refuseSecret('log text', hit); process.exit(1) }
1340
1432
  if (hit && force) console.error(`warning: logging possible ${hit} (--force)`)
1433
+ if (retire) {
1434
+ const n = retireOpenRisks(eng, text)
1435
+ if (!n) { console.error(`no open risk matched ${JSON.stringify(text)}`); process.exit(1) }
1436
+ const hash = commitMemory(eng, 'retire risk', { files: ['risks.md'] })
1437
+ console.log(`retired ${n} risk(s) → risks.md${hash ? ` @${hash}` : ''}`)
1438
+ return
1439
+ }
1440
+ if (type === 'delivery' && text.includes('|')) {
1441
+ const cells = text.split('|').map(s => s.trim())
1442
+ appendValueLedgerRow(eng, cells)
1443
+ const hash = memoryHead(eng)
1444
+ console.log(`logged → delivery.md (value ledger)${hash ? ` @${hash}` : ''}`)
1445
+ return
1446
+ }
1341
1447
  const date = new Date().toISOString().slice(0, 10)
1342
1448
  const entry = datedEntry(eng, date, text, signal || '')
1343
1449
  appendLogEntry(eng, type, entry)
@@ -1651,9 +1757,11 @@ function cmdDebrief(args) {
1651
1757
  input = readDebriefInput(args)
1652
1758
  }
1653
1759
 
1654
- if (smart) {
1760
+ if (smart) {
1655
1761
  const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
1656
1762
  console.log('SMART PROPOSE (heuristic - review before apply; no new facts invented beyond line rewrites)\n')
1763
+ console.log('Prefix vocabulary (lines that route): decision: risk: delivery: contact: next:')
1764
+ console.log('Everything else → context.md. Keep the prefixes; the preview gate stays.\n')
1657
1765
  routeDebriefInput(eng, clean, { dry: true, force, sealed: blocks })
1658
1766
  if (!apply) {
1659
1767
  console.log(`\nproposal saved → ${proposePath}`)
@@ -2182,6 +2290,8 @@ function collectDoctorIssues(eng) {
2182
2290
  `${aliases.length} stakeholder identity cluster(s) (e.g. "${sample}") - same person under different names? consolidate in stakeholders.md`
2183
2291
  )
2184
2292
  }
2293
+ const reality = parseReality(readClean(eng, 'reality.md'), 220)
2294
+ if (reality.missing) issues.push(reality.missing)
2185
2295
  return issues
2186
2296
  }
2187
2297
 
@@ -2484,7 +2594,7 @@ function cmdRedact(args) {
2484
2594
  console.log('nothing changed')
2485
2595
  return
2486
2596
  }
2487
- const hash = commitMemory(eng, `redact ${term.slice(0, 40)}`, { files: touched })
2597
+ const hash = commitMemory(eng, `redact ${hits.length} line(s)`, { files: touched })
2488
2598
  console.log(`redacted ${hits.length} line(s) in ${touched.join(', ')}${hash ? ` @${hash}` : ''}`)
2489
2599
  console.log('rotate the real credential if this was a secret - history may still contain it')
2490
2600
  }
@@ -2585,6 +2695,15 @@ function cmdGarden(args) {
2585
2695
  sessionBlocks,
2586
2696
  })
2587
2697
  }
2698
+ const dirty = memoryDirtyManual(eng)
2699
+ if (dirty.length) {
2700
+ proposals.push({
2701
+ id: 'bless-manual',
2702
+ kind: 'apply',
2703
+ text: `Bless ${dirty.length} hand-written file(s) into the ledger: ${dirty.slice(0, 5).join(', ')}${dirty.length > 5 ? '…' : ''}`,
2704
+ files: dirty,
2705
+ })
2706
+ }
2588
2707
  if (!proposals.length) {
2589
2708
  console.log('\nNothing to tidy.')
2590
2709
  return
@@ -2613,6 +2732,12 @@ function cmdGarden(args) {
2613
2732
  }
2614
2733
  continue
2615
2734
  }
2735
+ if (p.id === 'bless-manual') {
2736
+ applied++
2737
+ for (const f of p.files) touched.add(f)
2738
+ console.log(`applied: bless ${p.files.join(', ')}`)
2739
+ continue
2740
+ }
2616
2741
  if (p.id !== 'archive-sessions') continue
2617
2742
  const cutDates = new Set(p.sessionBlocks.map(b => b.date))
2618
2743
  const keep = []
@@ -2744,6 +2869,10 @@ function cmdStatus(args) {
2744
2869
  }
2745
2870
  }
2746
2871
  if (!all) console.log('\n(current engagement only - pass --all for the full portfolio)')
2872
+ if (!all) {
2873
+ const current = resolveEngagement()
2874
+ if (current) for (const line of hygieneTriageLines(current)) console.log(line)
2875
+ }
2747
2876
  console.log('\ntrust: worst active [signal:x] across stakeholders (latest per person) - a green from B cannot clear an amber/red on A; keyword heuristic only when none exists.')
2748
2877
  }
2749
2878
 
@@ -2801,7 +2930,9 @@ function cmdDashboard(args) {
2801
2930
  // line instead of sharing one, a shorter cap just meant more sentences
2802
2931
  // cut off mid-thought for no reason.
2803
2932
  e.brief = firstLine(readClean(e.dir, 'brief.md'), 220)
2804
- e.reality = firstLine(readClean(e.dir, 'reality.md'), 220)
2933
+ const reality = parseReality(readClean(e.dir, 'reality.md'), 220)
2934
+ e.reality = reality.line
2935
+ e.realityMissing = reality.missing
2805
2936
  e.overlay = detectOverlay(e.dir)
2806
2937
  e.days = daysElapsed(e.dir)
2807
2938
  e.phaseLabel = phaseLabel(e.signals.phase)
@@ -2839,6 +2970,10 @@ function cmdDashboard(args) {
2839
2970
  }
2840
2971
  console.log(`fieldbook → ${outPath}`)
2841
2972
  console.log(`${engagements.length} engagement(s) rendered · ${counts.RED} red / ${counts.amber} amber / ${counts.green} green · 0 tokens (pure render)`)
2973
+ if (!all) {
2974
+ const current = resolveEngagement()
2975
+ if (current) for (const line of hygieneTriageLines(current)) console.log(line)
2976
+ }
2842
2977
  if (args.includes('--open')) {
2843
2978
  // arg-array form: the path is never interpolated into a shell string.
2844
2979
  const [bin, pre] = process.platform === 'darwin' ? ['open', []]
@@ -2985,7 +3120,8 @@ function cmdVault(args) {
2985
3120
  const ctx = readClean(e.dir, 'context.md')
2986
3121
  e.next = (sectionBody(ctx, 'Next action', { lastNonEmpty: true }).split('\n').find(l => l.trim()) || '').trim()
2987
3122
  e.brief = firstLine(readClean(e.dir, 'brief.md'), 400)
2988
- e.reality = firstLine(readClean(e.dir, 'reality.md'), 400)
3123
+ const reality = parseReality(readClean(e.dir, 'reality.md'), 400)
3124
+ e.reality = reality.line || reality.missing
2989
3125
  e.overlay = detectOverlay(e.dir)
2990
3126
  e.days = daysElapsed(e.dir)
2991
3127
  e.stakeholders = extractStakeholders(e.dir)
@@ -3186,19 +3322,20 @@ function printUsage() {
3186
3322
  fde resume --init <name> create + bind engagement for this workspace (rebind replaces)
3187
3323
  fde resume --bind show what this workspace is bound to, and what resolves
3188
3324
  fde triage TRIAGE block only (hooks / Cursor session entry)
3189
- fde log <type> <text> append decision|risk|delivery|contact (contact takes --signal red|amber|green; --force to allow secret-like text)
3325
+ fde log <type> <text> append decision|risk|delivery|contact (contact takes --signal red|amber|green; delivery "a|b|c" writes the value ledger; --force to allow secret-like text)
3326
+ fde log risk --retire move matching open-risk bullets to ## Retired
3190
3327
  fde log phase <phase> set engagement phase (land|discover|plan|ship|prove|close)
3191
3328
  fde log --undo remove the last CLI log/debrief entry from memory
3192
3329
  fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
3193
- fde debrief --smart heuristic propose (prefix + light keywords); agent routes, CLI gates → --apply
3330
+ fde debrief --smart heuristic propose (prints decision:/risk:/delivery:/contact:/next:); --apply after confirm
3194
3331
  fde ingest stage … stage raw pull into <engagement>/.inbox/ (not .fde/)
3195
3332
  fde ingest list list staged inbox items
3196
3333
  fde ingest propose <id> smart-propose a staged item → .debrief-propose (confirm before apply)
3197
3334
  fde ingest apply same as: fde debrief --apply
3198
3335
  fde prep [label] grounded walk-in brief from existing .fde/ only
3199
- fde doctor lint engagement memory (stale signals, gaps)
3200
- fde redact <term> preview/remove lines containing a buried term (pass --apply to commit)
3201
- fde tidy [--apply] propose safe consolidations (contract: no new facts; git-reversible)
3336
+ fde doctor lint engagement memory (stale signals, gaps). status/dashboard/resume print the same issues
3337
+ fde redact <term> preview/remove lines containing a buried term (pass --apply to commit; subject never repeats the term)
3338
+ fde tidy [--apply] propose consolidations; blesses hand-written dirty files when you apply
3202
3339
  fde owner [set email] who keeps this engagement record
3203
3340
  fde receipts <term> "what did we agree?" with dates
3204
3341
  fde status [--all] value ledger, then trust (pass --all for full portfolio)
package/bin/lib/render.js CHANGED
@@ -467,10 +467,10 @@ ${e.lastSession ? `<div class="fb-now-session">${inlineMd(e.lastSession)}</div>`
467
467
  // needed vs what's actually true. Squeezed onto one truncated line, that
468
468
  // contrast disappears. Each gets its own line, its own room to finish a
469
469
  // thought, not a race to fit before an ellipsis.
470
- const whyBlock = (e.brief || e.reality) ? `<div class="fb-block">
470
+ const whyBlock = (e.brief || e.reality || e.realityMissing) ? `<div class="fb-block">
471
471
  <div class="fb-sec">Why</div>
472
472
  ${e.brief ? `<p class="fb-why"><span class="t-faint">What they asked for:</span> ${inlineMd(e.brief)}</p>` : ''}
473
- ${e.reality ? `<p class="fb-why fb-why-reality"><span class="fb-accent-label">What's actually true:</span> ${inlineMd(e.reality)}</p>` : ''}
473
+ ${e.realityMissing ? `<p class="fb-why fb-why-missing"><span class="fb-accent-label">What's actually true:</span> ${escapeHtml(e.realityMissing)}</p>` : e.reality ? `<p class="fb-why fb-why-reality"><span class="fb-accent-label">What's actually true:</span> ${inlineMd(e.reality)}</p>` : ''}
474
474
  </div>` : ''
475
475
 
476
476
  // Vitals: a fixed field-facing gut-check panel, not a Movement block that
package/bin/lib/trust.js CHANGED
@@ -48,12 +48,46 @@ function createTrustApi(deps) {
48
48
  return { ok: true, warn: '' }
49
49
  }
50
50
 
51
- // Subject key for a signal-history line - first real name word (same spirit as
52
- // extractStakeholders). A green about Randy must not clear an amber about Denise.
51
+ // Event labels are not people. "INCIDENT: Marcus escalated" must key on
52
+ // Marcus, or a recovered engagement stays RED in front of the sponsor.
53
+ const SIGNAL_EVENT_KEYS = new Set([
54
+ 'incident', 'recovery', 'alert', 'update', 'note', 'status', 'escalation',
55
+ 'blocker', 'outage', 'fire', 'issue', 'sev', 'sev1', 'sev2', 'p1', 'p2', 'p3',
56
+ 'resolved', 'risk', 'decision', 'delivery', 'contact',
57
+ ])
58
+
59
+ function personFromSignalText(text) {
60
+ let cleaned = String(text || '')
61
+ .replace(/\[@[^\]]+\]/g, '')
62
+ .replace(/\([^)]*\)/g, '')
63
+ .replace(/\[signal:[^\]]+\]/gi, '')
64
+ .replace(/\[\d{4}-\d{2}-\d{2}\]/g, '')
65
+ .replace(/^([A-Z]{2,}[A-Z0-9_-]*):?\s+/, '')
66
+ .trim()
67
+ const names = cleaned.match(/\b[A-Z][a-z]{1,20}(?:\s+[A-Z][a-z]{1,20})?\b/g) || []
68
+ for (const name of names) {
69
+ const first = name.split(/\s+/)[0].toLowerCase()
70
+ if (SIGNAL_EVENT_KEYS.has(first)) continue
71
+ return name
72
+ }
73
+ return ''
74
+ }
75
+
76
+ // Subject key for a signal-history line. Prefer a proper name in the bullet.
77
+ // A green about Randy must not clear an amber about Denise.
53
78
  // Strip author tags [@email-local] so attribution never becomes the subject key.
54
79
  function signalSubjectKey(text) {
80
+ const person = personFromSignalText(text)
81
+ if (person) {
82
+ const frag = person.split(/\s+/)[0].replace(/[^a-z0-9]/gi, '').toLowerCase()
83
+ if (frag.length >= 3) return frag
84
+ }
55
85
  const cleaned = String(text).replace(/\[@[^\]]+\]/g, '').replace(/\([^)]*\)/g, '')
56
- const words = cleaned.split(/\s+/).filter(w => w && !/^(dr|mr|mrs|ms)\.?$/i.test(w))
86
+ .replace(/^([A-Z]{2,}[A-Z0-9_-]*):?\s+/, '')
87
+ const words = cleaned.split(/\s+/).filter(w => {
88
+ const n = w.replace(/[^a-z0-9]/gi, '').toLowerCase()
89
+ return n.length >= 3 && !SIGNAL_EVENT_KEYS.has(n) && !/^(dr|mr|mrs|ms)$/i.test(w)
90
+ })
57
91
  const frag = (words[0] || '').replace(/[^a-z0-9]/gi, '').toLowerCase()
58
92
  return frag.length >= 3 ? frag : ('anon:' + cleaned.slice(0, 48).toLowerCase())
59
93
  }
@@ -196,6 +230,7 @@ function createTrustApi(deps) {
196
230
 
197
231
  return {
198
232
  stakeholdersMemoryHealth,
233
+ personFromSignalText,
199
234
  signalSubjectKey,
200
235
  parsePhase,
201
236
  countOpenRisks,
package/mcp/README.md CHANGED
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.15.2",
3
+ "version": "3.17.0",
4
4
  "private": true,
5
5
  "description": "Thin stdio MCP sink for FDEOps ingest (stage → propose → apply). Zero runtime dependencies.",
6
6
  "bin": {
@@ -13,4 +13,4 @@ FDEOps does **not** bundle Granola / Slack / Notion OAuth and does **not** push
13
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.15.2",
3
+ "version": "3.17.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.2",
4
+ "version": "3.17.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",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fde
3
- description: Keeps the engagement record for client work. Use when they name a client or stakeholder. Use when they debrief a meeting or paste notes. Use when they ask what was agreed. Use when they run a POC, slice a feature on the client's codebase, prove it on their staging, or need evals before a model acts. Use when they prep a readout, when trust shifts, or they say @fde. Route and run the local fde CLI (or npx --yes fdeops). Never ask them to type commands. Not for ordinary code edits in an unbound repo.
3
+ description: Keeps the engagement record for client work. Use when they name a client or stakeholder. Use when they debrief a meeting or paste notes. Use when they ask what was agreed. Use when they run a POC, change the client's codebase, prove it on their staging, go live, or need evals before a model acts. Use when they prep a readout, when trust shifts, or they say @fde. Route and run the local fde CLI (or npx --yes fdeops). Never ask them to type commands. Not for ordinary code edits in an unbound repo.
4
4
  ---
5
5
 
6
6
  # @fde
@@ -17,7 +17,7 @@ The **engagement record** for one client, from first meeting to signed outcome.
17
17
 
18
18
  ## When NOT to use
19
19
 
20
- A one-line typo or compile error in a file that will not ship. On a bound client: stay here for POC, slice, characterisation, eval, go-live, rollback, and acceptance.
20
+ A one-line typo or compile error in a file that will not ship. On a bound client: stay here for POC, characterisation, the change on their repo, eval, go-live, rollback, and acceptance.
21
21
 
22
22
  ## Use these first
23
23
 
@@ -26,25 +26,25 @@ A one-line typo or compile error in a file that will not ship. On a bound client
26
26
  | **The brief is wrong** | "If this works, who in their company would have to agree that it worked?" | `fde resume` then discover | `references/discover.md` |
27
27
  | **They went quiet** | "Is this a process gap, or a trust problem?" | `fde log contact "…" --signal amber\|red\|green` | `references/rescue.md` |
28
28
  | **When did we agree?** | Don't argue from memory. Search the record. | `fde receipts <term>` | - |
29
- | **What's the outcome?** | A number nobody signed is claimed, not delivered. | `fde status` | `references/status.md` |
29
+ | **What's the outcome?** | A number nobody signed is claimed, not delivered. | `fde status` | `references/readout.md` |
30
30
 
31
31
  After a meeting: `fde debrief --smart` → confirm → `--apply`. Walk-in: `fde prep`. Friday: `fde status`.
32
32
 
33
33
  ## Ground loop
34
34
 
35
- On someone else's site the work is not "write code, remember later." Every slice stays on `@fde`:
35
+ On someone else's site the work is not "write code, remember later." Every change on a bound client stays on `@fde`:
36
36
 
37
- 1. **Name it** in `decisions.md` (plan) or kill it in a day (sketch).
38
- 2. **Characterise their code** before you change it (incremental-build). Their tests, their runner.
39
- 3. **Prove it where they live.** Staging they operate, a screen the signer in `success.md` can reject.
40
- 4. **If a model judges:** `evals.md` Verdict SHIP before the slice is done (eval-pack).
41
- 5. **Log delivery.** Outcome is promised → measured → accepted, not a green CI.
37
+ 1. **Name it** in `decisions.md` (plan) or kill it in a day (poc).
38
+ 2. **Characterise their code** before you change it. Brownfield: their tests, their runner. Greenfield: the empty tree, first path they can click.
39
+ 3. **Prove it on their staging.** Staging they operate, a screen the signer in `success.md` can reject.
40
+ 4. **If a model judges:** `evals.md` Verdict SHIP before that change is done (eval-pack).
41
+ 5. **Log delivery.** Outcome is promised → measured → accepted, not a green CI. Then go live with a rollback you have run (`ship`).
42
42
 
43
- A throwaway file can skip the loop. A client slice cannot.
43
+ A throwaway file can skip the loop. Bound client work cannot.
44
44
 
45
45
  ## Human surface vs agent plumbing
46
46
 
47
- **FDE (human):** `@fde` + English, or `/brief` `/discover` `/plan` `/ship` `/outcome` `/close` `/debrief` `/prep` `/quiet` `/agreed` `/status`. Never a skill catalog.
47
+ **FDE (human):** `@fde` + English, or `/brief` `/discover` `/plan` `/ship` `/outcome` `/close` `/debrief` `/prep` `/trust` `/receipts` `/readout`. Never a skill catalog.
48
48
 
49
49
  **You (agent):** run the CLI. **Never tell the FDE to type** `fde …`. If unbound, you run `fde resume --init` after one question. Never ask them to run the CLI.
50
50
 
@@ -72,7 +72,7 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
72
72
  | clean up the fieldbook | `fde doctor` - never auto-rewrite |
73
73
  | scrub a secret | `fde redact <term>` then `--apply` after confirm |
74
74
  | pull Granola/Slack/transcript | capability check → `fde ingest stage` → confirm → apply. Never auto-apply. `references/ingest.md` |
75
- | connect an MCP | `references/ingest-connect.md` |
75
+ | connect an MCP | `references/connect.md` |
76
76
  | Obsidian / one window | `fde vault` (`--redacted` for a shared screen) |
77
77
 
78
78
  ## The memory contract
@@ -121,18 +121,18 @@ Read **one** reference and follow it. Do not improvise from memory.
121
121
  |----------|-------|-----------|
122
122
  | Starting fresh, new customer, first meeting, just got the brief | land | `references/land.md` |
123
123
  | Taking over, previous consultant left, joining mid-project | audit | `references/audit.md` |
124
- | Need to understand who matters, who decides, who blocks quietly | stakeholder-radar | `references/stakeholder-radar.md` |
125
- | Need to earn access, navigate AI policy, build credibility | trust-engineering | `references/trust-engineering.md` |
126
- | "Also can you…", scope expanding, timeline unchanged | scope-defense | `references/scope-defense.md` |
124
+ | Need to understand who matters, who decides, who blocks quietly | who-decides | `references/who-decides.md` |
125
+ | Need to earn access, navigate AI policy, build credibility | earn-trust | `references/earn-trust.md` |
126
+ | "Also can you…", scope expanding, timeline unchanged | hold-scope | `references/hold-scope.md` |
127
127
 
128
128
  ### Discover
129
129
 
130
130
  | You hear | Skill | Reference |
131
131
  |----------|-------|-----------|
132
132
  | Don't know the real problem, brief feels wrong, shadow processes | discover | `references/discover.md` |
133
- | The brief feels too neat, assumptions untested, "we just need…" | assumption-audit | `references/assumption-audit.md` |
134
- | Multiple use cases competing, "we want to do everything" | use-case-scoring | `references/use-case-scoring.md` |
135
- | Need to validate a direction, prototype, demo to de-risk, **POC**, spike, killer assumption | sketch | `references/sketch.md` |
133
+ | The brief feels too neat, assumptions untested, "we just need…" | test-assumptions | `references/test-assumptions.md` |
134
+ | Multiple use cases competing, "we want to do everything" | score-use-cases | `references/score-use-cases.md` |
135
+ | Need to validate a direction, prototype, demo to de-risk, **POC**, spike, killer assumption | poc | `references/poc.md` |
136
136
 
137
137
  ### Plan
138
138
 
@@ -140,43 +140,42 @@ Read **one** reference and follow it. Do not improvise from memory.
140
140
  |----------|-------|-----------|
141
141
  | Break this down, what order, sequence the build | plan | `references/plan.md` |
142
142
  | Sponsor needs justification, need to defend budget or timeline | business-case | `references/business-case.md` |
143
- | Significant decision, multiple approaches, "what should we do?" | options-analysis | `references/options-analysis.md` |
144
- | 20 things are "urgent," need to pick the 3 that matter | initiative-triage | `references/initiative-triage.md` |
143
+ | Significant decision, multiple approaches, "what should we do?" | three-options | `references/three-options.md` |
144
+ | 20 things are "urgent," need to pick the 3 that matter | pick-three | `references/pick-three.md` |
145
145
 
146
146
  ### Ship
147
147
 
148
148
  | You hear | Skill | Reference |
149
149
  |----------|-------|-----------|
150
- | Large feature, need visible progress every 2-3 days, slice it, characterise their tests, POC follow-through | incremental-build | `references/incremental-build.md` |
151
- | What could go wrong, touching shared infrastructure, need to assess impact | blast-radius | `references/blast-radius.md` |
150
+ | What could go wrong, touching shared infrastructure, need to assess impact | what-breaks | `references/what-breaks.md` |
152
151
  | Production down, urgent - OR stakeholder gone quiet, trust slipping | rescue | `references/rescue.md` |
153
- | Ready to deploy, going live, pre-flight check | ship | `references/ship.md` |
152
+ | Start building, update their checkout, first module, visible progress, their tests, POC follow-through, ready to deploy, going live, pre-flight | ship | `references/ship.md` |
154
153
  | Review this change, is it safe, does it match what we agreed | review | `references/review.md` |
155
154
  | Diff grew / scope creep in the PR / "did we only build what we said" / KEEP JUSTIFY SPLIT DROP | review (+ ship if going live) | `references/review.md` Stage 1 · `references/ship.md` Intent vs diff |
156
155
  | Wrap the session / share the thinking / catch teammates up / before I open the PR | (memory contract - session digest) | SKILL.md **On exit** - write TL;DR + decisions/why into `.fde/`; no transcript sync |
157
- | "We can always revert" - need to actually test the escape route | rollback-drill | `references/rollback-drill.md` |
156
+ | "We can always revert" - need to actually test the escape route | rollback | `references/rollback.md` |
158
157
 
159
158
  ### Prove
160
159
 
161
160
  | You hear | Skill | Reference |
162
161
  |----------|-------|-----------|
163
- | Weekly update due, "need to send the sponsor something" | status | `references/status.md` |
162
+ | Weekly update due, "need to send the sponsor something" | readout | `references/readout.md` |
164
163
  | Demo coming up, show-and-tell, exec walkthrough | demo-prep | `references/demo-prep.md` |
165
164
  | Just out of a meeting, raw notes, "they said…", "debrief" | debrief | the debrief verb (above) + `references/debrief.md` |
166
165
  | Make sure we're up to date, pull what's relevant, fetch from Granola/Slack/Gmail/transcript | ingest | `references/ingest.md` (capability check → stage → propose → confirm → apply) |
167
- | Connect a new MCP / connect Granola Slack or Notion / what can you pull | ingest-connect | `references/ingest-connect.md` (+ `mcp/recipes/`) |
166
+ | Connect a new MCP / connect Granola Slack or Notion / what can you pull | connect | `references/connect.md` (+ `mcp/recipes/`) |
168
167
  | Prep me for a meeting / walk-in brief / "what should I know before I talk to…" | - | run `fde prep "<label>"`, present in plain language |
169
- | Sponsor's boss needs a summary, board update, justify continued investment | exec-narrative | `references/exec-narrative.md` |
168
+ | Sponsor's boss needs a summary, board update, justify continued investment | board-memo | `references/board-memo.md` |
170
169
  | Status across all my customers | dashboard | `references/dashboard.md` |
171
170
 
172
171
  ### Close
173
172
 
174
173
  | You hear | Skill | Reference |
175
174
  |----------|-------|-----------|
176
- | Juggling 2+ customers, losing track, context-switching | multi-customer-ops | `references/multi-customer-ops.md` |
175
+ | Juggling 2+ customers, losing track, context-switching | switch-clients | `references/switch-clients.md` |
177
176
  | Wrapping up, handoff, making yourself replaceable | close | `references/close.md` |
178
- | Engagement ending, team needs to operate without you | handoff-engineering | `references/handoff-engineering.md` |
179
- | Something worked well and will apply to future engagements | pattern-extract | `references/pattern-extract.md` |
177
+ | Engagement ending, team needs to operate without you | runbook | `references/runbook.md` |
178
+ | Something worked well and will apply to future engagements | encode-pattern | `references/encode-pattern.md` |
180
179
  | "Red-team this," "stress-test my plan," poke holes, what am I missing | red-team | `references/red-team.md` |
181
180
  | "What did we agree about X?", scope dispute, receipts | - | run `fde receipts <term>`, answer with dates |
182
181
 
@@ -196,7 +195,7 @@ Ready to build with no `terrain.md` / plan: discover or plan first. Takeover wit
196
195
  ## Principles
197
196
 
198
197
  - Never ask the FDE to pick a phase. That's your job.
199
- - Ground loop on a bound client: name → characterise → prove where they live → log. Do not hand the slice to a generic coding pack.
198
+ - Ground loop on a bound client: name → characterise → prove on their staging → go live → log. Do not hand their repo to a generic coding pack.
200
199
  - Read `context.md` before speaking. One sharp question - never a barrage.
201
200
  - Never invent people, meetings, or numbers - `unknown - ask:` beats a polished lie.
202
201
  - Every phase ends with its artifact written. No artifact, no "done."
@@ -47,7 +47,7 @@ When any slice touches a model, embeddings, RAG, or an agent: create or update `
47
47
  4. **Pass/fail** - dated run; Verdict **SHIP** or **NO-SHIP**; critical fails must be 0.
48
48
  5. **HITL gate** - which decisions need human review before action (align with `trust-profile.md`). Empty when policy requires review → NO-SHIP.
49
49
 
50
- **When to write:** plan seeds the pack; sketch/build grows goldens; ship requires Verdict SHIP and a receipt in `delivery.md` → `## Ship receipts`. Non-AI work skips this file entirely.
50
+ **When to write:** plan seeds the pack; poc/build grows goldens; ship requires Verdict SHIP and a receipt in `delivery.md` → `## Ship receipts`. Non-AI work skips this file entirely.
51
51
 
52
52
  ## RAG architecture (retrieval-augmented generation)
53
53
 
@@ -1,4 +1,4 @@
1
- # exec-narrative - the story that gets the next phase funded
1
+ # board-memo - the story that gets the next phase funded
2
2
 
3
3
  **Enter when:** the sponsor's boss needs a summary, a board update mentions the engagement, the FDE needs to justify continued investment, or a quarterly review is approaching.
4
4
 
@@ -1,8 +1,8 @@
1
1
  # business-case - the economics that get the sponsor to say yes
2
2
 
3
- **Enter when:** the sponsor needs justification for the next phase, the FDE needs to defend budget or timeline, a feature decision needs cost/benefit evidence, or sketch produced a direction that needs funding.
3
+ **Enter when:** the sponsor needs justification for the next phase, the FDE needs to defend budget or timeline, a feature decision needs cost/benefit evidence, or poc produced a direction that needs funding.
4
4
 
5
- **Read first:** `reality.md`, `success.md`, `delivery.md`, `context.md`. Load `business-case.md` from sketch if it exists - extend it, don't restart.
5
+ **Read first:** `reality.md`, `success.md`, `delivery.md`, `context.md`. Load `business-case.md` from poc if it exists - extend it, don't restart.
6
6
 
7
7
  Technical FDEs lose engagements by shipping good code without business justification. The sponsor's boss doesn't ask "is the code clean?" - they ask "what did we get for the money?" A business case translates technical work into the language that keeps the engagement alive.
8
8
 
@@ -1,4 +1,4 @@
1
- # ingest-connect - wire a source MCP in plain language
1
+ # connect - wire a source MCP in plain language
2
2
 
3
3
  **Enter when:** the FDE says "I want to connect a new MCP", "connect Granola / Slack / Notion", "how do I pull from …", or a pull request fails because no source tools exist.
4
4