fdeops 3.11.1 → 3.15.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.
package/bin/check.js CHANGED
@@ -41,12 +41,11 @@ for (const f of requiredTemplates) {
41
41
  else ok(`template ${f}`)
42
42
  }
43
43
 
44
- const gif = path.join(root, 'media', 'demo.gif')
45
- if (!fs.existsSync(gif) || fs.statSync(gif).size < 1000) {
46
- fail('media/demo.gif missing or too small')
47
- } else {
48
- ok('media/demo.gif')
44
+ const deadMedia = ['demo.gif', 'demo.sh', 'demo.tape', 'terminal-demo.svg', 'fieldbook-dashboard.png', 'fieldbook-detail.png', 'fieldbook-walkthrough.gif']
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`)
49
47
  }
48
+ ok('no staged mock media')
50
49
 
51
50
  for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
52
51
  const skill = path.join(root, 'skills', dir, 'SKILL.md')
@@ -57,10 +56,14 @@ for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
57
56
  }
58
57
  ok('skills structure')
59
58
 
59
+ if (fs.existsSync(path.join(root, 'skills', 'fde', 'archive'))) {
60
+ fail('skills/fde/archive must not exist — unrouted methods are dead code')
61
+ } else ok('no archived skill dump')
62
+
60
63
  // v3: one skill + phase references (progressive disclosure)
61
64
  const requiredReferences = [
62
- 'land.md', 'discover.md', 'audit.md', 'plan.md', 'build.md', 'review.md',
63
- 'debug.md', 'rescue.md', 'ship.md', 'sketch.md', 'close.md', 'dashboard.md',
65
+ 'land.md', 'discover.md', 'audit.md', 'plan.md', 'review.md',
66
+ 'rescue.md', 'ship.md', 'sketch.md', 'close.md', 'dashboard.md',
64
67
  'debrief.md', 'status.md', 'demo-prep.md',
65
68
  'healthcare.md', 'fintech.md', 'gov.md',
66
69
  'ai.md', 'eval-pack.md',
@@ -80,7 +83,7 @@ ok('phase references')
80
83
  // every judgment-heavy reference carries a worked example that names the memory
81
84
  // file the work lands in. Prose-only guidance drifts into advice nobody can apply.
82
85
  const exampleReferences = [
83
- 'land.md', 'discover.md', 'plan.md', 'build.md', 'ship.md', 'close.md',
86
+ 'land.md', 'discover.md', 'plan.md', 'ship.md', 'close.md',
84
87
  'status.md', 'stakeholder-radar.md', 'options-analysis.md', 'business-case.md',
85
88
  'assumption-audit.md', 'scope-defense.md',
86
89
  ]
@@ -118,7 +121,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
118
121
  // Every routing row must parse. A row this misses is a method that could go
119
122
  // undocumented for free, so an unparsed row is a hard failure, not a silent skip.
120
123
  const routed = new Set()
121
- const routing = (router.split('## Routing - 6 domains')[1] || '').split('**Overlays')[0]
124
+ const routing = (router.split(/^## Routing[^\n]*$/m)[1] || '').split('**Overlays')[0]
122
125
  const methodCell = line => (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '')
123
126
  for (const line of routing.split('\n')) {
124
127
  if (!/^\|/.test(line)) continue
@@ -197,6 +200,11 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
197
200
  }
198
201
  }
199
202
  ok(`public method count is verifiable (${documented.size} documented, ${routed.size} routed)`)
203
+
204
+ const refDir = path.join(root, 'skills', 'fde', 'references')
205
+ 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(', ')}`)
207
+ else ok('no unrouted reference files')
200
208
  }
201
209
 
202
210
  const install = read('bin/install.js')
@@ -215,26 +223,20 @@ if (read('package.json').includes('postinstall')) {
215
223
  }
216
224
 
217
225
  const readme = read('README.md')
218
- // The README shows one recording: media/session.gif, a real CLI session.
219
- // media/demo.gif is a hand-typed mock kept for history - it must never be embedded,
220
- // or the front door shows output no command actually produced.
221
- if (readme.includes('demo.gif')) fail('README must not embed media/demo.gif (staged mock, not real CLI output)')
222
- else ok('README no staged demo gif')
223
-
224
- // The recording may live on the front door or one click in (docs/USAGE.md), but
225
- // it must stay reachable and reproducible - an orphaned gif rots silently.
226
- const recordingHosts = ['README.md', 'docs/USAGE.md'].filter(f => read(f).includes('media/session.gif'))
227
- if (!recordingHosts.length) {
228
- fail('media/session.gif must be embedded in README.md or docs/USAGE.md (the recorded session is the proof)')
229
- } else if (!recordingHosts.some(f => read(f).includes('media/record-session.sh'))) {
230
- fail('link media/record-session.sh next to the recording, so it can be re-recorded')
226
+ 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')
228
+ } else ok('README is text (no gif)')
229
+
230
+ const usage = read('docs/USAGE.md')
231
+ if (!usage.includes('media/session.gif') || !usage.includes('media/record-session.sh')) {
232
+ fail('docs/USAGE.md must embed media/session.gif and link media/record-session.sh')
231
233
  } else {
232
234
  const gifPath = path.join(root, 'media', 'session.gif')
233
235
  const rec = path.join(root, 'media', 'record-session.sh')
234
236
  if (!fs.existsSync(gifPath) || fs.statSync(gifPath).size < 50000) fail('media/session.gif missing or too small')
235
237
  else if (!fs.existsSync(rec)) fail('media/record-session.sh missing - the recording must be reproducible')
236
238
  else if (!fs.existsSync(path.join(root, 'media', 'session.cast'))) fail('media/session.cast missing - keep the source recording next to the gif')
237
- else ok(`recorded session in ${recordingHosts.join(' + ')} (gif + reproducible recorder + cast)`)
239
+ else ok('recorded session in docs/USAGE.md (gif + reproducible recorder + cast)')
238
240
  }
239
241
 
240
242
  // Every repo-relative README link and image must resolve, or the front door 404s.
@@ -249,10 +251,10 @@ else ok('README links all resolve')
249
251
 
250
252
  for (const section of [
251
253
  'How it works',
252
- 'Quickstart',
254
+ 'Quick Start',
253
255
  'Engagement memory',
254
256
  'Who this is for',
255
- 'The week',
257
+ 'Commands',
256
258
  'Principles',
257
259
  ]) {
258
260
  if (!readme.includes(section)) fail(`README missing section: ${section}`)
@@ -262,6 +264,16 @@ if (!readme.includes('AI coding agent')) {
262
264
  }
263
265
  ok('README clarity sections')
264
266
 
267
+ for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/got', '/close', '/debrief', '/prep', '/quiet', '/agreed', '/status']) {
268
+ if (!readme.includes(cmd)) fail(`README must document slash command ${cmd}`)
269
+ }
270
+ ok('README slash commands documented')
271
+
272
+ // Front-door map is the embed left-to-right (LAND → CLOSE), not a pile of situations.
273
+ if (!readme.slice(0, 4000).includes('LAND') || !readme.slice(0, 4000).includes('/discover')) {
274
+ fail('README must include the LAND→CLOSE command-map diagram near the top')
275
+ } else ok('README command-map diagram')
276
+
265
277
  if (readme.includes('your-client-repo')) {
266
278
  fail('README must not instruct install in customer repo (your-client-repo)')
267
279
  } else ok('README no customer-repo install')
@@ -286,6 +298,33 @@ if (!read('skills/fde/SKILL.md').includes('npx --yes fdeops')) {
286
298
  fail('skills/fde/SKILL.md must fall back to npx --yes fdeops when the CLI is not installed')
287
299
  } else ok('SKILL.md npx CLI fallback')
288
300
 
301
+ // The frontmatter description is the only text every host reads before deciding
302
+ // to load the skill. If it triggers on "@fde" alone, an FDE who just talks about
303
+ // their client gets no memory - so it must carry natural-intent triggers in the
304
+ // "Use when …" convention, and @fde must be one of several, never the gate.
305
+ {
306
+ const fm = /^---\n([\s\S]*?)\n---/.exec(read('skills/fde/SKILL.md'))
307
+ const desc = fm ? (/^description:[^\S\n]*(.*)$/m.exec(fm[1]) || [])[1] || '' : ''
308
+ const triggers = desc.match(/Use when/g) || []
309
+ if (triggers.length < 4) {
310
+ fail(`SKILL.md description needs several "Use when …" triggers so it fires on intent (found ${triggers.length})`)
311
+ } else if (/Use when the human says @fde or/.test(desc)) {
312
+ 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')
326
+ }
327
+
289
328
  if (!fs.existsSync(path.join(root, 'docs', 'USAGE.md'))) {
290
329
  fail('docs/USAGE.md missing')
291
330
  } else ok('docs/USAGE.md')
@@ -508,6 +547,16 @@ const plugin = JSON.parse(read('.claude-plugin/plugin.json'))
508
547
  if (pkg.version !== plugin.version) {
509
548
  fail(`version mismatch package.json ${pkg.version} vs plugin ${plugin.version}`)
510
549
  } else ok('plugin version aligned')
550
+ if (plugin.commands !== './.claude/commands' || plugin.skills !== './skills') {
551
+ fail('.claude-plugin/plugin.json must declare skills and commands')
552
+ } else {
553
+ for (const cmd of ['brief', 'discover', 'plan', 'ship', 'got', 'close', 'debrief', 'prep', 'quiet', 'agreed', 'status']) {
554
+ const rel = `.claude/commands/${cmd}.md`
555
+ if (!fs.existsSync(path.join(root, rel))) fail(`${rel} missing`)
556
+ else if (!read(rel).includes('@fde')) fail(`${rel} must load @fde`)
557
+ }
558
+ ok('slash commands load @fde')
559
+ }
511
560
 
512
561
  if (!fs.existsSync(path.join(root, 'mcp', 'fdeops-ingest', 'server.js'))) {
513
562
  fail('mcp/fdeops-ingest/server.js missing (ingest MCP sink)')
package/bin/fde.js CHANGED
@@ -18,13 +18,13 @@
18
18
  * fde debrief --smart propose routing from messy notes; --apply commits it
19
19
  * fde prep [label] grounded walk-in brief from existing .fde/ only
20
20
  * fde doctor deterministic memory lint (stale signals, gaps)
21
- * fde garden [--apply] propose safe consolidations; apply only with --apply
21
+ * fde tidy [--apply] propose safe consolidations; apply only with --apply (was: garden)
22
22
  * fde ingest … stage → propose → apply pull sink (.inbox/; never auto-writes .fde/)
23
23
  * fde owner [set …] who keeps this engagement record
24
24
  * fde receipts <term> "what did we agree?" - search memory with dates
25
25
  * fde capture session-end snapshot → context.md (hooks use this)
26
26
  * fde preserve pre-compaction context snapshot (hook-internal; hooks use this)
27
- * fde status [--all] current engagement (default) or full portfolio (--all)
27
+ * fde status [--all] value ledger first, then trust (pass --all for portfolio)
28
28
  * fde dashboard [--all] current engagement fieldbook (default) or all (--all)
29
29
  * fde vault derived Obsidian vault of the fieldbook (disposable; --redacted)
30
30
  */
@@ -738,11 +738,20 @@ function appendLogEntry(eng, type, entry, opts = {}) {
738
738
  // degrading to "nothing found" rather than guessing when the shape does not
739
739
  // match. Never fabricate a number, a name, or a signal that is not in the text.
740
740
 
741
+ const PHASES = ['land', 'discover', 'plan', 'ship', 'prove', 'close']
742
+ const PHASE_ALIASES = { build: 'ship' } // legacy SDLC name; public map is ship
741
743
  const PHASE_LABELS = {
742
- land: 'Embed & Trust', discover: 'Discover & Diagnose', plan: 'Plan & Align',
743
- build: 'Build & Guard', ship: 'Ship & Verify', close: 'Operate & Close',
744
+ land: 'Land', discover: 'Discover', plan: 'Plan',
745
+ ship: 'Ship', prove: 'Prove', close: 'Close',
746
+ }
747
+ function canonicalPhase(phase) {
748
+ const p = String(phase).toLowerCase()
749
+ return PHASE_ALIASES[p] || p
750
+ }
751
+ function phaseLabel(phase) {
752
+ const p = canonicalPhase(phase)
753
+ return PHASE_LABELS[p] || phase
744
754
  }
745
- function phaseLabel(phase) { return PHASE_LABELS[String(phase).toLowerCase()] || phase }
746
755
 
747
756
  // First real (non-blank, non-heading) line of a prose file, bold-label prefix
748
757
  // stripped ("**Confirmed:** text..." -> "text...") - same spirit as the
@@ -1223,7 +1232,7 @@ function cmdResume(args) {
1223
1232
  .filter(d => !d.startsWith('.') && fs.existsSync(path.join(ENGAGEMENTS_ROOT, d, '.fde')))
1224
1233
  .join(', ') || '(none yet)'
1225
1234
  : '(none yet)'
1226
- console.log(`NO ENGAGEMENT for this workspace.\nexisting: ${list}\ncreate + bind one: fde resume --init <client-name>`)
1235
+ console.log(`NO ENGAGEMENT for this workspace.\nexisting: ${list}\nAsk the human the client name (one question), then run: fde resume --init <client-name>\nDo not tell them to type that command.`)
1227
1236
  process.exit(2)
1228
1237
  }
1229
1238
  // Monday-morning: triage + proactive hygiene (silent when clean), then memory.
@@ -1312,11 +1321,11 @@ function cmdLog(args) {
1312
1321
  const eng = resolveEngagement({ forWrite: true })
1313
1322
  if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
1314
1323
 
1315
- // fde log phase <land|discover|plan|build|ship|close> - advances portfolio phase
1324
+ // fde log phase <land|discover|plan|ship|prove|close> - advances portfolio phase
1316
1325
  if (type === 'phase') {
1317
- const phase = (text || '').toLowerCase().trim()
1318
- if (!['land', 'discover', 'plan', 'build', 'ship', 'close'].includes(phase)) {
1319
- console.error('usage: fde log phase <land|discover|plan|build|ship|close>')
1326
+ const phase = canonicalPhase((text || '').toLowerCase().trim())
1327
+ if (!PHASES.includes(phase)) {
1328
+ console.error(`usage: fde log phase <${PHASES.join('|')}>`)
1320
1329
  process.exit(1)
1321
1330
  }
1322
1331
  const hash = setContextPhase(eng, phase)
@@ -1324,7 +1333,7 @@ function cmdLog(args) {
1324
1333
  return
1325
1334
  }
1326
1335
 
1327
- if (!LOG_FILES[type] || !text) { console.error('usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green] [--force]\n fde log phase <land|discover|plan|build|ship|close>\n fde log --undo'); process.exit(1) }
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) }
1328
1337
  if (signal && type !== 'contact') { console.error('--signal only applies to: fde log contact'); process.exit(1) }
1329
1338
  const hit = findSecretHit(text)
1330
1339
  if (hit && !force) { refuseSecret('log text', hit); process.exit(1) }
@@ -2091,7 +2100,7 @@ function collectDoctorIssues(eng) {
2091
2100
 
2092
2101
  if (s.phase === '?' || s.phase === 'unset') {
2093
2102
  if (hasDatedWork) {
2094
- issues.push('phase is unset but dated work exists - run: fde log phase <land|discover|plan|build|ship|close>')
2103
+ issues.push(`phase is unset but dated work exists - run: fde log phase <${PHASES.join('|')}>`)
2095
2104
  }
2096
2105
  }
2097
2106
  if (s.stale) issues.push(`trust signal is STALE (${s.signalAge}d) - reconfirm with fde log contact ... --signal`)
@@ -2161,7 +2170,7 @@ function collectDoctorIssues(eng) {
2161
2170
  }
2162
2171
  // Failure-path (exception-led operating map): required once past discover.
2163
2172
  // Land seeds; discover fills; plan+ without a real break→owner row is wallpaper.
2164
- if (/^(plan|build|ship|close)$/.test(s.phase) && !hasOperatingMapContent(eng)) {
2173
+ if (/^(plan|ship|prove|close)$/.test(s.phase) && !hasOperatingMapContent(eng)) {
2165
2174
  issues.push(
2166
2175
  `phase is ${s.phase} with empty operating map - fill terrain.md ## Operating map (exception-led): break → who notices → workaround → evidence`
2167
2176
  )
@@ -2295,21 +2304,59 @@ function hasValueBucket(eng) {
2295
2304
  const PENDING_CELL_RE =
2296
2305
  /^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not measured|\?+|\.{2,}|…|-+|—+|–+)(?:[^\w].*)?$/i
2297
2306
 
2298
- function claimedValueRows(eng) {
2307
+ function parseValueLedger(eng) {
2299
2308
  const ledger = stripTemplateNoise(sectionBody(readClean(eng, 'delivery.md'), 'Value ledger') || '')
2300
2309
  const table = parseMdTable(ledger)
2301
- if (!table) return { claimed: 0, columnMissing: false }
2302
- const mIdx = colIndex(table.headers, /measured/i)
2303
- if (mIdx === -1) return { claimed: 0, columnMissing: false }
2304
- const aIdx = colIndex(table.headers, /accept/i)
2305
- let claimed = 0
2310
+ if (!table) return { rows: [], columnMissing: false }
2311
+ const idx = {
2312
+ slice: colIndex(table.headers, /slice/i),
2313
+ promised: colIndex(table.headers, /promised/i),
2314
+ measured: colIndex(table.headers, /measured/i),
2315
+ accepted: colIndex(table.headers, /accept/i),
2316
+ }
2317
+ const cell = (row, i) => (i === -1 ? '' : String(row[i] || '').trim())
2318
+ const rows = []
2306
2319
  for (const row of table.rows) {
2307
- const measured = String(row[mIdx] || '').trim()
2308
- if (!measured || PENDING_CELL_RE.test(measured)) continue
2309
- const accepted = aIdx === -1 ? '' : String(row[aIdx] || '').trim()
2310
- if (!accepted || PENDING_CELL_RE.test(accepted)) claimed++
2320
+ const slice = cell(row, idx.slice)
2321
+ const promised = cell(row, idx.promised)
2322
+ const measured = cell(row, idx.measured)
2323
+ const accepted = cell(row, idx.accepted)
2324
+ if (!slice && !promised && !measured) continue
2325
+ const measuredPending = !measured || PENDING_CELL_RE.test(measured)
2326
+ const acceptedPending = idx.accepted === -1 || !accepted || PENDING_CELL_RE.test(accepted)
2327
+ let state = 'unmeasured'
2328
+ if (!measuredPending && acceptedPending) state = 'claimed'
2329
+ else if (!measuredPending) state = 'accepted'
2330
+ rows.push({ slice, promised, measured, accepted, state })
2311
2331
  }
2312
- return { claimed, columnMissing: aIdx === -1 }
2332
+ return { rows, columnMissing: idx.accepted === -1 }
2333
+ }
2334
+
2335
+ function claimedValueRows(eng) {
2336
+ const { rows, columnMissing } = parseValueLedger(eng)
2337
+ return { claimed: rows.filter(r => r.state === 'claimed').length, columnMissing }
2338
+ }
2339
+
2340
+ function formatValueLedgerLine(r) {
2341
+ const name = r.slice || 'value'
2342
+ let body = r.promised || ''
2343
+ if (r.state !== 'unmeasured' && r.measured) {
2344
+ if (!body) body = r.measured
2345
+ else if (!body.includes(r.measured)) body = `${body} → ${r.measured}`
2346
+ }
2347
+ const head = body ? `${name}: ${body}` : name
2348
+ if (r.state === 'accepted') return `${head} · accepted by ${r.accepted}`
2349
+ if (r.state === 'claimed') return `${head} · claimed, not yet accepted`
2350
+ return `${head} · not yet measured`
2351
+ }
2352
+
2353
+ function valueLedgerStatusLines(eng, opts = {}) {
2354
+ const { rows } = parseValueLedger(eng)
2355
+ if (!rows.length) return [' value: none yet']
2356
+ const cap = opts.compact ? 1 : 8
2357
+ const lines = rows.slice(0, cap).map(r => ` ${formatValueLedgerLine(r)}`)
2358
+ if (rows.length > cap) lines.push(` … ${rows.length - cap} more in delivery.md`)
2359
+ return lines
2313
2360
  }
2314
2361
 
2315
2362
  // AI in scope for ship/close hygiene — delivery/decisions/trust evidence only.
@@ -2488,18 +2535,18 @@ function cmdGarden(args) {
2488
2535
  // Gardener contract (from Rowboat note_curation): no new facts, no deleted substance,
2489
2536
  // reversible via git when healthy, confirm before apply. Mechanical only - no LLM rewrite.
2490
2537
  if (gitHealth.ok) {
2491
- console.log('GARDEN (contract: no new facts · no deleted substance · reversible via memory git)')
2538
+ console.log('TIDY (contract: no new facts · no deleted substance · reversible via memory git)')
2492
2539
  } else if (gitHealth.reason === 'broken') {
2493
- console.log('GARDEN (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)')
2494
2541
  } else {
2495
- console.log('GARDEN (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)')
2496
2543
  }
2497
2544
  console.log(resumeTriage(eng))
2498
2545
  if (!gitHealth.ok) {
2499
2546
  console.log(
2500
2547
  gitHealth.reason === 'broken'
2501
- ? '\n⚠ ledger is UNVERSIONED (corrupt .git). Repair before trusting garden apply: mv .fde/.git .fde/.git.broken && run any fde write to re-init.'
2502
- : '\n⚠ no memory git — garden apply cannot create a reversible commit until the ledger exists.'
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.'
2503
2550
  )
2504
2551
  }
2505
2552
  const proposals = []
@@ -2539,18 +2586,18 @@ function cmdGarden(args) {
2539
2586
  })
2540
2587
  }
2541
2588
  if (!proposals.length) {
2542
- console.log('\nNothing to garden.')
2589
+ console.log('\nNothing to tidy.')
2543
2590
  return
2544
2591
  }
2545
2592
  console.log(`\n${proposals.length} proposal(s):`)
2546
2593
  proposals.forEach((p, i) => console.log(` ${i + 1}. [${p.kind}] ${p.text}`))
2547
2594
  if (!apply) {
2548
- console.log('\nApply mechanical items only: fde garden --apply')
2595
+ console.log('\nApply mechanical items only: fde tidy --apply')
2549
2596
  console.log('Manual items stay yours. Every apply commits to memory git when the ledger is healthy.')
2550
2597
  return
2551
2598
  }
2552
2599
  if (!gitHealth.ok && gitHealth.reason === 'broken') {
2553
- console.error('refusing garden --apply while memory git is broken - repair the ledger first')
2600
+ console.error('refusing tidy --apply while memory git is broken - repair the ledger first')
2554
2601
  process.exit(1)
2555
2602
  }
2556
2603
  ensureMemoryGit(eng)
@@ -2603,9 +2650,9 @@ function cmdGarden(args) {
2603
2650
  touched.add('context-archive.md')
2604
2651
  console.log(`applied: archived ${p.sessionBlocks.length} old session-end blocks → context-archive.md`)
2605
2652
  }
2606
- const hash = commitMemory(eng, 'garden', { files: [...touched] })
2653
+ const hash = commitMemory(eng, 'tidy', { files: [...touched] })
2607
2654
  if (!applied) console.log('no mechanical proposals applied (manual items remain)')
2608
- else console.log(`garden done${hash ? ` @${hash}` : ''}`)
2655
+ else console.log(`tidy done${hash ? ` @${hash}` : ''}`)
2609
2656
  }
2610
2657
 
2611
2658
  // Keep the first open-risk bullet per fingerprint; move later echoes under ## Retired.
@@ -2647,7 +2694,7 @@ function applyRiskDedupe(eng, clusters) {
2647
2694
  const stamp = new Date().toISOString().slice(0, 10)
2648
2695
  const block = retiredLines.map(l => {
2649
2696
  const body = l.trim().replace(/^-\s*/, '')
2650
- return `- [${stamp}] (garden dedupe) ${body}`
2697
+ return `- [${stamp}] (tidy dedupe) ${body}`
2651
2698
  }).join('\n')
2652
2699
  out = appendUnderSection(out, 'Retired', block)
2653
2700
  withFileLock(p, () => { atomicWriteFile(p, out.endsWith('\n') ? out : out + '\n') })
@@ -2669,7 +2716,7 @@ function cmdStatus(args) {
2669
2716
  if (!fs.existsSync(eng)) continue
2670
2717
  const s = computeSignals(eng)
2671
2718
  const note = [s.memoryWarn, (s.dirtyFiles && s.dirtyFiles.length) ? `dirty:${s.dirtyFiles.length}` : '', s.reason || s.topRisk].filter(Boolean).join(' · ').slice(0, 70)
2672
- rows.push({ name: d, phase: s.phase, trust: s.trust, signalAge: s.signalAge, stale: s.stale, updated: s.updated, reason: note, memoryWarn: s.memoryWarn, dirtyFiles: s.dirtyFiles })
2719
+ rows.push({ name: d, phase: s.phase, trust: s.trust, signalAge: s.signalAge, stale: s.stale, updated: s.updated, reason: note, memoryWarn: s.memoryWarn, dirtyFiles: s.dirtyFiles, valueLines: valueLedgerStatusLines(eng, { compact: true }) })
2673
2720
  }
2674
2721
  } else {
2675
2722
  const eng = resolveEngagement()
@@ -2679,13 +2726,14 @@ function cmdStatus(args) {
2679
2726
  }
2680
2727
  const s = computeSignals(eng)
2681
2728
  const note = [s.memoryWarn, (s.dirtyFiles && s.dirtyFiles.length) ? `dirty:${s.dirtyFiles.length}` : '', s.reason || s.topRisk].filter(Boolean).join(' · ').slice(0, 70)
2682
- rows.push({ name: engagementSlugFromPath(eng), phase: s.phase, trust: s.trust, signalAge: s.signalAge, stale: s.stale, updated: s.updated, reason: note, memoryWarn: s.memoryWarn, dirtyFiles: s.dirtyFiles })
2729
+ rows.push({ name: engagementSlugFromPath(eng), phase: s.phase, trust: s.trust, signalAge: s.signalAge, stale: s.stale, updated: s.updated, reason: note, memoryWarn: s.memoryWarn, dirtyFiles: s.dirtyFiles, valueLines: valueLedgerStatusLines(eng) })
2683
2730
  }
2684
2731
  if (!rows.length) { console.log('no engagements yet'); return }
2685
2732
  const order = { RED: 0, amber: 1, green: 2 }
2686
2733
  rows.sort((a, b) => order[a.trust] - order[b.trust])
2687
- console.log((all ? 'FDE PORTFOLIO' : 'FDE STATUS') + ' - trust-first triage (heuristic: red > amber > green)\n')
2734
+ console.log((all ? 'FDE PORTFOLIO' : 'FDE STATUS') + ' - value first, then trust\n')
2688
2735
  for (const r of rows) {
2736
+ for (const line of r.valueLines) console.log(line)
2689
2737
  // "amber?" = structured signal went stale (>21d) - reconfirm before trusting it
2690
2738
  const label = r.trust + (r.stale ? '?' : '')
2691
2739
  const sig = r.signalAge != null ? `signal ${r.signalAge}d old${r.stale ? ' (STALE - reconfirm)' : ''} ` : ''
@@ -3139,7 +3187,7 @@ function printUsage() {
3139
3187
  fde resume --bind show what this workspace is bound to, and what resolves
3140
3188
  fde triage TRIAGE block only (hooks / Cursor session entry)
3141
3189
  fde log <type> <text> append decision|risk|delivery|contact (contact takes --signal red|amber|green; --force to allow secret-like text)
3142
- fde log phase <phase> set engagement phase (land|discover|plan|build|ship|close)
3190
+ fde log phase <phase> set engagement phase (land|discover|plan|ship|prove|close)
3143
3191
  fde log --undo remove the last CLI log/debrief entry from memory
3144
3192
  fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
3145
3193
  fde debrief --smart heuristic propose (prefix + light keywords); agent routes, CLI gates → --apply
@@ -3150,14 +3198,13 @@ function printUsage() {
3150
3198
  fde prep [label] grounded walk-in brief from existing .fde/ only
3151
3199
  fde doctor lint engagement memory (stale signals, gaps)
3152
3200
  fde redact <term> preview/remove lines containing a buried term (pass --apply to commit)
3153
- fde garden [--apply] propose safe consolidations (contract: no new facts; git-reversible)
3201
+ fde tidy [--apply] propose safe consolidations (contract: no new facts; git-reversible)
3154
3202
  fde owner [set email] who keeps this engagement record
3155
3203
  fde receipts <term> "what did we agree?" with dates
3156
- fde capture session-end memory snapshot (hooks use this)
3157
- fde preserve pre-compaction context snapshot (hook-internal; hooks use this)
3158
- fde status [--all] current engagement status (pass --all for full portfolio)
3204
+ fde status [--all] value ledger, then trust (pass --all for full portfolio)
3159
3205
  fde dashboard [--all] current engagement fieldbook (pass --all for every client)
3160
3206
  fde vault derived Obsidian vault of every engagement (--current for one, --redacted for a shared screen, --out <dir>)
3207
+ hooks call these; you do not: capture (session-end snapshot), preserve (pre-compaction snapshot)
3161
3208
  env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
3162
3209
  writes require a workspace bind (or FDEOPS_ENGAGEMENT) - folder-name match is read-only
3163
3210
  .fde/ is git-versioned locally for tamper-evident receipts (no remote, no telemetry)
@@ -3176,6 +3223,8 @@ switch (cmd) {
3176
3223
  case 'prep': cmdPrep(args); break
3177
3224
  case 'doctor': cmdDoctor(); break
3178
3225
  case 'redact': cmdRedact(args); break
3226
+ // `garden` was the name through 3.11.x; it keeps working.
3227
+ case 'tidy':
3179
3228
  case 'garden': cmdGarden(args); break
3180
3229
  case 'owner': cmdOwner(args); break
3181
3230
  case 'receipts': cmdReceipts(args); break
package/bin/install.js CHANGED
@@ -355,7 +355,7 @@ function cmdInstall(opts = {}) {
355
355
  // through to the CLI (fde.js reads process.argv itself, so require() is enough).
356
356
  const FDE_SUBCOMMANDS = [
357
357
  'demo', 'scan', 'resume', 'triage', 'log', 'debrief', 'ingest', 'prep', 'doctor', 'redact',
358
- 'garden', 'owner', 'receipts', 'capture', 'preserve', 'status', 'dashboard', 'vault', 'help',
358
+ 'tidy', 'garden', 'owner', 'receipts', 'capture', 'preserve', 'status', 'dashboard', 'vault', 'help',
359
359
  ]
360
360
 
361
361
  const INSTALL_SUBCOMMANDS = ['init', 'adapters', 'install']
package/bin/lib/trust.js CHANGED
@@ -64,8 +64,9 @@ function createTrustApi(deps) {
64
64
  if (!m) return '?'
65
65
  const raw = m[1].replace(/\*/g, '').trim()
66
66
  if (!raw || /\|/.test(raw) || /^unset$/i.test(raw) || /^[\[(]/.test(raw)) return '?'
67
- const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|close)\b/)
68
- return one ? one[1] : '?'
67
+ const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|prove|close)\b/)
68
+ if (!one) return '?'
69
+ return one[1] === 'build' ? 'ship' : one[1]
69
70
  }
70
71
 
71
72
  function countOpenRisks(eng) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.11.1",
3
+ "version": "3.15.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,7 +1,7 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.11.1",
4
- "description": "Field kit for engineers embedded in client work - a real CLI (recon, memory, portfolio), one @fde skill with field judgment on top, and hooks that make it automatic. Claude Code plugin and any agent that loads skills.",
3
+ "version": "3.15.0",
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",
7
7
  "fde": "bin/fde.js"
@@ -30,6 +30,9 @@
30
30
  "ai-agent",
31
31
  "skills",
32
32
  "plugin",
33
+ "engagement-memory",
34
+ "agent-memory",
35
+ "client-context",
33
36
  "fde",
34
37
  "forward-deployed",
35
38
  "forward-deployed-engineer",
package/plugin.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "fdeops",
4
- "version": "3.11.1",
5
- "description": "Engagement fieldbook for Forward Deployed Engineers: per-client memory in local .fde/ files, one @fde skill, land to close methodology. Local-only, no network.",
4
+ "version": "3.15.0",
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",
8
8
  "url": "https://github.com/suboss87"
9
9
  },
10
- "homepage": "https://fdeops.io/",
10
+ "homepage": "https://github.com/suboss87/fdeops",
11
11
  "repository": "https://github.com/suboss87/FDEOps",
12
12
  "license": "MIT",
13
13
  "keywords": [