fdeops 3.21.0 → 3.22.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/README.md CHANGED
@@ -6,6 +6,8 @@ You're on a customer site. The AI coding agent writes code in their repo. This k
6
6
 
7
7
  Notes stay on your laptop. Their repo stays theirs. You confirm before anything is written down.
8
8
 
9
+ Keep the coding pack you already use. Install this next to it. FDEOps is the client work. The other pack writes the code.
10
+
9
11
  <img width="1536" height="1024" alt="fdeops" src="https://github.com/user-attachments/assets/2bcb8739-55ee-445d-8a1a-8b38433b7b58" />
10
12
 
11
13
  ---
package/bin/check.js CHANGED
@@ -409,6 +409,28 @@ for (const f of exampleFiles) {
409
409
  }
410
410
  ok('examples walkthrough files')
411
411
 
412
+ // The examples are the first .fde/ a newcomer reads. They must pass the kit's
413
+ // own doctor, or the kit is telling people to do what its showcase does not.
414
+ // Tolerated: things a frozen reference copy cannot have (an owner, a memory
415
+ // git, a fresh trust signal).
416
+ {
417
+ const { spawnSync } = require('child_process')
418
+ const tolerated = /no \.owner|not git-versioned|trust signal is STALE/
419
+ for (const ex of fs.readdirSync(path.join(root, 'examples'))) {
420
+ const eng = path.join(root, 'examples', ex, '.fde')
421
+ if (!fs.existsSync(eng)) continue
422
+ const r = spawnSync(process.execPath, [path.join(root, 'bin', 'fde.js'), 'doctor'], {
423
+ encoding: 'utf8',
424
+ env: { ...process.env, FDEOPS_ENGAGEMENT: eng, HOME: fs.mkdtempSync(path.join(require('os').tmpdir(), 'fdeops-check-')) },
425
+ })
426
+ const issues = (r.stdout || '').split('\n')
427
+ .map(l => l.match(/^\s+\d+\.\s+(.*)$/)).filter(Boolean).map(m => m[1])
428
+ .filter(i => !tolerated.test(i))
429
+ if (issues.length) fail(`examples/${ex} fails its own doctor:\n - ${issues.join('\n - ')}`)
430
+ else ok(`examples/${ex} passes fde doctor`)
431
+ }
432
+ }
433
+
412
434
  if (fs.existsSync(path.join(root, 'tasks', 'plan.md'))) {
413
435
  fail('tasks/plan.md should not be in public tree (move to docs/internal)')
414
436
  }
package/bin/fde.js CHANGED
@@ -1515,8 +1515,8 @@ function smartProposeText(input) {
1515
1515
  out.push(`decision: ${bare.replace(/^decided:\s+/i, '')}`)
1516
1516
  continue
1517
1517
  }
1518
- if (/^(decision|risk|delivery|contact|next):\s*/i.test(bare)) {
1519
- let routed = bare.replace(/^(decision|risk|delivery|contact|next):\s*/i, (m, t) => `${t.toLowerCase()}: `)
1518
+ if (/^(decision|risk|delivery|contact|next|signer):\s*/i.test(bare)) {
1519
+ let routed = bare.replace(/^(decision|risk|delivery|contact|next|signer):\s*/i, (m, t) => `${t.toLowerCase()}: `)
1520
1520
  if (/^contact:/i.test(routed) && !/\[signal:(red|amber|green)\]/i.test(routed)) {
1521
1521
  const sig = inferContactSignal(routed)
1522
1522
  if (sig) routed = routed.replace(/\s*$/, ` [signal:${sig}]`)
@@ -1524,6 +1524,12 @@ function smartProposeText(input) {
1524
1524
  out.push(routed)
1525
1525
  continue
1526
1526
  }
1527
+ // Sentence-level: a signer named mid-paragraph gets its own routed line and
1528
+ // the original stays as context, so nothing is invented or lost.
1529
+ for (const sentence of bare.split(/(?<=[.!?])\s+/)) {
1530
+ const who = signerFromLine(sentence)
1531
+ if (who) { out.push(`signer: ${who}`); break }
1532
+ }
1527
1533
  if (/^(next action|follow-?ups?|action items?|todo):\s*/i.test(bare) ||
1528
1534
  /\b(next action|walk in with|follow up with)\b/i.test(bare)) {
1529
1535
  const next = bare.replace(/^(next action|follow-?ups?|action items?|todo):\s*/i, '').trim()
@@ -1549,6 +1555,56 @@ function smartProposeText(input) {
1549
1555
  return out.join('\n') + (out.length ? '\n' : '')
1550
1556
  }
1551
1557
 
1558
+ // "Priya signs off" is the most expensive sentence in a kickoff and used to land
1559
+ // in context.md as a note. signer: fills the success.md line the whole kit
1560
+ // keys on, and logs the person as a contact so prep/status can see them.
1561
+ const SIGNER_RX = /^(?<who>[A-Z][\w.'-]+(?:\s+[A-Z][\w.'-]+){0,3}(?:\s*\([^)]{1,40}\))?)\s+(?:signs?(?:\s+off)?|approves|has (?:the )?final say|can say yes|owns the decision|is the (?:sponsor|signer|decision[- ]maker))\b/
1562
+
1563
+ function signerFromLine(text) {
1564
+ const t = String(text || '').trim()
1565
+ const m = t.match(SIGNER_RX)
1566
+ if (!m) return ''
1567
+ const who = m.groups.who.trim()
1568
+ // "Staging exists" / "The API is slow" also match "Capital Word + verb"; a
1569
+ // sentence-initial common noun is not a person.
1570
+ if (/^(The|This|That|It|We|They|Staging|Budget|Prod|Production|Nobody|Someone|Everyone)\b/.test(who)) return ''
1571
+ return who
1572
+ }
1573
+
1574
+ function setSigner(eng, who) {
1575
+ ensureMemoryGit(eng)
1576
+ const p = path.join(eng, 'success.md')
1577
+ let md = readEng(eng, 'success.md')
1578
+ if (!md) md = '# Success definition\n\n'
1579
+ const norm = (s) => String(s).replace(/\s+/g, ' ').trim().toLowerCase()
1580
+ const line = /^\*\*Stakeholder who signs off:\*\*\s*(.*)$/m
1581
+ const m = md.match(line)
1582
+ if (m && !m[1].trim()) {
1583
+ md = md.replace(line, `**Stakeholder who signs off:** ${who}`)
1584
+ } else if (m) {
1585
+ // Whole-name compare against the primary and every "also named" line under
1586
+ // it: "Sam" must not vanish inside "Samantha", and re-applying must not
1587
+ // stack duplicates.
1588
+ const start = md.indexOf(m[0]) + m[0].length
1589
+ const alsoNamed = []
1590
+ for (const l of md.slice(start).split('\n').slice(1)) {
1591
+ const a = l.match(/^- also named:\s*(.+)$/)
1592
+ if (!a) break
1593
+ alsoNamed.push(a[1])
1594
+ }
1595
+ const known = [m[1], ...alsoNamed].map(norm)
1596
+ if (known.includes(norm(who))) return false
1597
+ // A second, different name is a fact worth keeping next to the first, not
1598
+ // a silent overwrite - who signs is exactly the thing people argue about.
1599
+ const block = [m[0], ...alsoNamed.map(a => `- also named: ${a}`)].join('\n')
1600
+ md = md.replace(block, `${block}\n- also named: ${who}`)
1601
+ } else {
1602
+ md = md.replace(/\n*$/, `\n\n**Stakeholder who signs off:** ${who}\n`)
1603
+ }
1604
+ withFileLock(p, () => { atomicWriteFile(p, md.endsWith('\n') ? md : md + '\n') })
1605
+ return true
1606
+ }
1607
+
1552
1608
  function setNextAction(eng, text) {
1553
1609
  ensureMemoryGit(eng)
1554
1610
  const bullet = `- ${stripControlChars(String(text).replace(/^[-*]\s+/, '').trim())}`
@@ -1665,7 +1721,7 @@ function readSealedProposal(eng) {
1665
1721
  function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
1666
1722
  const d = new Date()
1667
1723
  const date = d.toISOString().slice(0, 10)
1668
- const counts = { decision: 0, risk: 0, delivery: 0, contact: 0, next: 0 }
1724
+ const counts = { decision: 0, risk: 0, delivery: 0, contact: 0, next: 0, signer: 0 }
1669
1725
  const ctxLines = []
1670
1726
  let nextAction = ''
1671
1727
  ensureMemoryGit(eng)
@@ -1678,8 +1734,8 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
1678
1734
  for (const raw of routable.split('\n')) {
1679
1735
  let line = raw.trim()
1680
1736
  if (!line) continue
1681
- const bare = line.replace(/^[-*+]\s+/, '').replace(/^\*\*(decision|risk|delivery|contact|next):?\*\*:?\s*/i, '$1: ')
1682
- const m = bare.match(/^(decision|risk|delivery|contact|next):\s*(.+)$/i)
1737
+ const bare = line.replace(/^[-*+]\s+/, '').replace(/^\*\*(decision|risk|delivery|contact|next|signer):?\*\*:?\s*/i, '$1: ')
1738
+ const m = bare.match(/^(decision|risk|delivery|contact|next|signer):\s*(.+)$/i)
1683
1739
  if (m) {
1684
1740
  const type = m[1].toLowerCase()
1685
1741
  let body = m[2]
@@ -1688,6 +1744,18 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
1688
1744
  console.error(`skipped ${type} line - looks like a ${hit}. Redact it, or re-run with --force.`)
1689
1745
  continue
1690
1746
  }
1747
+ if (type === 'signer') {
1748
+ const who = body.replace(/\s+signs?(?:\s+off)?\b.*$/i, '').trim() || body.trim()
1749
+ if (dry) {
1750
+ console.log(`→ success.md **Stakeholder who signs off:** ${previewLine(who)}`)
1751
+ console.log(`→ stakeholders.md ${previewLine(datedEntry(eng, date, `${who} signs off`))}`)
1752
+ } else {
1753
+ setSigner(eng, who)
1754
+ appendLogEntry(eng, 'contact', datedEntry(eng, date, `${who} signs off`), { skipCommit: true })
1755
+ }
1756
+ counts.signer++
1757
+ continue
1758
+ }
1691
1759
  if (type === 'next') {
1692
1760
  if (dry) console.log(`→ context.md ## Next action - ${previewLine(body)}`)
1693
1761
  else nextAction = body
@@ -1760,7 +1828,7 @@ function cmdDebrief(args) {
1760
1828
  if (smart) {
1761
1829
  const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
1762
1830
  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:')
1831
+ console.log('Prefix vocabulary (lines that route): decision: risk: delivery: contact: next: signer:')
1764
1832
  console.log('Everything else → context.md. Keep the prefixes; the preview gate stays.\n')
1765
1833
  routeDebriefInput(eng, clean, { dry: true, force, sealed: blocks })
1766
1834
  if (!apply) {
@@ -1776,7 +1844,7 @@ function cmdDebrief(args) {
1776
1844
  const { counts, ctxLines, privateBlocks } = routeDebriefInput(eng, input, { dry, force, sealed })
1777
1845
  if (!dry) {
1778
1846
  const hash = commitMemory(eng, 'debrief', {
1779
- files: ['decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md', 'context.md', SIGNAL_LEDGER],
1847
+ files: ['decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md', 'success.md', 'context.md', SIGNAL_LEDGER],
1780
1848
  })
1781
1849
  try { fs.unlinkSync(path.join(eng, DEBRIEF_PROPOSE)) } catch (_) {}
1782
1850
  try { fs.unlinkSync(path.join(eng, DEBRIEF_PRIVATE)) } catch (_) {}
@@ -1784,7 +1852,7 @@ function cmdDebrief(args) {
1784
1852
  if (hash) console.log(`memory @${hash}`)
1785
1853
  }
1786
1854
  const plural = {
1787
- decision: 'decisions', risk: 'risks', delivery: 'deliveries', contact: 'contacts', next: 'next actions',
1855
+ decision: 'decisions', risk: 'risks', delivery: 'deliveries', contact: 'contacts', next: 'next actions', signer: 'signers',
1788
1856
  }
1789
1857
  const parts = Object.keys(counts).filter(t => counts[t])
1790
1858
  .map(t => `${counts[t]} ${counts[t] === 1 ? (t === 'next' ? 'next action' : t) : plural[t]}`)
@@ -2176,6 +2244,66 @@ function findDuplicateOpenRisks(eng) {
2176
2244
  return [...byKey.values()].filter(g => g.length >= 2)
2177
2245
  }
2178
2246
 
2247
+ // The bound client repo moved and delivery.md did not. This is the one place
2248
+ // the CLI can catch "we shipped code and told the record nothing" without AI:
2249
+ // registry gives the workspace(s) bound to this engagement, git gives commits
2250
+ // newer than the last dated delivery line. Local reads only.
2251
+ // Only dates that stamp an entry count: a ledger row's Date cell, a dated
2252
+ // bullet, a dated heading. "trial night 2026-06-02" inside a Measured cell is a
2253
+ // promise, not a receipt - and a future promise must not hide today's commits.
2254
+ function latestDeliveryEntry(md) {
2255
+ const today = new Date().toISOString().slice(0, 10)
2256
+ let latest = { date: '', line: '' }
2257
+ for (const raw of stripTemplateNoise(md).split('\n')) {
2258
+ const t = raw.trim()
2259
+ const m = t.match(/^[-*]\s*\[(\d{4}-\d{2}-\d{2})\]/) ||
2260
+ t.match(/^#{1,6}\s+\[?(\d{4}-\d{2}-\d{2})(?:\]|\b)/) ||
2261
+ t.match(/^\|\s*(\d{4}-\d{2}-\d{2})\s*\|/)
2262
+ if (!m || m[1] > today) continue
2263
+ if (m[1] >= latest.date) latest = { date: m[1], line: t }
2264
+ }
2265
+ return latest
2266
+ }
2267
+
2268
+ function silentCommitIssues(eng) {
2269
+ const slug = path.basename(path.dirname(eng))
2270
+ const workspaces = readRegistry().filter(r => r.slug === slug).map(r => r.workspace)
2271
+ if (!workspaces.length) return []
2272
+ const entry = latestDeliveryEntry(readClean(eng, 'delivery.md'))
2273
+ const lastDelivery = entry.date
2274
+ const out = []
2275
+ for (const ws of workspaces) {
2276
+ let st
2277
+ try { st = fs.statSync(ws) } catch (_) { continue }
2278
+ if (!st.isDirectory()) continue
2279
+ // The memory folder is itself a git repo; never lint it as the client repo.
2280
+ if (path.resolve(ws) === path.resolve(eng) || path.resolve(ws) === path.dirname(path.resolve(eng))) continue
2281
+ if (sh('git rev-parse --is-inside-work-tree', ws) !== 'true') continue
2282
+ // Memory git knows the exact moment that entry was written; dates in the
2283
+ // file are day-grained and would miss a commit made later the same day.
2284
+ // Pickaxe on the entry text, not the file: a later status edit to
2285
+ // delivery.md must not become the cutoff and hide commits before it.
2286
+ const raw = entry.line
2287
+ ? sh(`git log -1 --format=%cI -S${JSON.stringify(entry.line)} -- delivery.md`, eng)
2288
+ : ''
2289
+ // git --since is inclusive at second grain; a commit in the same second as
2290
+ // the receipt is the receipt's own work, not a silent one.
2291
+ const stamp = raw && !Number.isNaN(Date.parse(raw)) ? new Date(Date.parse(raw) + 1000).toISOString() : ''
2292
+ const since = stamp ? `--since="${stamp}"`
2293
+ : lastDelivery ? `--since="${lastDelivery} 23:59:59"`
2294
+ : "--since='30 days ago'"
2295
+ const commits = sh(`git log ${since} --format=%h -- .`, ws).split('\n').filter(Boolean).length
2296
+ if (!commits) continue
2297
+ const where = path.basename(ws)
2298
+ out.push(
2299
+ lastDelivery
2300
+ ? `${commits} commit(s) in ${where} since the last delivery line (${lastDelivery}) - code moved, ledger did not; log the receipt or say why nothing shipped`
2301
+ : `${commits} commit(s) in ${where} in 30d and delivery.md has no dated line - code moved, ledger did not; fde log delivery "slice | bucket | promised | measured | accepted | evidence | rollback"`
2302
+ )
2303
+ }
2304
+ return out
2305
+ }
2306
+
2179
2307
  // Deterministic fieldbook hygiene - shared by doctor + session TRIAGE.
2180
2308
  // Silent when clean OR brand-new (no dated work yet). Never auto-rewrites.
2181
2309
  // High-value moments: week-start (via triage), ship/close, after real work accrues.
@@ -2246,9 +2374,11 @@ function collectDoctorIssues(eng) {
2246
2374
  'duplicate ## Next action headings in context.md - fill the first (template) section and remove extras; triage reads the last non-empty'
2247
2375
  )
2248
2376
  }
2249
- if ((s.phase === 'close' || s.phase === 'ship') && s.openRisks > 0) {
2377
+ // Open, owned risks are normal mid-ship (triage already shows the count every
2378
+ // session). The gate is close: nothing still live when you call the embed done.
2379
+ if (s.phase === 'close' && s.openRisks > 0) {
2250
2380
  issues.push(
2251
- `phase is ${s.phase} with ${s.openRisks} open risk(s) - retire, hand off, or move still-live ones before calling the embed done`
2381
+ `phase is close with ${s.openRisks} open risk(s) - retire, hand off, or move still-live ones before calling the embed done`
2252
2382
  )
2253
2383
  }
2254
2384
  if (s.phase === 'close' || s.phase === 'ship') {
@@ -2268,6 +2398,7 @@ function collectDoctorIssues(eng) {
2268
2398
  `phase is ${s.phase} with AI in scope but no eval receipt (evals.md Verdict or delivery Eval / Ship receipts) - required before green ship/close`
2269
2399
  )
2270
2400
  }
2401
+ issues.push(...silentCommitIssues(eng))
2271
2402
  }
2272
2403
  const dupes = findDuplicateOpenRisks(eng)
2273
2404
  if (dupes.length) {
package/bin/lib/trust.js CHANGED
@@ -194,6 +194,9 @@ function createTrustApi(deps) {
194
194
  }) || '').replace(/\|/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80)
195
195
  // Prefer trust trigger / memory warn over a random risk line; always keep mem.warn available
196
196
  const reason = (trustReason || mem.warn) ? (trustReason || mem.warn) : topRisk
197
+ // What the triage line is actually quoting. A risk bullet printed under
198
+ // "trust:" read as a stakeholder problem that did not exist.
199
+ const reasonKind = mem.warn && trustReason === mem.warn ? 'memory' : trustReason ? 'trust' : mem.warn ? 'memory' : topRisk ? 'risk' : ''
197
200
  const openRisks = countOpenRisks(eng)
198
201
  const nextAction = nextActionLine(ctx)
199
202
  let updated = 'never', ageDays = Infinity
@@ -203,7 +206,7 @@ function createTrustApi(deps) {
203
206
  } catch (_) {}
204
207
  const dirty = memoryDirtyManual(eng)
205
208
  return {
206
- phase, trust, signalAge, stale, topRisk, reason, memoryWarn: mem.warn,
209
+ phase, trust, signalAge, stale, topRisk, reason, reasonKind, memoryWarn: mem.warn,
207
210
  dirtyFiles: dirty, openRisks, nextAction, updated, ageDays,
208
211
  }
209
212
  }
@@ -215,9 +218,9 @@ function createTrustApi(deps) {
215
218
  const lines = [
216
219
  `TRIAGE [${label.padEnd(6)}] phase:${phase} updated:${s.updated} open risks:${s.openRisks}`,
217
220
  ]
218
- if (s.reason) {
219
- const age = s.signalAge != null ? ` (${s.signalAge}d old${s.stale ? ', STALE - reconfirm' : ''})` : ''
220
- lines.push(` trust: ${s.reason}${age}`)
221
+ if (s.reason && s.reasonKind !== 'memory') {
222
+ const age = s.reasonKind === 'trust' && s.signalAge != null ? ` (${s.signalAge}d old${s.stale ? ', STALE - reconfirm' : ''})` : ''
223
+ lines.push(` ${s.reasonKind === 'risk' ? 'top risk' : 'trust'}: ${s.reason}${age}`)
221
224
  }
222
225
  // Always surface corruption / unreadable memory - even when trust still reads green
223
226
  if (s.memoryWarn) lines.push(` memory: ${s.memoryWarn}`)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.21.0",
3
+ "version": "3.22.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.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code: who can say yes, what went live, whether they signed off. Dated markdown on your laptop. You confirm each write.",
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.21.0",
4
+ "version": "3.22.0",
5
5
  "description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code. You confirm; then it lands in .fde/ on your laptop.",
6
6
  "author": {
7
7
  "name": "Subash Natarajan",
@@ -42,6 +42,8 @@ On someone else's site the work is not "write code, remember later." Every chang
42
42
 
43
43
  A throwaway file can skip the loop. Bound client work cannot.
44
44
 
45
+ **Skip is loud.** Bound + a change that will ship + no this-turn line in `delivery.md` = not done. Say that. Do not call it shipped. A coding pack may write the function; `@fde` still owns done.
46
+
45
47
  ## Human surface vs agent plumbing
46
48
 
47
49
  **FDE (human):** `@fde` + English, or `/brief` `/discover` `/plan` `/ship` `/outcome` `/close` `/debrief` `/prep` `/trust` `/receipts` `/readout`. Never a skill catalog.
@@ -196,8 +198,8 @@ Ready to build with no `terrain.md` / plan: discover or plan first. Takeover wit
196
198
 
197
199
  - Never ask the FDE to pick a phase. That's your job.
198
200
  - 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.
201
+ - Ground loop on a bound client: name → characterise → prove on their staging → go live → log. A coding pack may write the function. `@fde` still owns done. When they disagree, their repo and the signer win.
202
+ - Do not call a change done until the signer in `success.md` can reject it on staging they operate. No this-turn receipt in `delivery.md` is a failed test, not a note to write later.
201
203
  - Read `context.md` before speaking. One sharp question - never a barrage.
202
204
  - Never invent people, meetings, or numbers - `unknown - ask:` beats a polished lie.
203
205
  - Every phase ends with its artifact written. No artifact, no "done."
@@ -11,7 +11,8 @@
11
11
  ## Honest contract (read once)
12
12
 
13
13
  - The `fde` CLI is **local, deterministic, no AI**. `--smart` is a **gate + writer**, not a brain.
14
- - It keeps lines that already have `decision:` / `risk:` / `delivery:` / `contact:` / `next:` prefixes, plus a thin keyword pass (e.g. "we agreed", person+verb lines, "open question").
14
+ - It keeps lines that already have `decision:` / `risk:` / `delivery:` / `contact:` / `next:` / `signer:` prefixes, plus a thin keyword pass (e.g. "we agreed", person+verb lines, "open question", "X signs off").
15
+ - `signer: Priya` fills **Stakeholder who signs off** in `success.md` and logs Priya as a contact. The CLI proposes it when a sentence says someone signs off / approves / has final say. If the notes name who can say yes and the proposal does not carry a `signer:` line, add one - that is the most expensive sentence in the meeting.
15
16
  - Real messy notes without prefixes often route **0 useful lines** - everything else lands as a context dump. That is expected. **You are the router:** rewrite `.debrief-propose` with type prefixes, then `--apply`.
16
17
  - `.debrief-propose` is raw lines only (no routing annotations). "Edit if mis-routed" means **rewrite the line with the right prefix**, not leave a comment in the file.
17
18
 
@@ -25,6 +26,7 @@
25
26
  - `decision: agreed chargebacks stay phase 2 - Priya`
26
27
  - `risk: legal may reopen scope if we slip the SOW date`
27
28
  - `contact: Priya pushed hard on Friday deck [signal:amber]`
29
+ - `signer: Priya` (she can say yes; lands in `success.md`)
28
30
  - `next: send one-pager before Thursday 9am`
29
31
  - unprefixed lines stay context color only
30
32
  4. Show the **proposed** routing in plain language (what would become decisions, risks, contacts, next).
@@ -43,7 +45,7 @@ If `--smart` is unavailable or you already have clean prefixes:
43
45
  - **Stakeholder signals** - tone shifts with evidence → green/amber/red
44
46
  - **Risks** - new / confirmed / retired
45
47
  - **Open questions** - what to chase next
46
- 2. Format lines as `decision:` / `risk:` / `delivery:` / `contact:` / `next:` (contacts may end with `[signal:green|amber|red]`).
48
+ 2. Format lines as `decision:` / `risk:` / `delivery:` / `contact:` / `next:` / `signer:` (contacts may end with `[signal:green|amber|red]`).
47
49
  3. Show that structured version to the FDE for confirmation.
48
50
  4. Pipe to `fde debrief` (or write a file and run it).
49
51
 
@@ -31,7 +31,7 @@ List what you can actually call **this session**:
31
31
  3. **Stage** - `fde ingest stage [--source NAME] [--title TEXT] [file|-]` writes raw text into `<engagement>/.inbox/` (outside the memory git ledger).
32
32
  4. **List** (optional) - `fde ingest list` shows staged items when you need an id or filename.
33
33
  5. **Propose** - `fde ingest propose <id-or-filename>` runs the debrief `--smart` path on the staged body (+ provenance line). Opens `.debrief-propose`.
34
- 6. **Rewrite prefixes** - same as debrief: lines without `decision:` / `risk:` / `delivery:` / `contact:` / `next:` need **you** to rewrite before showing the FDE. `--smart` is a gate, not a brain.
34
+ 6. **Rewrite prefixes** - same as debrief: lines without `decision:` / `risk:` / `delivery:` / `contact:` / `next:` / `signer:` need **you** to rewrite before showing the FDE. `--smart` is a gate, not a brain.
35
35
  7. **Show** the proposed routing in plain language. Wait for confirm.
36
36
  8. **Apply** - on FDE confirm only → `fde ingest apply` (= `fde debrief --apply`). On reject → stop; ask what to change.
37
37
 
@@ -56,6 +56,10 @@ Each change is independently revertible.
56
56
  - [ ] Rollback named: revert this change, or something more specific
57
57
  - [ ] No dependency on an unmerged change (if dependent, state it and land in order)
58
58
  - [ ] `Kill if` is written - the observation that stops this change
59
+ - [ ] Before-receipt captured: the failing output, number, or screen as it is today, dated in `delivery.md`, before you change anything
60
+ - [ ] Open PRs and uncommitted work in the area checked (`gh pr list`, `gh pr diff <n> --name-only`); overlap goes to `decisions.md` before you start
61
+
62
+ Your coding pack writes the function. This skill owns done. When they disagree with this repo, the repo wins.
59
63
 
60
64
  **The loop.** In this order:
61
65
 
@@ -72,7 +76,7 @@ Read existing code in the area (search before creating)
72
76
 
73
77
  **Prove it on their staging.** A green check on your laptop is not delivery.
74
78
 
75
- - Run **their** test command, on **their** CI, with **their** fixtures. Write the command and the result in `delivery.md`. You do not add a runner they will not keep. If you have not run their command in this turn, you cannot write that it passed. Last session's green, "should pass," and "looks correct" are not a receipt.
79
+ - Run **their** test command, on **their** CI, with **their** fixtures. Write the command and the result in `delivery.md` in this turn. You do not add a runner they will not keep. If you have not run their command in this turn, you cannot write that it passed. Last session's green, "should pass," and "looks correct" are not a receipt. Missing this-turn line = not proven. Same as a failing test.
76
80
  - If the signer in `success.md` cannot reject this on a screen they already use, it is not proven.
77
81
  - Staging they operate beats a local demo. If you have no staging: `unknown - ask:` who owns an environment, then stop pretending it shipped.
78
82
  - **Monday-shaped data.** Staging that is empty, synthetic, or last quarter is not next Tuesday. Before go-live, write what staging is missing (volume, PII, the batch that only runs in prod, the account that only exists in the warehouse) and what that means for the kill test. If the signer cannot reject it on a screen they already operate, with data that looks like next Tuesday, it is not proven.