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 +2 -0
- package/bin/check.js +22 -0
- package/bin/fde.js +141 -10
- package/bin/lib/trust.js +7 -4
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +4 -2
- package/skills/fde/references/debrief.md +4 -2
- package/skills/fde/references/ingest.md +1 -1
- package/skills/fde/references/ship.md +5 -1
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
|
-
|
|
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
|
|
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}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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",
|
package/skills/fde/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|