fdeops 3.16.0 → 3.20.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 (51) hide show
  1. package/README.md +82 -84
  2. package/adapters/AGENTS.md +1 -1
  3. package/adapters/GEMINI.md +1 -1
  4. package/adapters/LOCAL-LLM.md +5 -5
  5. package/adapters/README.md +2 -2
  6. package/adapters/copilot-instructions.md +1 -1
  7. package/adapters/cursor.fde.mdc +1 -1
  8. package/bin/check.js +42 -11
  9. package/bin/fde.js +157 -20
  10. package/bin/lib/render.js +2 -2
  11. package/bin/lib/trust.js +42 -5
  12. package/mcp/fdeops-ingest/package.json +1 -1
  13. package/package.json +1 -1
  14. package/plugin.json +1 -1
  15. package/skills/fde/SKILL.md +43 -42
  16. package/skills/fde/references/ai.md +2 -2
  17. package/skills/fde/references/audit.md +2 -2
  18. package/skills/fde/references/board-memo.md +1 -1
  19. package/skills/fde/references/business-case.md +1 -1
  20. package/skills/fde/references/close.md +1 -1
  21. package/skills/fde/references/connect.md +1 -1
  22. package/skills/fde/references/dashboard.md +2 -2
  23. package/skills/fde/references/debrief.md +1 -1
  24. package/skills/fde/references/demo-prep.md +1 -1
  25. package/skills/fde/references/discover.md +34 -15
  26. package/skills/fde/references/earn-trust.md +1 -1
  27. package/skills/fde/references/encode-pattern.md +3 -3
  28. package/skills/fde/references/eval-pack.md +2 -2
  29. package/skills/fde/references/hold-scope.md +1 -1
  30. package/skills/fde/references/ingest.md +1 -1
  31. package/skills/fde/references/land.md +1 -1
  32. package/skills/fde/references/pick-three.md +1 -1
  33. package/skills/fde/references/plan.md +10 -7
  34. package/skills/fde/references/poc.md +4 -2
  35. package/skills/fde/references/readout.md +1 -1
  36. package/skills/fde/references/red-team.md +1 -1
  37. package/skills/fde/references/rescue.md +3 -2
  38. package/skills/fde/references/review.md +1 -1
  39. package/skills/fde/references/rollback.md +1 -1
  40. package/skills/fde/references/runbook.md +1 -1
  41. package/skills/fde/references/score-use-cases.md +1 -1
  42. package/skills/fde/references/ship.md +124 -17
  43. package/skills/fde/references/switch-clients.md +1 -1
  44. package/skills/fde/references/test-assumptions.md +2 -2
  45. package/skills/fde/references/three-options.md +1 -1
  46. package/skills/fde/references/what-breaks.md +1 -1
  47. package/skills/fde/references/who-decides.md +1 -1
  48. package/templates/.fde/assumptions.md +1 -1
  49. package/templates/.fde/reality.md +6 -1
  50. package/templates/.fde/terrain.md +1 -1
  51. package/skills/fde/references/thin-slices.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) {
@@ -738,11 +740,11 @@ function appendLogEntry(eng, type, entry, opts = {}) {
738
740
  // degrading to "nothing found" rather than guessing when the shape does not
739
741
  // match. Never fabricate a number, a name, or a signal that is not in the text.
740
742
 
741
- const PHASES = ['land', 'discover', 'plan', 'ship', 'prove', 'close']
742
- const PHASE_ALIASES = { build: 'ship' } // legacy SDLC name; public map is ship
743
+ const PHASES = ['land', 'discover', 'plan', 'ship', 'outcome', 'close']
744
+ const PHASE_ALIASES = { build: 'ship', prove: 'outcome' } // legacy names; public map is ship / outcome
743
745
  const PHASE_LABELS = {
744
746
  land: 'Land', discover: 'Discover', plan: 'Plan',
745
- ship: 'Ship', prove: 'Prove', close: 'Close',
747
+ ship: 'Ship', outcome: 'Outcome', close: 'Close',
746
748
  }
747
749
  function canonicalPhase(phase) {
748
750
  const p = String(phase).toLowerCase()
@@ -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,11 +1405,14 @@ 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) }
1323
1414
 
1324
- // fde log phase <land|discover|plan|ship|prove|close> - advances portfolio phase
1415
+ // fde log phase <land|discover|plan|ship|outcome|close> - advances portfolio phase (prove → outcome)
1325
1416
  if (type === 'phase') {
1326
1417
  const phase = canonicalPhase((text || '').toLowerCase().trim())
1327
1418
  if (!PHASES.includes(phase)) {
@@ -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}`)
@@ -2129,7 +2237,7 @@ function collectDoctorIssues(eng) {
2129
2237
  }
2130
2238
  }
2131
2239
  const success = readClean(eng, 'success.md')
2132
- if (!firstLine(success, 80)) issues.push('success.md has no stated done-definition - fill before plan/build')
2240
+ if (!firstLine(success, 80)) issues.push('success.md has no stated done-definition - fill before plan/ship')
2133
2241
  const ctxMd = readClean(eng, 'context.md')
2134
2242
  if (!sectionBody(ctxMd, 'Next action', { lastNonEmpty: true })) {
2135
2243
  issues.push('no ## Next action in context.md - Monday morning has nothing to drive')
@@ -2170,7 +2278,7 @@ function collectDoctorIssues(eng) {
2170
2278
  }
2171
2279
  // Failure-path (exception-led operating map): required once past discover.
2172
2280
  // Land seeds; discover fills; plan+ without a real break→owner row is wallpaper.
2173
- if (/^(plan|ship|prove|close)$/.test(s.phase) && !hasOperatingMapContent(eng)) {
2281
+ if (/^(plan|ship|outcome|close)$/.test(s.phase) && !hasOperatingMapContent(eng)) {
2174
2282
  issues.push(
2175
2283
  `phase is ${s.phase} with empty operating map - fill terrain.md ## Operating map (exception-led): break → who notices → workaround → evidence`
2176
2284
  )
@@ -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)
3190
- fde log phase <phase> set engagement phase (land|discover|plan|ship|prove|close)
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
3327
+ fde log phase <phase> set engagement phase (land|discover|plan|ship|outcome|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
  }
@@ -64,9 +98,11 @@ function createTrustApi(deps) {
64
98
  if (!m) return '?'
65
99
  const raw = m[1].replace(/\*/g, '').trim()
66
100
  if (!raw || /\|/.test(raw) || /^unset$/i.test(raw) || /^[\[(]/.test(raw)) return '?'
67
- const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|prove|close)\b/)
101
+ const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|prove|outcome|close)\b/)
68
102
  if (!one) return '?'
69
- return one[1] === 'build' ? 'ship' : one[1]
103
+ if (one[1] === 'build') return 'ship'
104
+ if (one[1] === 'prove') return 'outcome'
105
+ return one[1]
70
106
  }
71
107
 
72
108
  function countOpenRisks(eng) {
@@ -196,6 +232,7 @@ function createTrustApi(deps) {
196
232
 
197
233
  return {
198
234
  stakeholdersMemoryHealth,
235
+ personFromSignalText,
199
236
  signalSubjectKey,
200
237
  parsePhase,
201
238
  countOpenRisks,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.16.0",
3
+ "version": "3.20.0",
4
4
  "private": true,
5
5
  "description": "Thin stdio MCP sink for FDEOps ingest (stage → propose → apply). Zero runtime dependencies.",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.16.0",
3
+ "version": "3.20.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.16.0",
4
+ "version": "3.20.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,13 +1,13 @@
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
7
7
 
8
8
  ## Purpose
9
9
 
10
- The **engagement record** for one client, from first meeting to signed outcome. One skill; six stages (land → close). You route; they never pick a skill. Confirm, then write `.fde/`. The workspace still compiles and commits. `@fde` does not leave.
10
+ The **engagement record** for one client, from first meeting to signed outcome. One skill; six stages (land → close). Same map at any scale, on greenfield or brownfield, in any industry (overlays). You route; they never pick a skill. Confirm, then write `.fde/`. The workspace still compiles and commits. `@fde` does not leave.
11
11
 
12
12
  ## When to use
13
13
 
@@ -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
 
@@ -32,15 +32,15 @@ After a meeting: `fde debrief --smart` → confirm → `--apply`. Walk-in: `fde
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
37
  1. **Name it** in `decisions.md` (plan) or kill it in a day (poc).
38
- 2. **Characterise their code** before you change it (thin-slices). 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.
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
 
@@ -77,7 +77,7 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
77
77
 
78
78
  ## The memory contract
79
79
 
80
- 1. **On entry:** `fde resume` only. Pull other `.fde/` files when the method needs them.
80
+ 1. **On entry:** `fde resume` only. Pull other `.fde/` files when the skill needs them.
81
81
  2. **Deliverable = memory.** The work *is* the `.fde/` file. The reference names which one.
82
82
  3. **Evidence.** Every claim has a source. Traceable beats plausible.
83
83
  4. **No invented facts.** People, quotes, meetings, numbers: they said it or the repo shows it. Else `unknown - ask: <question>`.
@@ -107,84 +107,83 @@ CLI is local (`git` + files, no network). You see their code only when they poin
107
107
 
108
108
  Direct. Their words. No "Certainly." Playback 2-4 lines, then act. One question only when a missing fact changes the next move.
109
109
 
110
- New embed: sprint / standard / programme changes depth, not which methods exist. Before first code: safe place to break things, plus AI-code policy. Before go-live: who needs to know, what's the rollback. Before a sponsor artifact: as-is or gut-check first.
110
+ New embed: sprint / standard / programme changes depth, not which skills exist. Before first code: safe place to break things, plus AI-code policy. Before go-live: who needs to know, what's the rollback. Before a sponsor artifact: as-is or gut-check first.
111
111
 
112
112
  Muddy signal: name it ("discover or rescue - leaning X"). Never a phase-picker interview. Default: land if new, audit if takeover.
113
113
 
114
114
  ## Routing - 6 stages
115
115
 
116
- Read **one** reference and follow it. Do not improvise from memory.
116
+ Work names (engage, diagnose, align, deliver, realize, transfer) are the same map. Read **one** reference and follow it. Do not improvise from memory.
117
117
 
118
118
  ### Land
119
119
 
120
120
  | You hear | Skill | Reference |
121
121
  |----------|-------|-----------|
122
- | Starting fresh, new customer, first meeting, just got the brief | land | `references/land.md` |
122
+ | Engage, onboarding, starting fresh, new customer, first meeting, just got the brief, set product strategy, define success metrics, scope 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 | who-decides | `references/who-decides.md` |
124
+ | Need to understand who matters, who decides, map decision rights, who blocks quietly | who-decides | `references/who-decides.md` |
125
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` |
126
+ | "Also can you…", scope expanding, timeline unchanged, hold scope, scope the brief after kickoff | hold-scope | `references/hold-scope.md` |
127
127
 
128
128
  ### Discover
129
129
 
130
130
  | You hear | Skill | Reference |
131
131
  |----------|-------|-----------|
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…" | 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` |
132
+ | Diagnose, don't know the real problem, brief feels wrong, shadow processes, frame discovery, understand the problem space, data not ready, data estate, catalog the data | discover | `references/discover.md` |
133
+ | The brief feels too neat, assumptions untested, "we just need…", test assumptions | test-assumptions | `references/test-assumptions.md` |
134
+ | Multiple use cases competing, "we want to do everything", score use cases | score-use-cases | `references/score-use-cases.md` |
135
+ | Need to validate a direction, prototype, demo to de-risk, **POC**, spike, killer assumption, validate the solution, build prototype | poc | `references/poc.md` |
136
136
 
137
137
  ### Plan
138
138
 
139
139
  | You hear | Skill | Reference |
140
140
  |----------|-------|-----------|
141
- | Break this down, what order, sequence the build | plan | `references/plan.md` |
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?" | three-options | `references/three-options.md` |
144
- | 20 things are "urgent," need to pick the 3 that matter | pick-three | `references/pick-three.md` |
141
+ | Align, break this down, what order, sequence the build, plan the roadmap, create user stories, write the tasks | plan | `references/plan.md` |
142
+ | Sponsor needs justification, need to defend budget or timeline, build the business case | business-case | `references/business-case.md` |
143
+ | Significant decision, multiple approaches, "what should we do?", generate solutions, generate options | three-options | `references/three-options.md` |
144
+ | 20 things are "urgent," need to pick the 3 that matter, prioritize three | 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 | thin-slices | `references/thin-slices.md` |
151
- | What could go wrong, touching shared infrastructure, need to assess impact | what-breaks | `references/what-breaks.md` |
152
- | 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` |
154
- | Review this change, is it safe, does it match what we agreed | review | `references/review.md` |
150
+ | What could go wrong, touching shared infrastructure, need to assess impact, assess impact, provision, IaC, shared infra | what-breaks | `references/what-breaks.md` |
151
+ | Production down, urgent, fix a prod bug, resolve incident, restore service - OR stakeholder gone quiet, trust slipping | rescue | `references/rescue.md` |
152
+ | Deliver, start building, update their checkout, first module, visible progress, their tests, POC follow-through, ready to deploy, going live, pre-flight, deliver the increment, build the increment, create the launch plan, design their UI | ship | `references/ship.md` |
153
+ | Review this change, review the pull request, 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 | `references/rollback.md` |
156
+ | "We can always revert" - need to actually test the escape route, rehearse rollback | rollback | `references/rollback.md` |
158
157
 
159
- ### Prove
158
+ ### Outcome
160
159
 
161
160
  | You hear | Skill | Reference |
162
161
  |----------|-------|-----------|
163
- | Weekly update due, "need to send the sponsor something" | readout | `references/readout.md` |
164
- | Demo coming up, show-and-tell, exec walkthrough | demo-prep | `references/demo-prep.md` |
165
- | Just out of a meeting, raw notes, "they said…", "debrief" | debrief | the debrief verb (above) + `references/debrief.md` |
162
+ | Realize, weekly update due, "need to send the sponsor something", report the outcome | readout | `references/readout.md` |
163
+ | Demo coming up, show-and-tell, exec walkthrough, prepare the demo | demo-prep | `references/demo-prep.md` |
164
+ | Just out of a meeting, raw notes, "they said…", "debrief", user interviews, workshop notes, capture the meeting | 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
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 | board-memo | `references/board-memo.md` |
170
- | Status across all my customers | dashboard | `references/dashboard.md` |
168
+ | Sponsor's boss needs a summary, board update, brief the board, justify continued investment | board-memo | `references/board-memo.md` |
169
+ | Status across all my customers, view the portfolio | 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 | switch-clients | `references/switch-clients.md` |
177
- | Wrapping up, handoff, making yourself replaceable | close | `references/close.md` |
178
- | Engagement ending, team needs to operate without you | runbook | `references/runbook.md` |
179
- | Something worked well and will apply to future engagements | encode-pattern | `references/encode-pattern.md` |
180
- | "Red-team this," "stress-test my plan," poke holes, what am I missing | red-team | `references/red-team.md` |
175
+ | Juggling 2+ customers, losing track, context-switching, switch engagements | switch-clients | `references/switch-clients.md` |
176
+ | Transfer, wrapping up, handoff, making yourself replaceable, transfer operations | close | `references/close.md` |
177
+ | Engagement ending, team needs to operate without you, write the runbook | runbook | `references/runbook.md` |
178
+ | Something worked well and will apply to future engagements, encode the pattern | encode-pattern | `references/encode-pattern.md` |
179
+ | "Red-team this," "stress-test my plan," poke holes, challenge the plan, 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
 
183
182
  **Overlays - activate alongside any skill on signal, don't wait to be told:**
184
183
 
185
184
  | Signal | Overlay |
186
185
  |--------|---------|
187
- | AI, ML, LLM, model, embeddings, RAG, agents, fine-tuning, inference, drift | `references/ai.md` |
186
+ | AI, ML, LLM, model, embeddings, RAG, agents, fine-tuning, inference, drift, train the model | `references/ai.md` |
188
187
  | Golden set, eval suite, eval pack, pass/fail before AI ship, HITL gate for model, POC the model | `references/eval-pack.md` (+ `ai.md`) |
189
188
  | Deck, slides, report, governance framework, compliance pack, ADR, PDF | `references/artifacts.md` |
190
189
  | Patient data, PHI, HIPAA, EHR, clinical | `references/healthcare.md` |
@@ -196,7 +195,9 @@ 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
+ - Same six stages at any scale. Overlays carry the industry. Greenfield and brownfield change the first move inside ship, not the map.
199
+ - 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
+ - Do not call a change done until the signer in `success.md` can reject it on staging they operate.
200
201
  - Read `context.md` before speaking. One sharp question - never a barrage.
201
202
  - Never invent people, meetings, or numbers - `unknown - ask:` beats a polished lie.
202
203
  - Every phase ends with its artifact written. No artifact, no "done."