fdeops 3.22.2 → 3.23.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
@@ -34,7 +34,7 @@ One chat. Name the client:
34
34
  @fde this is client01
35
35
  ```
36
36
 
37
- That creates `~/fde-engagements/client01/.fde/` on your laptop. Paste kickoff notes in the same thread. `@fde` picks what to check. You still decide.
37
+ That creates `~/fde-engagements/client01/.fde/` on your laptop. Paste kickoff notes in the same thread. `@fde` picks what to check. You still decide. After a meeting you get one screen: what changed, new asks, open questions, next actions. Confirm once.
38
38
 
39
39
  Day to day: [docs/USAGE.md](docs/USAGE.md).
40
40
 
package/bin/check.js CHANGED
@@ -636,6 +636,11 @@ if (apManifest.$schema !== AP_PLUGIN_SCHEMA) {
636
636
  fail(`version mismatch package.json ${pkg.version} vs plugin.json ${apManifest.version}`)
637
637
  } else ok('agent plugins manifest')
638
638
 
639
+ const ingestPkg = JSON.parse(read('mcp/fdeops-ingest/package.json'))
640
+ if (ingestPkg.version !== pkg.version) {
641
+ fail(`version mismatch package.json ${pkg.version} vs mcp/fdeops-ingest ${ingestPkg.version}`)
642
+ } else ok('ingest package version aligned')
643
+
639
644
  const apMcp = JSON.parse(read('mcp.json'))
640
645
  const apServers = apMcp.mcpServers || {}
641
646
  if (apMcp.$schema !== AP_MCP_SCHEMA) {
package/bin/fde.js CHANGED
@@ -58,6 +58,26 @@ function sh(cmd, cwd) {
58
58
  } catch (_) { return '' }
59
59
  }
60
60
 
61
+ // Hex hashes only - never a shell. True when olderHash is an ancestor of newerHash.
62
+ function gitIsAncestor(eng, olderHash, newerHash) {
63
+ if (!olderHash || !newerHash || olderHash === newerHash) return false
64
+ if (!/^[0-9a-f]{7,64}$/i.test(olderHash) || !/^[0-9a-f]{7,64}$/i.test(newerHash)) return false
65
+ try {
66
+ execFileSync('git', ['merge-base', '--is-ancestor', olderHash, newerHash], {
67
+ cwd: eng, stdio: 'ignore', timeout: 15000,
68
+ })
69
+ return true
70
+ } catch (_) { return false }
71
+ }
72
+
73
+ function gitLogHash(eng, args) {
74
+ try {
75
+ return execFileSync('git', ['log', '-1', '--format=%H', ...args], {
76
+ cwd: eng, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000,
77
+ }).trim()
78
+ } catch (_) { return '' }
79
+ }
80
+
61
81
  function slugify(name) {
62
82
  return String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'engagement'
63
83
  }
@@ -1781,6 +1801,101 @@ function writeProposal(eng, text) {
1781
1801
  return { proposePath, clean, blocks }
1782
1802
  }
1783
1803
 
1804
+ function approvedStamp(text) {
1805
+ const m = String(text || '').match(/\[approved:\s*([^\]]+)\]/i)
1806
+ return m ? m[1].trim() : ''
1807
+ }
1808
+
1809
+ function stripApprovedStamp(text) {
1810
+ return String(text || '').replace(/\s*\[approved:\s*[^\]]+\]/i, '').trim()
1811
+ }
1812
+
1813
+ // One screen a human can confirm in two minutes. The file-by-file routing
1814
+ // still prints after this - agents edit prefixes; people read this.
1815
+ function printDebriefReview(text) {
1816
+ const buckets = { decided: [], asked: [], open: [], next: [], signer: [] }
1817
+ for (const raw of String(text || '').split('\n')) {
1818
+ const line = raw.trim().replace(/^[-*+]\s+/, '')
1819
+ if (!line) continue
1820
+ const m = line.match(/^(decision|risk|delivery|contact|next|signer):\s*(.+)$/i)
1821
+ if (!m) continue
1822
+ const type = m[1].toLowerCase()
1823
+ const body = m[2]
1824
+ if (type === 'decision') {
1825
+ const who = approvedStamp(body)
1826
+ const core = previewLine(stripApprovedStamp(body), 90)
1827
+ buckets.decided.push(who ? `${core} (approved ${who})` : `${core} (unconfirmed)`)
1828
+ } else if (type === 'delivery') {
1829
+ buckets.asked.push(previewLine(body, 100))
1830
+ } else if (type === 'risk') {
1831
+ buckets.open.push(previewLine(body, 100))
1832
+ } else if (type === 'next') {
1833
+ buckets.next.push(previewLine(body, 100))
1834
+ } else if (type === 'signer') {
1835
+ buckets.signer.push(previewLine(body, 80))
1836
+ }
1837
+ }
1838
+ console.log('REVIEW (one screen - confirm once, then apply)\n')
1839
+ const order = [
1840
+ ['decided', buckets.decided],
1841
+ ['asked', buckets.asked],
1842
+ ['open', buckets.open],
1843
+ ['next', buckets.next],
1844
+ ['signer', buckets.signer],
1845
+ ]
1846
+ let any = false
1847
+ for (const [label, items] of order) {
1848
+ if (!items.length) continue
1849
+ any = true
1850
+ console.log(` ${label}:`)
1851
+ for (const item of items) console.log(` - ${item}`)
1852
+ }
1853
+ if (!any) console.log(' (nothing prefixed yet - edit .debrief-propose, then apply)')
1854
+ console.log('')
1855
+ }
1856
+
1857
+ function latestDatedDecision(md) {
1858
+ let latest = { date: '', line: '' }
1859
+ for (const raw of String(md || '').split('\n')) {
1860
+ const t = raw.trim()
1861
+ const m = t.match(/^[-*]\s*\[(\d{4}-\d{2}-\d{2})\]/)
1862
+ if (!m) continue
1863
+ if (m[1] >= latest.date) latest = { date: m[1], line: t }
1864
+ }
1865
+ return latest
1866
+ }
1867
+
1868
+ function formatDecisionRecord(line) {
1869
+ const raw = String(line || '').trim().replace(/^[-*]\s*/, '')
1870
+ const who = approvedStamp(raw)
1871
+ const core = stripApprovedStamp(raw)
1872
+ const stamp = who ? `(approved ${who})` : '(unconfirmed)'
1873
+ return previewLine(`${core} ${stamp}`, 110)
1874
+ }
1875
+
1876
+ function changeReviewIssues(eng) {
1877
+ const issues = []
1878
+ const del = latestDeliveryEntry(readClean(eng, 'delivery.md'))
1879
+ const dec = latestDatedDecision(readClean(eng, 'decisions.md'))
1880
+ const delHash = del.line
1881
+ ? sh(`git log -1 --format=%H -S${JSON.stringify(del.line)} -- delivery.md`, eng)
1882
+ : ''
1883
+ if (del.date && dec.date && dec.date > del.date && delHash) {
1884
+ issues.push(
1885
+ 'a decision landed after the last delivery line - review whether what you are shipping still matches'
1886
+ )
1887
+ }
1888
+ if (claimedValueRows(eng).claimed && delHash) {
1889
+ const sigHash = gitLogHash(eng, ['-GStakeholder who signs off|also named:', '--', 'success.md'])
1890
+ if (sigHash && gitIsAncestor(eng, delHash, sigHash)) {
1891
+ issues.push(
1892
+ 'the signer line changed while a measured number is still unaccepted - pending acceptance may need a new yes'
1893
+ )
1894
+ }
1895
+ }
1896
+ return issues
1897
+ }
1898
+
1784
1899
  function readSealCount(eng) {
1785
1900
  try {
1786
1901
  const n = parseInt(fs.readFileSync(path.join(eng, DEBRIEF_SEAL), 'utf8').trim(), 10)
@@ -1904,7 +2019,9 @@ function cmdDebrief(args) {
1904
2019
  if (smart) {
1905
2020
  const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
1906
2021
  console.log('SMART PROPOSE (heuristic - review before apply; no new facts invented beyond line rewrites)\n')
2022
+ printDebriefReview(clean)
1907
2023
  console.log('Prefix vocabulary (lines that route): decision: risk: delivery: contact: next: signer:')
2024
+ console.log('Optional on a decision: [approved: Name YYYY-MM-DD]. Missing means unconfirmed.')
1908
2025
  console.log('Everything else → context.md. Keep the prefixes; the preview gate stays.\n')
1909
2026
  routeDebriefInput(eng, clean, { dry: true, force, sealed: blocks })
1910
2027
  if (!apply) {
@@ -2046,6 +2163,7 @@ function cmdIngest(args) {
2046
2163
  }
2047
2164
  const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
2048
2165
  console.log(`INGEST PROPOSE from ${path.basename(item)} (via:${source})\n`)
2166
+ printDebriefReview(clean)
2049
2167
  routeDebriefInput(eng, clean, { dry: true, force: false, sealed: blocks })
2050
2168
  console.log(`\nproposal saved → ${proposePath}`)
2051
2169
  console.log('confirm: fde ingest apply')
@@ -2516,6 +2634,7 @@ function collectDoctorIssues(eng) {
2516
2634
  }
2517
2635
  const reality = parseReality(readClean(eng, 'reality.md'), 220)
2518
2636
  if (reality.missing) issues.push(reality.missing)
2637
+ issues.push(...changeReviewIssues(eng))
2519
2638
  return issues
2520
2639
  }
2521
2640
 
@@ -2768,7 +2887,7 @@ function hygieneTriageLines(eng) {
2768
2887
  const top = issues[0].replace(/\s+/g, ' ').trim().slice(0, 72)
2769
2888
  return [
2770
2889
  ` hygiene: ${issues.length} issue(s) - ${top}${issues[0].length > 72 ? '…' : ''}`,
2771
- ' → say "@fde clean up the fieldbook" when ready (agent runs fde doctor; nothing auto-rewrites)',
2890
+ ' → say "@fde clean up the fieldbook" when ready (agent runs fde doctor; nothing auto-rewrites), or: fde doctor',
2772
2891
  ]
2773
2892
  }
2774
2893
 
@@ -2798,7 +2917,7 @@ function recordDigest(eng) {
2798
2917
  }
2799
2918
  const decisions = readClean(eng, 'decisions.md').split('\n')
2800
2919
  .filter(l => /^-\s*\[\d{4}-\d{2}-\d{2}\]/.test(l.trim())).slice(-2)
2801
- for (const d of decisions) lines.push(` decided: ${d.trim().replace(/^-\s*/, '').slice(0, 110)}`)
2920
+ for (const d of decisions) lines.push(` decided: ${formatDecisionRecord(d)}`)
2802
2921
  return ['RECORD (read-only - success, delivery, decisions)', ...lines]
2803
2922
  }
2804
2923
 
@@ -3614,7 +3733,7 @@ function printUsage() {
3614
3733
  fde log phase <phase> set engagement phase (land|discover|plan|ship|outcome|close)
3615
3734
  fde log --undo remove the last CLI log/debrief entry from memory
3616
3735
  fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
3617
- fde debrief --smart heuristic propose (prints decision:/risk:/delivery:/contact:/next:); --apply after confirm
3736
+ fde debrief --smart heuristic propose; REVIEW first (decided/asked/open/next/signer); --apply after one confirm
3618
3737
  fde ingest stage … stage raw pull into <engagement>/.inbox/ (not .fde/)
3619
3738
  fde ingest list list staged inbox items
3620
3739
  fde ingest propose <id> smart-propose a staged item → .debrief-propose (confirm before apply)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.22.2",
3
+ "version": "3.23.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.22.2",
3
+ "version": "3.23.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.22.2",
4
+ "version": "3.23.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",
@@ -28,7 +28,7 @@ A one-line typo or compile error in a file that will not ship. On a bound client
28
28
  | **When did we agree?** | Don't argue from memory. Search the record. | `fde receipts <term>` | - |
29
29
  | **What's the outcome?** | A number nobody signed is claimed, not delivered. | `fde status` | `references/readout.md` |
30
30
 
31
- After a meeting: `fde debrief --smart` → confirm → `--apply`. Walk-in: `fde prep`. Friday: `fde status`.
31
+ After a meeting: `fde debrief --smart` → one REVIEW screen (decided / asked / open / next / signer) → confirm once → `--apply`. Walk-in: `fde prep`. Friday: `fde status`.
32
32
 
33
33
  ## Ground loop
34
34
 
@@ -65,7 +65,7 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
65
65
  |----------|---------|
66
66
  | where are we | `fde resume` |
67
67
  | day-1 look at the repo | `fde scan` |
68
- | debrief / pasted notes | `fde debrief --smart` → you rewrite prefixes → confirm → `--apply`. `--smart` is a gate, not a brain. `references/debrief.md` |
68
+ | debrief / pasted notes | `fde debrief --smart` → REVIEW → confirm once → `--apply`. `--smart` is a gate, not a brain. `references/debrief.md` |
69
69
  | prep me for … | `fde prep "<label>"` |
70
70
  | when did we agree | `fde receipts <term>` |
71
71
  | sponsor update / the outcome | `fde status` |
@@ -24,12 +24,13 @@
24
24
  2. Run `fde debrief --smart <notes.md>` (or `npx fdeops debrief --smart …`).
25
25
  3. Open `.debrief-propose`. If lines lack type prefixes, **rewrite them** before showing the FDE, e.g.:
26
26
  - `decision: agreed chargebacks stay phase 2 - Priya`
27
+ - `decision: freeze the API [approved: Priya 2026-09-08]` (optional; missing means unconfirmed)
27
28
  - `risk: legal may reopen scope if we slip the SOW date`
28
29
  - `contact: Priya pushed hard on Friday deck [signal:amber]`
29
30
  - `signer: Priya` (she can say yes; lands in `success.md`)
30
31
  - `next: send one-pager before Thursday 9am`
31
32
  - unprefixed lines stay context color only
32
- 4. Show the **proposed** routing in plain language (what would become decisions, risks, contacts, next).
33
+ 4. Show the **REVIEW** block first (decided / asked / open / next / signer). That is the one screen to confirm. File routing stays underneath.
33
34
  5. On FDE confirm → run `fde debrief --apply`.
34
35
  6. On reject → stop; ask what to change; do not apply.
35
36