fdeops 3.22.1 → 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 +159 -18
- 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
|
}
|
|
@@ -1610,20 +1630,38 @@ function smartProposeText(input) {
|
|
|
1610
1630
|
return out.join('\n') + (out.length ? '\n' : '')
|
|
1611
1631
|
}
|
|
1612
1632
|
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
const
|
|
1633
|
+
// Kickoff English, not only "signer: Priya". "Helena signs off", "Anand Mehta
|
|
1634
|
+
// has final say", "Finance controller (Helena) signs off" all have to fill
|
|
1635
|
+
// success.md - that is the line Monday's RECORD reads.
|
|
1636
|
+
const SIGNER_VERB = '(?:signs?(?:\\s+off)?|approves|has (?:the )?final say|can say yes|owns the decision|is the (?:sponsor|signer|decision[- ]maker))'
|
|
1637
|
+
const SIGNER_NAME = '([A-Z][\\w.\'-]+(?:\\s+[A-Z][\\w.\'-]+){0,3})'
|
|
1638
|
+
const NOT_A_PERSON = /^(The|This|That|It|We|They|She|He|Staging|Budget|Prod|Production|Nobody|Someone|Everyone|Finance|Legal|Security|Platform|Engineering)\b/
|
|
1639
|
+
const ROLE_TOKEN = /\b(VP|SVP|EVP|CTO|CFO|COO|CEO|CISO|Eng|Engineer|Director|Lead|Head|Manager|Controller|Ops|Legal|Finance|Sponsor)\b/i
|
|
1640
|
+
|
|
1641
|
+
function looksLikePersonName(s) {
|
|
1642
|
+
const t = String(s || '').trim()
|
|
1643
|
+
if (!t || NOT_A_PERSON.test(t) || ROLE_TOKEN.test(t)) return false
|
|
1644
|
+
return /^[A-Z][\w.'-]+(?:\s+[A-Z][\w.'-]+){0,2}$/.test(t)
|
|
1645
|
+
}
|
|
1617
1646
|
|
|
1618
1647
|
function signerFromLine(text) {
|
|
1619
|
-
const t = String(text || '').trim()
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1648
|
+
const t = String(text || '').replace(/^[-*+]\s+/, '').trim()
|
|
1649
|
+
if (!t) return ''
|
|
1650
|
+
// "Priya (VP Eng) signs off" → Priya. "Finance controller (Helena) signs off" → Helena.
|
|
1651
|
+
const titled = t.match(new RegExp('\\b' + SIGNER_NAME + '\\s+\\(' + SIGNER_NAME + '\\)\\s+' + SIGNER_VERB + '\\b'))
|
|
1652
|
+
if (titled) {
|
|
1653
|
+
const before = titled[1].trim()
|
|
1654
|
+
const inside = titled[2].trim()
|
|
1655
|
+
if (looksLikePersonName(before) && ROLE_TOKEN.test(inside)) return before
|
|
1656
|
+
if (looksLikePersonName(inside)) return inside
|
|
1657
|
+
if (looksLikePersonName(before)) return before
|
|
1658
|
+
}
|
|
1659
|
+
const paren = t.match(new RegExp('\\(' + SIGNER_NAME + '\\)\\s+' + SIGNER_VERB + '\\b'))
|
|
1660
|
+
if (paren && looksLikePersonName(paren[1])) return paren[1].trim()
|
|
1661
|
+
const named = t.match(new RegExp('\\b' + SIGNER_NAME + '\\s+' + SIGNER_VERB + '\\b'))
|
|
1662
|
+
if (!named) return ''
|
|
1663
|
+
const who = named[1].trim()
|
|
1664
|
+
return looksLikePersonName(who) ? who : ''
|
|
1627
1665
|
}
|
|
1628
1666
|
|
|
1629
1667
|
function setSigner(eng, who) {
|
|
@@ -1763,6 +1801,101 @@ function writeProposal(eng, text) {
|
|
|
1763
1801
|
return { proposePath, clean, blocks }
|
|
1764
1802
|
}
|
|
1765
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
|
+
|
|
1766
1899
|
function readSealCount(eng) {
|
|
1767
1900
|
try {
|
|
1768
1901
|
const n = parseInt(fs.readFileSync(path.join(eng, DEBRIEF_SEAL), 'utf8').trim(), 10)
|
|
@@ -1886,7 +2019,9 @@ function cmdDebrief(args) {
|
|
|
1886
2019
|
if (smart) {
|
|
1887
2020
|
const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
|
|
1888
2021
|
console.log('SMART PROPOSE (heuristic - review before apply; no new facts invented beyond line rewrites)\n')
|
|
2022
|
+
printDebriefReview(clean)
|
|
1889
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.')
|
|
1890
2025
|
console.log('Everything else → context.md. Keep the prefixes; the preview gate stays.\n')
|
|
1891
2026
|
routeDebriefInput(eng, clean, { dry: true, force, sealed: blocks })
|
|
1892
2027
|
if (!apply) {
|
|
@@ -2028,6 +2163,7 @@ function cmdIngest(args) {
|
|
|
2028
2163
|
}
|
|
2029
2164
|
const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
|
|
2030
2165
|
console.log(`INGEST PROPOSE from ${path.basename(item)} (via:${source})\n`)
|
|
2166
|
+
printDebriefReview(clean)
|
|
2031
2167
|
routeDebriefInput(eng, clean, { dry: true, force: false, sealed: blocks })
|
|
2032
2168
|
console.log(`\nproposal saved → ${proposePath}`)
|
|
2033
2169
|
console.log('confirm: fde ingest apply')
|
|
@@ -2134,7 +2270,7 @@ function cmdReceipts(args) {
|
|
|
2134
2270
|
agreed.map(h => (h.match(/^\s*([^:]+):/) || [])[1]).filter(f => f && dirtySet.has(f))
|
|
2135
2271
|
)]
|
|
2136
2272
|
if (agreed.length) {
|
|
2137
|
-
console.log('ON RECORD (dated
|
|
2273
|
+
console.log('ON RECORD (dated):')
|
|
2138
2274
|
agreed.forEach(h => {
|
|
2139
2275
|
const file = (h.match(/^\s*([^:]+):/) || [])[1]
|
|
2140
2276
|
console.log(h + (file && dirtySet.has(file) ? ' ⚠ dirty file' : ''))
|
|
@@ -2498,6 +2634,7 @@ function collectDoctorIssues(eng) {
|
|
|
2498
2634
|
}
|
|
2499
2635
|
const reality = parseReality(readClean(eng, 'reality.md'), 220)
|
|
2500
2636
|
if (reality.missing) issues.push(reality.missing)
|
|
2637
|
+
issues.push(...changeReviewIssues(eng))
|
|
2501
2638
|
return issues
|
|
2502
2639
|
}
|
|
2503
2640
|
|
|
@@ -2620,7 +2757,7 @@ function hasValueBucket(eng) {
|
|
|
2620
2757
|
// "pending review", "TBD.", "n/a (blocked)" and "..." are all the same thing an
|
|
2621
2758
|
// FDE means by an empty cell - nagging about them teaches people to ignore doctor.
|
|
2622
2759
|
const PENDING_CELL_RE =
|
|
2623
|
-
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not
|
|
2760
|
+
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not(?:\s+yet)?\s+measured|unmeasured|awaiting|\?+|\.{2,}|…|-+)(?:[^\w].*)?$/i
|
|
2624
2761
|
|
|
2625
2762
|
function parseValueLedger(eng) {
|
|
2626
2763
|
// Last section with actual rows, not merely the last non-empty one: a template
|
|
@@ -2704,7 +2841,7 @@ function engagementTouchesAI(eng) {
|
|
|
2704
2841
|
].join('\n'))
|
|
2705
2842
|
// No bare "prompt": "prompt response" / "prompt payment" is ordinary delivery
|
|
2706
2843
|
// English and would fail every non-AI ship on a missing eval receipt.
|
|
2707
|
-
return /\b(llm|rag|embedding|model card|model output|model drift|agentic|openai|anthropic|vector database|vector db|fine-tun\w*|hallucinat\w*)\b|\bmodel inference\b|\binference (?:api|endpoint|server|engine)\b|\b(?:system|model|user)\s+prompts?\b|\bprompt (?:engineering|injection|template)/i.test(blob)
|
|
2844
|
+
return /\bAI in scope\b|\b(llm|rag|embedding|model card|model output|model drift|agentic|openai|anthropic|vector database|vector db|fine-tun\w*|hallucinat\w*)\b|\bmodel inference\b|\binference (?:api|endpoint|server|engine)\b|\b(?:system|model|user)\s+prompts?\b|\bprompt (?:engineering|injection|template)/i.test(blob)
|
|
2708
2845
|
}
|
|
2709
2846
|
|
|
2710
2847
|
// The repo says AI even when the record does not. Read-only, capped, local: the
|
|
@@ -2750,7 +2887,7 @@ function hygieneTriageLines(eng) {
|
|
|
2750
2887
|
const top = issues[0].replace(/\s+/g, ' ').trim().slice(0, 72)
|
|
2751
2888
|
return [
|
|
2752
2889
|
` hygiene: ${issues.length} issue(s) - ${top}${issues[0].length > 72 ? '…' : ''}`,
|
|
2753
|
-
' → 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',
|
|
2754
2891
|
]
|
|
2755
2892
|
}
|
|
2756
2893
|
|
|
@@ -2780,7 +2917,7 @@ function recordDigest(eng) {
|
|
|
2780
2917
|
}
|
|
2781
2918
|
const decisions = readClean(eng, 'decisions.md').split('\n')
|
|
2782
2919
|
.filter(l => /^-\s*\[\d{4}-\d{2}-\d{2}\]/.test(l.trim())).slice(-2)
|
|
2783
|
-
for (const d of decisions) lines.push(` decided: ${d
|
|
2920
|
+
for (const d of decisions) lines.push(` decided: ${formatDecisionRecord(d)}`)
|
|
2784
2921
|
return ['RECORD (read-only - success, delivery, decisions)', ...lines]
|
|
2785
2922
|
}
|
|
2786
2923
|
|
|
@@ -3596,7 +3733,7 @@ function printUsage() {
|
|
|
3596
3733
|
fde log phase <phase> set engagement phase (land|discover|plan|ship|outcome|close)
|
|
3597
3734
|
fde log --undo remove the last CLI log/debrief entry from memory
|
|
3598
3735
|
fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
|
|
3599
|
-
fde debrief --smart heuristic propose (
|
|
3736
|
+
fde debrief --smart heuristic propose; REVIEW first (decided/asked/open/next/signer); --apply after one confirm
|
|
3600
3737
|
fde ingest stage … stage raw pull into <engagement>/.inbox/ (not .fde/)
|
|
3601
3738
|
fde ingest list list staged inbox items
|
|
3602
3739
|
fde ingest propose <id> smart-propose a staged item → .debrief-propose (confirm before apply)
|
|
@@ -3618,6 +3755,10 @@ function printUsage() {
|
|
|
3618
3755
|
}
|
|
3619
3756
|
|
|
3620
3757
|
const [cmd, ...args] = process.argv.slice(2)
|
|
3758
|
+
if (args.includes('--help') || args.includes('-h') || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
3759
|
+
printUsage()
|
|
3760
|
+
process.exit(0)
|
|
3761
|
+
}
|
|
3621
3762
|
switch (cmd) {
|
|
3622
3763
|
case 'demo': cmdDemo(args); break
|
|
3623
3764
|
case 'scan': cmdScan(); break
|
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
|
|