fdeops 3.15.1 → 3.16.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/AGENTS.md +1 -1
- package/README.md +132 -93
- package/adapters/AGENTS.md +2 -2
- package/adapters/GEMINI.md +2 -2
- package/adapters/LOCAL-LLM.md +3 -3
- package/adapters/README.md +1 -1
- package/adapters/copilot-instructions.md +2 -2
- package/adapters/cursor.fde.mdc +3 -3
- package/bin/check.js +66 -14
- package/bin/fde.js +16 -16
- package/bin/lib/memory.js +1 -1
- package/bin/lib/render.js +1 -1
- package/bin/lib/trust.js +1 -1
- package/mcp/README.md +3 -3
- package/mcp/fdeops-ingest/README.md +8 -8
- package/mcp/fdeops-ingest/package.json +1 -1
- package/mcp/fdeops-ingest/server.js +1 -1
- package/mcp/recipes/README.md +3 -3
- package/mcp/recipes/file.md +1 -1
- package/mcp/recipes/granola.md +5 -5
- package/mcp/recipes/notion.md +2 -2
- package/mcp/recipes/slack.md +5 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +52 -39
- package/skills/fde/references/ai.md +9 -9
- package/skills/fde/references/{exec-narrative.md → board-memo.md} +1 -1
- package/skills/fde/references/business-case.md +5 -5
- package/skills/fde/references/close.md +5 -5
- package/skills/fde/references/{ingest-connect.md → connect.md} +7 -7
- package/skills/fde/references/debrief.md +2 -2
- package/skills/fde/references/discover.md +8 -8
- package/skills/fde/references/{trust-engineering.md → earn-trust.md} +2 -2
- package/skills/fde/references/{pattern-extract.md → encode-pattern.md} +1 -1
- package/skills/fde/references/eval-pack.md +6 -6
- package/skills/fde/references/{scope-defense.md → hold-scope.md} +4 -4
- package/skills/fde/references/ingest.md +16 -16
- package/skills/fde/references/land.md +10 -10
- package/skills/fde/references/{initiative-triage.md → pick-three.md} +7 -7
- package/skills/fde/references/plan.md +9 -9
- package/skills/fde/references/{sketch.md → poc.md} +4 -4
- package/skills/fde/references/{status.md → readout.md} +10 -10
- package/skills/fde/references/red-team.md +2 -2
- package/skills/fde/references/rescue.md +1 -1
- package/skills/fde/references/review.md +7 -7
- package/skills/fde/references/{rollback-drill.md → rollback.md} +1 -1
- package/skills/fde/references/{handoff-engineering.md → runbook.md} +5 -5
- package/skills/fde/references/{use-case-scoring.md → score-use-cases.md} +4 -4
- package/skills/fde/references/ship.md +16 -16
- package/skills/fde/references/{multi-customer-ops.md → switch-clients.md} +2 -2
- package/skills/fde/references/{assumption-audit.md → test-assumptions.md} +3 -3
- package/skills/fde/references/{incremental-build.md → thin-slices.md} +22 -13
- package/skills/fde/references/{options-analysis.md → three-options.md} +3 -3
- package/skills/fde/references/{blast-radius.md → what-breaks.md} +4 -4
- package/skills/fde/references/{stakeholder-radar.md → who-decides.md} +5 -5
- package/templates/.fde/assumptions.md +1 -1
package/bin/check.js
CHANGED
|
@@ -43,7 +43,7 @@ for (const f of requiredTemplates) {
|
|
|
43
43
|
|
|
44
44
|
const deadMedia = ['demo.gif', 'demo.sh', 'demo.tape', 'terminal-demo.svg', 'fieldbook-dashboard.png', 'fieldbook-detail.png', 'fieldbook-walkthrough.gif']
|
|
45
45
|
for (const name of deadMedia) {
|
|
46
|
-
if (fs.existsSync(path.join(root, 'media', name))) fail(`dead media/${name} must not ship
|
|
46
|
+
if (fs.existsSync(path.join(root, 'media', name))) fail(`dead media/${name} must not ship - the recorded session is session.gif`)
|
|
47
47
|
}
|
|
48
48
|
ok('no staged mock media')
|
|
49
49
|
|
|
@@ -57,14 +57,14 @@ for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
|
|
|
57
57
|
ok('skills structure')
|
|
58
58
|
|
|
59
59
|
if (fs.existsSync(path.join(root, 'skills', 'fde', 'archive'))) {
|
|
60
|
-
fail('skills/fde/archive must not exist
|
|
60
|
+
fail('skills/fde/archive must not exist - unrouted methods are dead code')
|
|
61
61
|
} else ok('no archived skill dump')
|
|
62
62
|
|
|
63
63
|
// v3: one skill + phase references (progressive disclosure)
|
|
64
64
|
const requiredReferences = [
|
|
65
65
|
'land.md', 'discover.md', 'audit.md', 'plan.md', 'review.md',
|
|
66
|
-
'rescue.md', 'ship.md', '
|
|
67
|
-
'debrief.md', '
|
|
66
|
+
'rescue.md', 'ship.md', 'poc.md', 'close.md', 'dashboard.md',
|
|
67
|
+
'debrief.md', 'readout.md', 'demo-prep.md',
|
|
68
68
|
'healthcare.md', 'fintech.md', 'gov.md',
|
|
69
69
|
'ai.md', 'eval-pack.md',
|
|
70
70
|
]
|
|
@@ -84,8 +84,8 @@ ok('phase references')
|
|
|
84
84
|
// file the work lands in. Prose-only guidance drifts into advice nobody can apply.
|
|
85
85
|
const exampleReferences = [
|
|
86
86
|
'land.md', 'discover.md', 'plan.md', 'ship.md', 'close.md',
|
|
87
|
-
'
|
|
88
|
-
'
|
|
87
|
+
'readout.md', 'who-decides.md', 'three-options.md', 'business-case.md',
|
|
88
|
+
'test-assumptions.md', 'hold-scope.md',
|
|
89
89
|
]
|
|
90
90
|
for (const f of exampleReferences) {
|
|
91
91
|
const p = path.join(root, 'skills', 'fde', 'references', f)
|
|
@@ -115,7 +115,7 @@ for (const refFile of mentioned) {
|
|
|
115
115
|
ok(`router dispatch (${mentioned.length} reference targets verified) + memory contract`)
|
|
116
116
|
|
|
117
117
|
// Public claims must match the router. The docs advertise a method count and a
|
|
118
|
-
// per-domain list; both drifted from SKILL.md once (ingest /
|
|
118
|
+
// per-domain list; both drifted from SKILL.md once (ingest / connect
|
|
119
119
|
// routed but undocumented), and a number nobody can verify is worse than none.
|
|
120
120
|
{
|
|
121
121
|
// Every routing row must parse. A row this misses is a method that could go
|
|
@@ -186,7 +186,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
|
|
|
186
186
|
}
|
|
187
187
|
for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) {
|
|
188
188
|
const body = read(rel)
|
|
189
|
-
// `-` is a word boundary, so \
|
|
189
|
+
// `-` is a word boundary, so \bscore\b matches inside `score-use-cases`:
|
|
190
190
|
// a method could disappear from the docs behind a hyphenated sibling.
|
|
191
191
|
const absent = [...documented].filter(name => !new RegExp(`(?<![\\w-])${name}(?![\\w-])`).test(body))
|
|
192
192
|
if (absent.length) fail(`${rel} does not list skill(s): ${absent.join(', ')}`)
|
|
@@ -201,7 +201,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
|
|
|
201
201
|
|
|
202
202
|
const refDir = path.join(root, 'skills', 'fde', 'references')
|
|
203
203
|
const extra = fs.readdirSync(refDir).filter(f => f.endsWith('.md') && !mentioned.includes(f))
|
|
204
|
-
if (extra.length) fail(`unrouted reference file(s)
|
|
204
|
+
if (extra.length) fail(`unrouted reference file(s) - dead method: ${extra.join(', ')}`)
|
|
205
205
|
else ok('no unrouted reference files')
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -222,7 +222,7 @@ if (read('package.json').includes('postinstall')) {
|
|
|
222
222
|
|
|
223
223
|
const readme = read('README.md')
|
|
224
224
|
if (/session\.gif|demo\.gif|<img /i.test(readme)) {
|
|
225
|
-
fail('README must not embed images
|
|
225
|
+
fail('README must not embed images - front door is text; the recording lives in docs/USAGE.md')
|
|
226
226
|
} else ok('README is text (no gif)')
|
|
227
227
|
|
|
228
228
|
const usage = read('docs/USAGE.md')
|
|
@@ -262,9 +262,10 @@ if (!readme.includes('AI coding agent')) {
|
|
|
262
262
|
}
|
|
263
263
|
ok('README clarity sections')
|
|
264
264
|
|
|
265
|
-
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/
|
|
265
|
+
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/outcome', '/close', '/debrief', '/prep', '/trust', '/receipts', '/readout']) {
|
|
266
266
|
if (!readme.includes(cmd)) fail(`README must document slash command ${cmd}`)
|
|
267
267
|
}
|
|
268
|
+
if (/(^|[^\w/])\/got\b/.test(readme)) fail('README must use /outcome, not /got')
|
|
268
269
|
ok('README slash commands documented')
|
|
269
270
|
|
|
270
271
|
// Front-door map is the embed left-to-right (LAND → CLOSE), not a pile of situations.
|
|
@@ -535,7 +536,7 @@ if (pkg.version !== plugin.version) {
|
|
|
535
536
|
if (plugin.commands !== './.claude/commands' || plugin.skills !== './skills') {
|
|
536
537
|
fail('.claude-plugin/plugin.json must declare skills and commands')
|
|
537
538
|
} else {
|
|
538
|
-
for (const cmd of ['brief', 'discover', 'plan', 'ship', '
|
|
539
|
+
for (const cmd of ['brief', 'discover', 'plan', 'ship', 'outcome', 'close', 'debrief', 'prep', 'trust', 'receipts', 'readout']) {
|
|
539
540
|
const rel = `.claude/commands/${cmd}.md`
|
|
540
541
|
if (!fs.existsSync(path.join(root, rel))) fail(`${rel} missing`)
|
|
541
542
|
else if (!read(rel).includes('@fde')) fail(`${rel} must load @fde`)
|
|
@@ -549,8 +550,8 @@ if (!fs.existsSync(path.join(root, 'mcp', 'fdeops-ingest', 'server.js'))) {
|
|
|
549
550
|
fail('ingest MCP must expose ingest_stage')
|
|
550
551
|
} else if (!read('skills/fde/references/ingest.md').includes('stage')) {
|
|
551
552
|
fail('skills/fde/references/ingest.md missing stage contract')
|
|
552
|
-
} else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', '
|
|
553
|
-
fail('skills/fde/references/
|
|
553
|
+
} else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', 'connect.md'))) {
|
|
554
|
+
fail('skills/fde/references/connect.md missing')
|
|
554
555
|
} else {
|
|
555
556
|
for (const recipe of ['file.md', 'granola.md', 'slack.md', 'notion.md']) {
|
|
556
557
|
if (!fs.existsSync(path.join(root, 'mcp', 'recipes', recipe))) fail(`mcp/recipes/${recipe} missing`)
|
|
@@ -647,4 +648,55 @@ if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'
|
|
|
647
648
|
fail('GitHub issue template missing')
|
|
648
649
|
} else ok('issue templates')
|
|
649
650
|
|
|
651
|
+
function findUnicodeDashes(dir, acc = []) {
|
|
652
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
653
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '.agents') continue
|
|
654
|
+
const p = path.join(dir, e.name)
|
|
655
|
+
if (e.isDirectory()) findUnicodeDashes(p, acc)
|
|
656
|
+
else if (e.name === 'session.cast' || e.name === 'session.gif') continue
|
|
657
|
+
else {
|
|
658
|
+
let t
|
|
659
|
+
try { t = fs.readFileSync(p, 'utf8') } catch { continue }
|
|
660
|
+
if (t.includes('\u2014') || t.includes('\u2013')) acc.push(path.relative(root, p))
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return acc
|
|
664
|
+
}
|
|
665
|
+
{
|
|
666
|
+
const dashed = findUnicodeDashes(root)
|
|
667
|
+
if (dashed.length) fail(`em/en dashes must be ASCII hyphen: ${dashed.join(', ')}`)
|
|
668
|
+
else ok('no em/en dashes')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!fs.existsSync(path.join(root, 'CODE_OF_CONDUCT.md'))) {
|
|
672
|
+
fail('CODE_OF_CONDUCT.md missing - GitHub community profile needs it')
|
|
673
|
+
} else ok('CODE_OF_CONDUCT.md')
|
|
674
|
+
|
|
675
|
+
if (!fs.existsSync(path.join(root, '.github', 'PULL_REQUEST_TEMPLATE.md'))) {
|
|
676
|
+
fail('.github/PULL_REQUEST_TEMPLATE.md missing - GitHub community profile needs it')
|
|
677
|
+
} else ok('pull request template')
|
|
678
|
+
|
|
679
|
+
if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'question.md'))) {
|
|
680
|
+
fail('.github/ISSUE_TEMPLATE/question.md missing - community profile needs a markdown template with name/about')
|
|
681
|
+
} else {
|
|
682
|
+
const q = read('.github/ISSUE_TEMPLATE/question.md')
|
|
683
|
+
if (!/^---[\s\S]*\nname:/m.test(q) || !/^---[\s\S]*\nabout:/m.test(q)) {
|
|
684
|
+
fail('question.md must have YAML name: and about: so GitHub ticks issue templates')
|
|
685
|
+
} else ok('markdown issue template (name + about)')
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
{
|
|
689
|
+
const mktPath = path.join(root, '.claude-plugin', 'marketplace.json')
|
|
690
|
+
if (!fs.existsSync(mktPath)) fail('.claude-plugin/marketplace.json missing - /plugin marketplace add needs it')
|
|
691
|
+
else {
|
|
692
|
+
const mkt = JSON.parse(read('.claude-plugin/marketplace.json'))
|
|
693
|
+
const plug = (mkt.plugins || [])[0]
|
|
694
|
+
if (mkt.name !== 'fdeops' || !plug || plug.source !== './') {
|
|
695
|
+
fail('marketplace.json must name fdeops and list source ./')
|
|
696
|
+
} else if (!mkt.description && !(mkt.metadata && mkt.metadata.description)) {
|
|
697
|
+
fail('marketplace.json needs a description for the plugin directory')
|
|
698
|
+
} else ok('claude marketplace.json')
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
650
702
|
process.exit(failed)
|
package/bin/fde.js
CHANGED
|
@@ -1134,7 +1134,7 @@ function cmdResume(args) {
|
|
|
1134
1134
|
const fdeDir = path.join(engRoot, '.fde')
|
|
1135
1135
|
const existed = fs.existsSync(fdeDir)
|
|
1136
1136
|
|
|
1137
|
-
// Optional stubs (AI eval pack, …) stay in templates/ for copy-on-use
|
|
1137
|
+
// Optional stubs (AI eval pack, …) stay in templates/ for copy-on-use - not day-1 scaffold.
|
|
1138
1138
|
const SKIP_INIT_TEMPLATES = new Set(['evals.md'])
|
|
1139
1139
|
const fillTemplates = (destFde) => {
|
|
1140
1140
|
for (const f of fs.readdirSync(tpl)) {
|
|
@@ -1381,7 +1381,7 @@ function setContextPhase(eng, phase) {
|
|
|
1381
1381
|
// as one dated debrief block. contact: lines may carry an inline [signal:x]
|
|
1382
1382
|
// token anywhere in the text - preserved verbatim so computeSignals can trust it.
|
|
1383
1383
|
// --smart: thin heuristic propose (existing prefixes + light keywords). Not a
|
|
1384
|
-
// brain
|
|
1384
|
+
// brain - the agent rewrites .debrief-propose with prefixes; --apply commits.
|
|
1385
1385
|
// --dry-run prints the routing without writing anything.
|
|
1386
1386
|
function inferContactSignal(text) {
|
|
1387
1387
|
const t = String(text)
|
|
@@ -1392,7 +1392,7 @@ function inferContactSignal(text) {
|
|
|
1392
1392
|
}
|
|
1393
1393
|
|
|
1394
1394
|
function looksLikePersonLine(text) {
|
|
1395
|
-
// "Denise …" / "Randy opened…"
|
|
1395
|
+
// "Denise …" / "Randy opened…" - capitalized subject + field verb.
|
|
1396
1396
|
return /^[A-Z][a-z]{1,20}\b/.test(text) &&
|
|
1397
1397
|
/\b(helping|quiet|skipped|said|will|opened|resistant|champion|warm|cold|unresponsive|demo|sheet|slack)\b/i.test(text)
|
|
1398
1398
|
}
|
|
@@ -1688,7 +1688,7 @@ function cmdDebrief(args) {
|
|
|
1688
1688
|
|
|
1689
1689
|
// Pull sink: stage raw artifacts outside the memory ledger, then reuse debrief
|
|
1690
1690
|
// propose/apply. Never writes .fde/ until the FDE confirms apply. Source SaaS
|
|
1691
|
-
// (Granola/Gmail/…) is not here
|
|
1691
|
+
// (Granola/Gmail/…) is not here - only staging + the existing confirm gate.
|
|
1692
1692
|
function inboxDir(eng) {
|
|
1693
1693
|
return path.join(path.dirname(eng), '.inbox')
|
|
1694
1694
|
}
|
|
@@ -1863,7 +1863,7 @@ function cmdIngest(args) {
|
|
|
1863
1863
|
console.log(`staged → ${dest}`)
|
|
1864
1864
|
console.log(`id: ${id}`)
|
|
1865
1865
|
console.log('next: fde ingest propose ' + id)
|
|
1866
|
-
console.log('(does not write .fde/
|
|
1866
|
+
console.log('(does not write .fde/ - confirm via propose → apply)')
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
1869
|
function cmdReceipts(args) {
|
|
@@ -2157,7 +2157,7 @@ function collectDoctorIssues(eng) {
|
|
|
2157
2157
|
}
|
|
2158
2158
|
if (engagementTouchesAI(eng) && !hasEvalReceipt(eng)) {
|
|
2159
2159
|
issues.push(
|
|
2160
|
-
`phase is ${s.phase} with AI in scope but no eval receipt (evals.md Verdict or delivery Eval / Ship receipts)
|
|
2160
|
+
`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`
|
|
2161
2161
|
)
|
|
2162
2162
|
}
|
|
2163
2163
|
}
|
|
@@ -2302,7 +2302,7 @@ function hasValueBucket(eng) {
|
|
|
2302
2302
|
// "pending review", "TBD.", "n/a (blocked)" and "..." are all the same thing an
|
|
2303
2303
|
// FDE means by an empty cell - nagging about them teaches people to ignore doctor.
|
|
2304
2304
|
const PENDING_CELL_RE =
|
|
2305
|
-
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not measured|\?+|\.{2,}
|
|
2305
|
+
/^(?:pending|tbd|to ?be ?(?:measured|confirmed|determined)|n\s*\/\s*a|na|none|unknown|not measured|\?+|\.{2,}|…|-+|-+|-+)(?:[^\w].*)?$/i
|
|
2306
2306
|
|
|
2307
2307
|
function parseValueLedger(eng) {
|
|
2308
2308
|
const ledger = stripTemplateNoise(sectionBody(readClean(eng, 'delivery.md'), 'Value ledger') || '')
|
|
@@ -2359,7 +2359,7 @@ function valueLedgerStatusLines(eng, opts = {}) {
|
|
|
2359
2359
|
return lines
|
|
2360
2360
|
}
|
|
2361
2361
|
|
|
2362
|
-
// AI in scope for ship/close hygiene
|
|
2362
|
+
// AI in scope for ship/close hygiene - delivery/decisions/trust evidence only.
|
|
2363
2363
|
// Do not scan terrain.md: its template headers mention LLM and would false-positive every ship.
|
|
2364
2364
|
function engagementTouchesAI(eng) {
|
|
2365
2365
|
const trust = readClean(eng, 'trust-profile.md')
|
|
@@ -2376,7 +2376,7 @@ function hasEvalReceipt(eng) {
|
|
|
2376
2376
|
const evalsPath = path.join(eng, 'evals.md')
|
|
2377
2377
|
if (fs.existsSync(evalsPath)) {
|
|
2378
2378
|
const e = stripTemplateNoise(readClean(eng, 'evals.md'))
|
|
2379
|
-
// Empty G1 stub + "Pass / fail" heading is not a receipt
|
|
2379
|
+
// Empty G1 stub + "Pass / fail" heading is not a receipt - need a real verdict/run/result.
|
|
2380
2380
|
if (/\*\*Verdict:\*\*\s*SHIP\b/i.test(e) || /(?:^|\n)\s*-\s*\*\*Verdict:\*\*\s*SHIP\b/i.test(e)) return true
|
|
2381
2381
|
if (/\bLast run:\s*\d{4}-\d{2}-\d{2}/i.test(e)) return true
|
|
2382
2382
|
if (/\|\s*G\d+\s*\|[^|\n]+\|[^|\n]+\|[^|\n]+\|[^|\n]+\|\s*pass\s*\|/i.test(e)) return true
|
|
@@ -2397,7 +2397,7 @@ function hygieneTriageLines(eng) {
|
|
|
2397
2397
|
if (!issues.length) return []
|
|
2398
2398
|
const top = issues[0].replace(/\s+/g, ' ').trim().slice(0, 72)
|
|
2399
2399
|
return [
|
|
2400
|
-
` hygiene: ${issues.length} issue(s)
|
|
2400
|
+
` hygiene: ${issues.length} issue(s) - ${top}${issues[0].length > 72 ? '…' : ''}`,
|
|
2401
2401
|
' → say "@fde clean up the fieldbook" when ready (agent runs fde doctor; nothing auto-rewrites)',
|
|
2402
2402
|
]
|
|
2403
2403
|
}
|
|
@@ -2494,7 +2494,7 @@ function cmdPrep(args) {
|
|
|
2494
2494
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
2495
2495
|
const label = args.join(' ').trim() || 'next meeting'
|
|
2496
2496
|
// Grounded brief: only text already in .fde/. No invention (Rowboat meeting-prep rule).
|
|
2497
|
-
console.log(`MEETING PREP
|
|
2497
|
+
console.log(`MEETING PREP - ${label}`)
|
|
2498
2498
|
console.log('(grounded in local .fde/ only - if a fact is missing, it is missing)\n')
|
|
2499
2499
|
console.log(resumeTriage(eng))
|
|
2500
2500
|
const owner = readOwner(eng)
|
|
@@ -2504,7 +2504,7 @@ function cmdPrep(args) {
|
|
|
2504
2504
|
const people = extractStakeholders(eng).slice(0, 8)
|
|
2505
2505
|
console.log('\nStakeholders (table + signal history)')
|
|
2506
2506
|
if (!people.length) console.log(' (none yet - log contacts with --signal)')
|
|
2507
|
-
else people.forEach(p => console.log(` [${p.signal}] ${p.name}${p.role ? `
|
|
2507
|
+
else people.forEach(p => console.log(` [${p.signal}] ${p.name}${p.role ? ` - ${p.role}` : ''}${p.note ? ` · ${p.note.slice(0, 60)}` : ''}`))
|
|
2508
2508
|
|
|
2509
2509
|
const risks = extractRisks(eng).slice(0, 5)
|
|
2510
2510
|
console.log('\nOpen risks (table + dated bullets)')
|
|
@@ -2537,16 +2537,16 @@ function cmdGarden(args) {
|
|
|
2537
2537
|
if (gitHealth.ok) {
|
|
2538
2538
|
console.log('TIDY (contract: no new facts · no deleted substance · reversible via memory git)')
|
|
2539
2539
|
} else if (gitHealth.reason === 'broken') {
|
|
2540
|
-
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory git BROKEN
|
|
2540
|
+
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory git BROKEN - NOT reversible until ledger is repaired)')
|
|
2541
2541
|
} else {
|
|
2542
|
-
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory not git-versioned
|
|
2542
|
+
console.log('TIDY (contract: no new facts · no deleted substance · ⚠ memory not git-versioned - NOT reversible)')
|
|
2543
2543
|
}
|
|
2544
2544
|
console.log(resumeTriage(eng))
|
|
2545
2545
|
if (!gitHealth.ok) {
|
|
2546
2546
|
console.log(
|
|
2547
2547
|
gitHealth.reason === 'broken'
|
|
2548
2548
|
? '\n⚠ ledger is UNVERSIONED (corrupt .git). Repair before trusting tidy apply: mv .fde/.git .fde/.git.broken && run any fde write to re-init.'
|
|
2549
|
-
: '\n⚠ no memory git
|
|
2549
|
+
: '\n⚠ no memory git - tidy apply cannot create a reversible commit until the ledger exists.'
|
|
2550
2550
|
)
|
|
2551
2551
|
}
|
|
2552
2552
|
const proposals = []
|
|
@@ -2564,7 +2564,7 @@ function cmdGarden(args) {
|
|
|
2564
2564
|
proposals.push({
|
|
2565
2565
|
id: 'dedupe-risks',
|
|
2566
2566
|
kind: 'apply',
|
|
2567
|
-
text: `Consolidate ${dupes.length} duplicate open-risk cluster(s) (e.g. "${sample}${sample.length >= 50 ? '…' : ''}")
|
|
2567
|
+
text: `Consolidate ${dupes.length} duplicate open-risk cluster(s) (e.g. "${sample}${sample.length >= 50 ? '…' : ''}") - keep first, retire echoes`,
|
|
2568
2568
|
clusters: dupes,
|
|
2569
2569
|
})
|
|
2570
2570
|
}
|
package/bin/lib/memory.js
CHANGED
|
@@ -152,7 +152,7 @@ function createMemoryApi(deps) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// True only when .git exists AND can resolve HEAD. A corrupt ledger (broken
|
|
155
|
-
// HEAD / missing objects) still has a .git directory
|
|
155
|
+
// HEAD / missing objects) still has a .git directory - existsSync alone lied.
|
|
156
156
|
function memoryGitHealthy(eng) {
|
|
157
157
|
if (!eng) return { ok: false, reason: 'missing' }
|
|
158
158
|
if (!fs.existsSync(path.join(eng, '.git'))) return { ok: false, reason: 'missing' }
|
package/bin/lib/render.js
CHANGED
|
@@ -438,7 +438,7 @@ function todayViewHtml({ today, total, attentionCount, highRiskTotal, todayQueue
|
|
|
438
438
|
<h1 class="fb-h1">Today</h1>
|
|
439
439
|
<div class="fb-meta-line">${escapeHtml(today)} · ${total} engagement${total === 1 ? '' : 's'} · ${attentionCount} need you · ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
|
|
440
440
|
<div class="fb-block">
|
|
441
|
-
<div class="fb-sec">Queue
|
|
441
|
+
<div class="fb-sec">Queue - next action per client, worst first</div>
|
|
442
442
|
${todayQueue}
|
|
443
443
|
</div>
|
|
444
444
|
${flagsHtml ? `<div class="fb-block">
|
package/bin/lib/trust.js
CHANGED
|
@@ -95,7 +95,7 @@ function createTrustApi(deps) {
|
|
|
95
95
|
|
|
96
96
|
function nextActionLine(ctx) {
|
|
97
97
|
// lastNonEmpty: template ships an empty ## Next action; agents often append a
|
|
98
|
-
// second heading with the real bullet
|
|
98
|
+
// second heading with the real bullet - first-match would report "(none set)".
|
|
99
99
|
const body = sectionBody(ctx, 'Next action', { lastNonEmpty: true })
|
|
100
100
|
for (const raw of body.split('\n')) {
|
|
101
101
|
const t = raw.trim().replace(/^[-*]\s+/, '')
|
package/mcp/README.md
CHANGED
|
@@ -9,7 +9,7 @@ FDEOps MCP servers follow a **pluggable source model**: core owns the **sink**,
|
|
|
9
9
|
| **Source** | FDE configures separately | Granola, Slack, Notion, Gmail, file |
|
|
10
10
|
| **Sink** | FDEOps (`fdeops-ingest`) | stage → propose → apply into engagement memory |
|
|
11
11
|
|
|
12
|
-
Source MCPs fetch raw text from SaaS APIs using credentials the FDE manages. The ingest MCP never stores OAuth tokens or calls external services
|
|
12
|
+
Source MCPs fetch raw text from SaaS APIs using credentials the FDE manages. The ingest MCP never stores OAuth tokens or calls external services - it only shells out to the local `fde` CLI.
|
|
13
13
|
|
|
14
14
|
## Ground loop
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ Source MCP(s) fdeops-ingest MCP fde CLI
|
|
|
28
28
|
1. Agent pulls from whichever source MCPs are configured.
|
|
29
29
|
2. Agent stages raw content via `ingest_stage` (with `source` provenance).
|
|
30
30
|
3. Agent proposes routing via `ingest_propose`; FDE confirms.
|
|
31
|
-
4. Agent applies via `ingest_apply`
|
|
31
|
+
4. Agent applies via `ingest_apply` - nothing writes `.fde/` unreviewed.
|
|
32
32
|
|
|
33
33
|
## Packages
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ Source MCP(s) fdeops-ingest MCP fde CLI
|
|
|
38
38
|
|
|
39
39
|
## Recipes (copy-paste connect)
|
|
40
40
|
|
|
41
|
-
See [`recipes/`](./recipes/) for file, Granola-shaped, and Notion-shaped setup. In chat: `@fde I want to connect Granola` → skill `
|
|
41
|
+
See [`recipes/`](./recipes/) for file, Granola-shaped, and Notion-shaped setup. In chat: `@fde I want to connect Granola` → skill `connect` walks the FDE through config + reload + verify.
|
|
42
42
|
|
|
43
43
|
## Adding a source MCP
|
|
44
44
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Thin stdio MCP server for the FDEOps **ingest sink** only: **stage → propose → apply**.
|
|
4
4
|
|
|
5
|
-
This package shells out to the local `fde` CLI. It never calls SaaS APIs. Source MCPs (Granola, Slack, Notion, etc.) are **separate**
|
|
5
|
+
This package shells out to the local `fde` CLI. It never calls SaaS APIs. Source MCPs (Granola, Slack, Notion, etc.) are **separate** - you add those in your own `mcp.json`.
|
|
6
6
|
|
|
7
|
-
**Prefer the CLI when this workspace is bound:** `fde ingest stage|list|propose|apply`. Use this MCP when the host did not start in a bound workspace
|
|
7
|
+
**Prefer the CLI when this workspace is bound:** `fde ingest stage|list|propose|apply`. Use this MCP when the host did not start in a bound workspace - then pass `engagement` (path to `.fde/` from `fde resume --bind`) on every tool call.
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
@@ -70,12 +70,12 @@ Or after `npm link` in this directory:
|
|
|
70
70
|
|
|
71
71
|
## Daily loop (with separate source MCPs)
|
|
72
72
|
|
|
73
|
-
1. **Fetch**
|
|
74
|
-
2. **Stage**
|
|
75
|
-
3. **List**
|
|
76
|
-
4. **Propose**
|
|
77
|
-
5. **Confirm**
|
|
78
|
-
6. **Apply**
|
|
73
|
+
1. **Fetch** - Use your source MCP (e.g. Granola, Gmail) to pull raw text. FDEOps does not bundle these.
|
|
74
|
+
2. **Stage** - `ingest_stage` with `content`, `source` (e.g. `"granola"`), optional `title`.
|
|
75
|
+
3. **List** - `ingest_list` to see staged items in `.inbox/`.
|
|
76
|
+
4. **Propose** - `ingest_propose` with the staged `id`; agent reviews `.debrief-propose`.
|
|
77
|
+
5. **Confirm** - FDE approves the proposal in chat.
|
|
78
|
+
6. **Apply** - `ingest_apply` writes dated facts into `.fde/` with provenance.
|
|
79
79
|
|
|
80
80
|
Raw artifacts stay in `.inbox/`; the system of record (`.fde/`) stays thin and reviewed.
|
|
81
81
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* fdeops-ingest MCP
|
|
5
|
+
* fdeops-ingest MCP - thin stdio sink for FDEOps ingest.
|
|
6
6
|
* Shells out to local `fde` CLI only. Never calls SaaS.
|
|
7
7
|
* MCP stdio transport: newline-delimited JSON-RPC 2.0
|
|
8
8
|
* (Content-Length frames are accepted on input).
|
package/mcp/recipes/README.md
CHANGED
|
@@ -4,13 +4,13 @@ FDEOps does **not** bundle Granola / Slack / Notion OAuth and does **not** push
|
|
|
4
4
|
|
|
5
5
|
**Daily (no MCP):** paste notes to `@fde debrief`, or drop a file ([file.md](./file.md)).
|
|
6
6
|
|
|
7
|
-
**Pull (optional):** you add a **source** MCP. The agent fetches text, then runs `fde ingest` in this bound workspace (stage → propose → you confirm → apply). The `fdeops-ingest` MCP is optional
|
|
7
|
+
**Pull (optional):** you add a **source** MCP. The agent fetches text, then runs `fde ingest` in this bound workspace (stage → propose → you confirm → apply). The `fdeops-ingest` MCP is optional - only if you are not using the CLI from a bound workspace.
|
|
8
8
|
|
|
9
9
|
| Recipe | When |
|
|
10
10
|
|--------|------|
|
|
11
11
|
| [file.md](./file.md) | Transcript / export already on disk, or paste |
|
|
12
12
|
| [granola.md](./granola.md) | Meeting transcripts via a notes MCP (or export) |
|
|
13
|
-
| [slack.md](./slack.md) | Pull a thread/channel as text
|
|
13
|
+
| [slack.md](./slack.md) | Pull a thread/channel as text - never post |
|
|
14
14
|
| [notion.md](./notion.md) | Read a Notion page (or export markdown) |
|
|
15
15
|
|
|
16
|
-
**Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/
|
|
16
|
+
**Natural language:** `@fde I want to connect Granola` (or Slack / Notion) → `skills/fde/references/connect.md`.
|
package/mcp/recipes/file.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: local file / paste (no source MCP)
|
|
2
2
|
|
|
3
|
-
**Use when:** you already have a transcript, `.eml`, or export on disk
|
|
3
|
+
**Use when:** you already have a transcript, `.eml`, or export on disk - or you paste into chat. This is the default FDE path.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
package/mcp/recipes/granola.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: Granola-shaped meeting transcripts
|
|
2
2
|
|
|
3
|
-
**Use when:** meeting notes live in Granola (or a similar notes MCP). FDEOps does not ship a Granola server
|
|
3
|
+
**Use when:** meeting notes live in Granola (or a similar notes MCP). FDEOps does not ship a Granola server - you add whichever MCP/export path you trust.
|
|
4
4
|
|
|
5
5
|
Daily path if the notes are already in chat or on disk: paste to `@fde debrief` or [file.md](./file.md). This recipe is only for **pull**.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Daily path if the notes are already in chat or on disk: paste to `@fde debrief`
|
|
|
8
8
|
|
|
9
9
|
1. Enable a **Granola (or notes) MCP** in Cursor/Claude per that product’s docs.
|
|
10
10
|
2. Reload MCP / restart the host.
|
|
11
|
-
3. Test: `@fde what can you pull?`
|
|
11
|
+
3. Test: `@fde what can you pull?` - notes-source tools should appear.
|
|
12
12
|
|
|
13
13
|
The sink is **`fde ingest` in this bound workspace.** You do not need `fdeops-ingest` MCP for daily pull.
|
|
14
14
|
|
|
@@ -40,15 +40,15 @@ Replace command/args with whatever the real Granola MCP documents. FDEOps only n
|
|
|
40
40
|
|
|
41
41
|
## Agent steps
|
|
42
42
|
|
|
43
|
-
1. Capability check
|
|
43
|
+
1. Capability check - notes-source tools present?
|
|
44
44
|
2. Fetch transcript text (ask which meeting if ambiguous).
|
|
45
45
|
3. `fde ingest stage --source granola --title "<short>"` then propose → confirm → apply.
|
|
46
|
-
4. Extract decisions/risks/next
|
|
46
|
+
4. Extract decisions/risks/next - do not dump the raw transcript into `.fde/`.
|
|
47
47
|
|
|
48
48
|
## Common fails
|
|
49
49
|
|
|
50
50
|
| Symptom | Fix |
|
|
51
51
|
|---------|-----|
|
|
52
|
-
| No Granola tools | Source MCP not loaded
|
|
52
|
+
| No Granola tools | Source MCP not loaded - they save + reload |
|
|
53
53
|
| Wrong meeting | One clarifying question, then fetch |
|
|
54
54
|
| Wrong engagement | `fde resume` in this workspace before staging |
|
package/mcp/recipes/notion.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe: Notion docs / meeting notes
|
|
2
2
|
|
|
3
|
-
**Use when:** useful engagement notes live in Notion. FDEOps does not ship a Notion server
|
|
3
|
+
**Use when:** useful engagement notes live in Notion. FDEOps does not ship a Notion server - use a Notion MCP (or export markdown). We do not write back to Notion.
|
|
4
4
|
|
|
5
5
|
## Setup (once)
|
|
6
6
|
|
|
@@ -36,7 +36,7 @@ The sink is **`fde ingest` in this bound workspace.** `fdeops-ingest` MCP is opt
|
|
|
36
36
|
|
|
37
37
|
## Agent steps
|
|
38
38
|
|
|
39
|
-
1. Capability check
|
|
39
|
+
1. Capability check - Notion read tools present?
|
|
40
40
|
2. Fetch page/block text (ask which page if ambiguous).
|
|
41
41
|
3. `fde ingest stage --source notion --title "<short>"` → propose → confirm → apply.
|
|
42
42
|
|
package/mcp/recipes/slack.md
CHANGED
|
@@ -8,7 +8,7 @@ You add whatever Slack MCP your host already supports. We only accept **text you
|
|
|
8
8
|
|
|
9
9
|
1. Enable a **Slack MCP** (official or community) with read access to the threads you need.
|
|
10
10
|
2. Reload MCP / restart the host.
|
|
11
|
-
3. Test: `@fde what can you pull?`
|
|
11
|
+
3. Test: `@fde what can you pull?` - Slack fetch tools should appear. The FDEOps **CLI** (`fde ingest`) is the sink if this workspace is bound; you do not need `fdeops-ingest` MCP for daily use.
|
|
12
12
|
|
|
13
13
|
### Example mcp.json shape (illustrative)
|
|
14
14
|
|
|
@@ -40,10 +40,10 @@ Optional sink MCP (only if you are not running `fde ingest` from this workspace)
|
|
|
40
40
|
|
|
41
41
|
## Agent steps
|
|
42
42
|
|
|
43
|
-
1. Capability check
|
|
43
|
+
1. Capability check - Slack **read** tools present? If not → this recipe, then stop.
|
|
44
44
|
2. Fetch the thread/channel as **text** (ask which channel/thread if ambiguous).
|
|
45
45
|
3. `fde ingest stage --source slack --title "<short>"` (CLI in this bound workspace).
|
|
46
|
-
4. Propose → FDE confirms → apply. Extract decisions/risks/asks
|
|
46
|
+
4. Propose → FDE confirms → apply. Extract decisions/risks/asks - do not dump the thread into `.fde/`.
|
|
47
47
|
|
|
48
48
|
## Never
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ Optional sink MCP (only if you are not running `fde ingest` from this workspace)
|
|
|
55
55
|
|
|
56
56
|
| Symptom | Fix |
|
|
57
57
|
|---------|-----|
|
|
58
|
-
| No Slack tools | Source MCP not loaded
|
|
59
|
-
| Missing channel | Token/scopes cannot read that workspace
|
|
58
|
+
| No Slack tools | Source MCP not loaded - they save + reload; we cannot silent-install |
|
|
59
|
+
| Missing channel | Token/scopes cannot read that workspace - their Slack admin, not FDEOps |
|
|
60
60
|
| Huge dump | Stage full text in `.inbox/`; apply only short dated facts |
|
|
61
61
|
| Wrong client | Bind this workspace (`fde resume`) before staging |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"description": "Forward deployed engineering skills for AI coding agents. Your agent forgets the client every morning - the sponsor, the promise, who signed off. FDEOps keeps that as dated markdown on your laptop: one @fde skill, a deterministic local CLI, and hooks that make it automatic. Claude Code plugin and any agent that loads skills.",
|
|
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.16.0",
|
|
5
5
|
"description": "Forward deployed engineering skills for AI coding agents: per-client memory in local .fde/ files, one @fde skill. Local-only, no network.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|