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 +1 -1
- package/bin/check.js +5 -0
- package/bin/fde.js +122 -3
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +2 -2
- package/skills/fde/references/debrief.md +2 -1
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
|
|
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 (
|
|
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)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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",
|
package/skills/fde/SKILL.md
CHANGED
|
@@ -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` →
|
|
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 **
|
|
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
|
|