fdeops 3.7.7 → 3.8.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 +2 -2
- package/bin/fde.js +164 -36
- package/bin/install.js +10 -0
- package/package.json +1 -1
- package/skills/fde/SKILL.md +2 -1
- package/skills/fde/references/land.md +1 -0
- package/templates/.fde/stakeholders.md +8 -0
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - working in the fdeops repository
|
|
2
2
|
|
|
3
|
-
This repository **is** fdeops - the second brain for Forward Deployed Engineers. One `@fde` skill routes an entire client engagement across six domains, the `fde` CLI does the deterministic work, and per-customer memory
|
|
3
|
+
This repository **is** fdeops - the second brain for Forward Deployed Engineers. One `@fde` skill routes an entire client engagement across six domains, the `fde` CLI does the deterministic work, and per-customer memory lands in `.fde/` files as a side effect of the work (you still confirm judgment).
|
|
4
4
|
|
|
5
5
|
## If you are helping use fdeops in an engagement
|
|
6
6
|
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ A notes app stores what you type. fdeops loads the right client into your AI age
|
|
|
37
37
|
| **After a meeting** | Notes rot in a scratch file | `fde debrief` routes decisions, risks, deliveries, and contacts into the record, dated |
|
|
38
38
|
| **Scope dispute** | "Small" additions absorbed silently; no record when the sponsor asks | `fde receipts <term>` answers "when did we agree to that?" with dates |
|
|
39
39
|
| **Quiet stakeholder** | Noticed three weeks too late | `fde log contact --signal amber` the day it happens; `fde status` surfaces it |
|
|
40
|
-
| **Multiple clients** | Details blur across engagements | One folder per client
|
|
40
|
+
| **Multiple clients** | Details blur across engagements | One folder per client; bind the workspace so writes cannot land on a name-alike checkout |
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -66,7 +66,7 @@ fdeops' `--init` creates the engagement memory at `~/fde-engagements/garvey/.fde
|
|
|
66
66
|
@fde I just got the brief. New client, payments platform, they want it live before their Q3 audit.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`@fde` is the one skill fdeops installs. Describe what's happening; it routes to the right field method and
|
|
69
|
+
`@fde` is the one skill fdeops installs. Describe what's happening; it routes to the right field method and writes matching `.fde/` artifacts — you still confirm judgment. Full workflow: [docs/USAGE.md](docs/USAGE.md).
|
|
70
70
|
|
|
71
71
|
Not ready to install? `npx fdeops scan` runs on any repo you can read - day-1 recon (pure `git` + file reads, no config, no account) that maps hotspots, test gaps, and reverted attempts, and ends with the ASK ON DAY 1 questions the brief never mentions. The scan is heuristic by design - treat its output as leads to verify on day one, not findings.
|
|
72
72
|
|
package/bin/fde.js
CHANGED
|
@@ -93,7 +93,11 @@ function readRegistry() {
|
|
|
93
93
|
} catch (_) { return [] }
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function resolveEngagement() {
|
|
96
|
+
function resolveEngagement(opts = {}) {
|
|
97
|
+
// opts.forWrite: memory mutations (log/debrief/capture) require an intentional
|
|
98
|
+
// bind - env, registry, pointer, or in-repo .fde. Basename matching is
|
|
99
|
+
// read-only convenience; writing on a folder-name guess contaminates clients.
|
|
100
|
+
const forWrite = !!opts.forWrite
|
|
97
101
|
// 1) explicit env (back-compat: accept old FDEOS_ENGAGEMENT too)
|
|
98
102
|
const env = (process.env.FDEOPS_ENGAGEMENT || process.env.FDEOS_ENGAGEMENT || '').replace(/^~/, HOME).trim()
|
|
99
103
|
if (env && fs.existsSync(env)) return env
|
|
@@ -123,15 +127,21 @@ function resolveEngagement() {
|
|
|
123
127
|
}
|
|
124
128
|
} catch (_) {}
|
|
125
129
|
}
|
|
126
|
-
// 4) workspace dir name matches an engagement slug.
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// unnoticed, and tell the user how to make the binding explicit.
|
|
132
|
-
const guess = path.join(ENGAGEMENTS_ROOT, slugify(path.basename(cwd)), '.fde')
|
|
130
|
+
// 4) workspace dir name matches an engagement slug. Read-only convenience.
|
|
131
|
+
// NEVER a write target - an unbound checkout named like a client must not
|
|
132
|
+
// append into that client's memory.
|
|
133
|
+
const slugGuess = slugify(path.basename(cwd))
|
|
134
|
+
const guess = path.join(ENGAGEMENTS_ROOT, slugGuess, '.fde')
|
|
133
135
|
if (fs.existsSync(guess)) {
|
|
134
|
-
|
|
136
|
+
if (forWrite) {
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
`no binding for this workspace - folder name matched "${slugGuess}" but writes require an explicit bind.\n` +
|
|
139
|
+
`run: fde resume --init ${slugGuess}\n` +
|
|
140
|
+
` or: export FDEOPS_ENGAGEMENT=${guess}\n`
|
|
141
|
+
)
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
process.stderr.write(`⚠ resolved engagement by directory name ("${slugGuess}"), not a saved binding (read-only). If this is the right client, run \`fde resume --init ${slugGuess}\` here to bind it before logging or debriefing.\n`)
|
|
135
145
|
return guess
|
|
136
146
|
}
|
|
137
147
|
// 5) in-repo .fde (engagement-approved only)
|
|
@@ -156,6 +166,49 @@ function readEng(eng, f) {
|
|
|
156
166
|
// a markdown file, so a forgotten stripPrivate() call can't leak a <private> block.
|
|
157
167
|
function readClean(eng, f) { return stripPrivate(readEng(eng, f)) }
|
|
158
168
|
|
|
169
|
+
// CLI-owned append-only mirror of [signal:x] lines. Survives an agent rewrite
|
|
170
|
+
// that drops stakeholders.md "## Signal history" - skill discipline still
|
|
171
|
+
// matters, but CLI-logged trust tokens must not vanish with the markdown.
|
|
172
|
+
const SIGNAL_LEDGER = '.signal-ledger'
|
|
173
|
+
|
|
174
|
+
// Exclusive create lock + retry. Two parallel agent sessions (or hook + CLI)
|
|
175
|
+
// appending the same .fde file otherwise interleave/corrupt under load.
|
|
176
|
+
function withFileLock(targetPath, fn) {
|
|
177
|
+
const lockPath = targetPath + '.lock'
|
|
178
|
+
const deadline = Date.now() + 5000
|
|
179
|
+
while (true) {
|
|
180
|
+
let fd
|
|
181
|
+
try {
|
|
182
|
+
fd = fs.openSync(lockPath, 'wx')
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (e.code !== 'EEXIST') throw e
|
|
185
|
+
if (Date.now() > deadline) {
|
|
186
|
+
console.error(`could not lock ${path.basename(targetPath)} - another writer is active; retry`)
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
const waitUntil = Date.now() + 20
|
|
190
|
+
while (Date.now() < waitUntil) { /* spin */ }
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return fn()
|
|
195
|
+
} finally {
|
|
196
|
+
try { fs.closeSync(fd) } catch (_) {}
|
|
197
|
+
try { fs.unlinkSync(lockPath) } catch (_) {}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function atomicWriteFile(p, content) {
|
|
203
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`
|
|
204
|
+
fs.writeFileSync(tmp, content)
|
|
205
|
+
fs.renameSync(tmp, p)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lockedAppendFile(p, text) {
|
|
209
|
+
withFileLock(p, () => { fs.appendFileSync(p, text) })
|
|
210
|
+
}
|
|
211
|
+
|
|
159
212
|
// Pull the body under a "## Heading" up to the next "##" (or EOF).
|
|
160
213
|
function sectionBody(md, heading) {
|
|
161
214
|
const lines = md.split('\n')
|
|
@@ -169,6 +222,49 @@ function sectionBody(md, heading) {
|
|
|
169
222
|
return body.join('\n').trim()
|
|
170
223
|
}
|
|
171
224
|
|
|
225
|
+
// Append `entry` as the last line of a "## Heading" section, creating the
|
|
226
|
+
// section at end-of-file if it doesn't exist yet. Plain fs.appendFileSync
|
|
227
|
+
// would land the entry after ANY later section the agent added (e.g. a
|
|
228
|
+
// "## Notes" heading appended after "## Signal history"), silently moving a
|
|
229
|
+
// signal token outside the section the reader scans - this keeps it inside
|
|
230
|
+
// regardless of what follows.
|
|
231
|
+
function appendUnderSection(md, heading, entry) {
|
|
232
|
+
const lines = md.split('\n')
|
|
233
|
+
const start = lines.findIndex(l => new RegExp('^#{1,6}\\s+' + heading + '\\b', 'i').test(l.trim()))
|
|
234
|
+
if (start === -1) {
|
|
235
|
+
const sep = md.length && !md.endsWith('\n') ? '\n' : ''
|
|
236
|
+
return `${md}${sep}\n## ${heading}\n\n${entry}\n`
|
|
237
|
+
}
|
|
238
|
+
let end = lines.length
|
|
239
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
240
|
+
if (/^#{1,6}\s/.test(lines[i].trim())) { end = i; break }
|
|
241
|
+
}
|
|
242
|
+
const before = lines.slice(0, end)
|
|
243
|
+
const after = lines.slice(end)
|
|
244
|
+
while (before.length > start + 1 && before[before.length - 1].trim() === '') before.pop()
|
|
245
|
+
before.push(entry)
|
|
246
|
+
if (after.length) before.push('')
|
|
247
|
+
return before.concat(after).join('\n')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Shared by cmdLog and cmdDebrief so the two writers can't drift (the earlier
|
|
251
|
+
// bug: fde log's format and fde debrief's format both existed, only one of
|
|
252
|
+
// them matched what extractStakeholders actually read). A contact entry
|
|
253
|
+
// carrying a [signal:x] token - however it got there - lands inside
|
|
254
|
+
// "## Signal history"; everything else is a plain end-of-file append.
|
|
255
|
+
function appendLogEntry(eng, type, entry) {
|
|
256
|
+
const p = path.join(eng, LOG_FILES[type])
|
|
257
|
+
if (type === 'contact' && /\[signal:(red|amber|green)\]/i.test(entry)) {
|
|
258
|
+
withFileLock(p, () => {
|
|
259
|
+
atomicWriteFile(p, appendUnderSection(readEng(eng, LOG_FILES[type]), 'Signal history', entry))
|
|
260
|
+
})
|
|
261
|
+
// Durable CLI ledger - not rewritten by agent artifact passes.
|
|
262
|
+
lockedAppendFile(path.join(eng, SIGNAL_LEDGER), `${entry}\n`)
|
|
263
|
+
} else {
|
|
264
|
+
lockedAppendFile(p, `\n${entry}\n`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
172
268
|
// phase / trust / top risk / freshness - identical heuristic for status + dashboard.
|
|
173
269
|
// Trust resolution: structured [signal:red|amber|green] tokens in stakeholders.md
|
|
174
270
|
// (written by `fde log contact --signal` and `fde debrief`) win - the latest dated
|
|
@@ -180,9 +276,11 @@ function computeSignals(eng) {
|
|
|
180
276
|
// readClean, not readEng: status/dashboard echo topRisk and stakeholder lines
|
|
181
277
|
// to the terminal and the rendered HTML - a <private> risk must never surface.
|
|
182
278
|
const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md'); const risks = readClean(eng, 'risks.md')
|
|
279
|
+
// Prefer structured tokens from stakeholders + CLI ledger (ledger survives wipes)
|
|
280
|
+
const signalText = stake + '\n' + readClean(eng, SIGNAL_LEDGER)
|
|
183
281
|
const phase = (ctx.match(/phase[:* ]+\**([a-z-]+)/i) || [])[1] || '?'
|
|
184
282
|
let latest = null
|
|
185
|
-
for (const l of
|
|
283
|
+
for (const l of signalText.split('\n')) {
|
|
186
284
|
const sm = l.match(/\[signal:(red|amber|green)\]/i)
|
|
187
285
|
if (!sm) continue
|
|
188
286
|
const date = (l.match(/\[(\d{4}-\d{2}-\d{2})\]/) || [])[1] || ''
|
|
@@ -305,9 +403,21 @@ function extractStakeholders(eng) {
|
|
|
305
403
|
const notesIdx = colIndex(headers, /notes?/i)
|
|
306
404
|
|
|
307
405
|
const history = []
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
406
|
+
// Format-agnostic on token position: `fde log contact --signal` writes
|
|
407
|
+
// "[date] [signal:x] text" (token right after the date), but `fde debrief`
|
|
408
|
+
// appends the token at the END of whatever the agent wrote per the skill's
|
|
409
|
+
// own contact: convention - "[date] text [signal:x]". Both are subject-first
|
|
410
|
+
// once the token is stripped, so match the token anywhere on the line rather
|
|
411
|
+
// than requiring it immediately after the date; a debrief-written signal was
|
|
412
|
+
// silently invisible to per-stakeholder matching before this.
|
|
413
|
+
const histText = sectionBody(md, 'Signal history') + '\n' + readEng(eng, SIGNAL_LEDGER)
|
|
414
|
+
histText.split('\n').forEach(l => {
|
|
415
|
+
const dm = l.trim().match(/^-\s*\[(\d{4}-\d{2}-\d{2})\]\s*(.*)$/i)
|
|
416
|
+
if (!dm) return
|
|
417
|
+
const sm = dm[2].match(/\[signal:(red|amber|green)\]/i)
|
|
418
|
+
if (!sm) return
|
|
419
|
+
const text = dm[2].replace(/\[signal:(red|amber|green)\]/i, '').trim()
|
|
420
|
+
history.push({ date: dm[1], signal: sm[1].toLowerCase(), text })
|
|
311
421
|
})
|
|
312
422
|
|
|
313
423
|
return rows.map(cs => {
|
|
@@ -420,7 +530,8 @@ function extractLog(eng) {
|
|
|
420
530
|
sectionBody(readClean(eng, 'risks.md'), 'Retired').split('\n').forEach(l => {
|
|
421
531
|
const m = l.trim().match(FLAT); if (m) push(m[1], m[2], 'receipt')
|
|
422
532
|
})
|
|
423
|
-
sectionBody(readClean(eng, 'stakeholders.md'), 'Signal history')
|
|
533
|
+
const signalLog = sectionBody(readClean(eng, 'stakeholders.md'), 'Signal history') + '\n' + readClean(eng, SIGNAL_LEDGER)
|
|
534
|
+
signalLog.split('\n').forEach(l => {
|
|
424
535
|
const m = l.trim().match(FLAT); if (m) push(m[1], m[2], 'note')
|
|
425
536
|
})
|
|
426
537
|
|
|
@@ -542,7 +653,7 @@ function cmdResume(args) {
|
|
|
542
653
|
const prev = readRegistry().find(r => r.workspace === cwd)
|
|
543
654
|
const kept = readRegistry().filter(r => r.workspace !== cwd).map(r => `${r.workspace} ${r.slug}`)
|
|
544
655
|
kept.push(`${cwd} ${slug}`)
|
|
545
|
-
|
|
656
|
+
withFileLock(REGISTRY, () => { atomicWriteFile(REGISTRY, kept.join('\n') + '\n') })
|
|
546
657
|
console.log(`ENGAGEMENT READY: ${fdeDir}\nbound to workspace: ${cwd}`)
|
|
547
658
|
if (prev && prev.slug !== slug) console.log(`rebound: this workspace previously wrote to "${prev.slug}" - that memory is untouched; sessions here now write to "${slug}"`)
|
|
548
659
|
// NDA surface: engagement notes must not silently leave the machine via file sync
|
|
@@ -617,10 +728,11 @@ function cmdLog(args) {
|
|
|
617
728
|
const type = args[0]; const text = args.slice(1).join(' ')
|
|
618
729
|
if (!LOG_FILES[type] || !text) { console.error('usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green]'); process.exit(1) }
|
|
619
730
|
if (signal && type !== 'contact') { console.error('--signal only applies to: fde log contact'); process.exit(1) }
|
|
620
|
-
const eng = resolveEngagement()
|
|
731
|
+
const eng = resolveEngagement({ forWrite: true })
|
|
621
732
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
622
733
|
const date = new Date().toISOString().slice(0, 10)
|
|
623
|
-
|
|
734
|
+
const entry = `- [${date}] ${signal ? `[signal:${signal}] ` : ''}${text}`
|
|
735
|
+
appendLogEntry(eng, type, entry)
|
|
624
736
|
console.log(`logged → ${LOG_FILES[type]}${signal ? ` (signal:${signal})` : ''}`)
|
|
625
737
|
}
|
|
626
738
|
|
|
@@ -639,7 +751,7 @@ function cmdDebrief(args) {
|
|
|
639
751
|
const dryIdx = args.indexOf('--dry-run')
|
|
640
752
|
const dry = dryIdx !== -1
|
|
641
753
|
if (dry) args.splice(dryIdx, 1)
|
|
642
|
-
const eng = resolveEngagement()
|
|
754
|
+
const eng = resolveEngagement({ forWrite: true })
|
|
643
755
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
644
756
|
let input = ''
|
|
645
757
|
if (args[0]) {
|
|
@@ -676,15 +788,19 @@ function cmdDebrief(args) {
|
|
|
676
788
|
const m = bare.match(/^(decision|risk|delivery|contact):\s*(.+)$/i)
|
|
677
789
|
if (m) {
|
|
678
790
|
const type = m[1].toLowerCase()
|
|
679
|
-
|
|
680
|
-
|
|
791
|
+
const entry = `- [${date}] ${m[2]}`
|
|
792
|
+
if (dry) console.log(`→ ${LOG_FILES[type]} ${entry}`)
|
|
793
|
+
// appendLogEntry, not a blind append: a contact: line may carry an
|
|
794
|
+
// inline [signal:x] token (the skill's own convention) and must land
|
|
795
|
+
// inside "## Signal history" the same way `fde log --signal` does.
|
|
796
|
+
else appendLogEntry(eng, type, entry)
|
|
681
797
|
counts[type]++
|
|
682
798
|
} else ctxLines.push(line)
|
|
683
799
|
}
|
|
684
800
|
if (ctxLines.length) {
|
|
685
801
|
const stamp = `${date} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
686
802
|
if (dry) ctxLines.forEach(l => console.log(`→ context.md - ${l}`))
|
|
687
|
-
else
|
|
803
|
+
else lockedAppendFile(path.join(eng, 'context.md'), `\n## Debrief - ${stamp}\n${ctxLines.map(l => `- ${l}`).join('\n')}\n`)
|
|
688
804
|
}
|
|
689
805
|
const plural = { decision: 'decisions', risk: 'risks', delivery: 'deliveries', contact: 'contacts' }
|
|
690
806
|
const parts = Object.keys(counts).filter(t => counts[t])
|
|
@@ -739,7 +855,7 @@ function cmdReceipts(args) {
|
|
|
739
855
|
}
|
|
740
856
|
|
|
741
857
|
function cmdCapture() {
|
|
742
|
-
const eng = resolveEngagement()
|
|
858
|
+
const eng = resolveEngagement({ forWrite: true })
|
|
743
859
|
if (!eng) process.exit(0) // silent: capture must never break a session
|
|
744
860
|
const branch = sh('git branch --show-current')
|
|
745
861
|
const lastCommit = sh("git log -1 --format='%h %s'").slice(0, 100)
|
|
@@ -757,7 +873,7 @@ function cmdCapture() {
|
|
|
757
873
|
if (branch) block += `- workspace: \`${branch}\` @ ${lastCommit || 'no commits yet'}\n`
|
|
758
874
|
if (changed) block += `- uncommitted: ${changed}\n`
|
|
759
875
|
if (updated) block += `- engagement files updated: ${updated}\n`
|
|
760
|
-
try {
|
|
876
|
+
try { lockedAppendFile(path.join(eng, 'context.md'), block) } catch (_) {}
|
|
761
877
|
}
|
|
762
878
|
|
|
763
879
|
function engagementSlugFromPath(eng) {
|
|
@@ -1554,18 +1670,8 @@ ${clientViews}
|
|
|
1554
1670
|
}
|
|
1555
1671
|
}
|
|
1556
1672
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
case 'scan': cmdScan(); break
|
|
1560
|
-
case 'resume': cmdResume(args); break
|
|
1561
|
-
case 'log': cmdLog(args); break
|
|
1562
|
-
case 'debrief': cmdDebrief(args); break
|
|
1563
|
-
case 'receipts': cmdReceipts(args); break
|
|
1564
|
-
case 'capture': cmdCapture(); break
|
|
1565
|
-
case 'status': cmdStatus(args); break
|
|
1566
|
-
case 'dashboard': cmdDashboard(args); break
|
|
1567
|
-
default:
|
|
1568
|
-
console.log(`fde - deterministic core of fdeops
|
|
1673
|
+
function printUsage() {
|
|
1674
|
+
console.log(`fde - deterministic core of fdeops
|
|
1569
1675
|
fde scan day-1 recon of this repo (facts, no AI)
|
|
1570
1676
|
fde resume load this workspace's engagement memory (bounded)
|
|
1571
1677
|
fde resume --full load the complete context.md (no bound)
|
|
@@ -1577,5 +1683,27 @@ switch (cmd) {
|
|
|
1577
1683
|
fde capture session-end memory snapshot (hooks use this)
|
|
1578
1684
|
fde status [--all] current engagement status (pass --all for full portfolio)
|
|
1579
1685
|
fde dashboard [--all] current engagement fieldbook (pass --all for every client)
|
|
1580
|
-
env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
|
|
1686
|
+
env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
|
|
1687
|
+
writes require a workspace bind (or FDEOPS_ENGAGEMENT) - folder-name match is read-only`)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const [cmd, ...args] = process.argv.slice(2)
|
|
1691
|
+
switch (cmd) {
|
|
1692
|
+
case 'scan': cmdScan(); break
|
|
1693
|
+
case 'resume': cmdResume(args); break
|
|
1694
|
+
case 'log': cmdLog(args); break
|
|
1695
|
+
case 'debrief': cmdDebrief(args); break
|
|
1696
|
+
case 'receipts': cmdReceipts(args); break
|
|
1697
|
+
case 'capture': cmdCapture(); break
|
|
1698
|
+
case 'status': cmdStatus(args); break
|
|
1699
|
+
case 'dashboard': cmdDashboard(args); break
|
|
1700
|
+
case 'help':
|
|
1701
|
+
case '-h':
|
|
1702
|
+
case '--help':
|
|
1703
|
+
printUsage()
|
|
1704
|
+
break
|
|
1705
|
+
default:
|
|
1706
|
+
printUsage()
|
|
1707
|
+
// Missing or unknown command must fail - exit 0 made typos look like success in scripts/hooks.
|
|
1708
|
+
process.exit(1)
|
|
1581
1709
|
}
|
package/bin/install.js
CHANGED
|
@@ -142,6 +142,16 @@ function cmdAdapters(targetDir) {
|
|
|
142
142
|
console.log(` fdeops cross-platform adapters → ${dest}`)
|
|
143
143
|
console.log(' One brain (skills/fde/SKILL.md). These are thin pointers per tool.')
|
|
144
144
|
console.log('')
|
|
145
|
+
// The pointers below all point at ~/.claude/skills/fde/SKILL.md. Only the
|
|
146
|
+
// default install (bare `npx fdeops` / `node bin/install.js`) used to place
|
|
147
|
+
// that file - `adapters` alone wrote pointers to a brain that didn't exist
|
|
148
|
+
// yet, a dangling reference for anyone following the documented Cursor/Codex
|
|
149
|
+
// path. installSkills() is idempotent (safe to call every run).
|
|
150
|
+
if (!fs.existsSync(path.join(GLOBAL_SKILLS_DIR, 'fde', 'SKILL.md'))) {
|
|
151
|
+
installSkills()
|
|
152
|
+
console.log(' Skills → ~/.claude/skills/ (installed - the pointers below need this)')
|
|
153
|
+
console.log('')
|
|
154
|
+
}
|
|
145
155
|
for (const a of ADAPTER_TARGETS) {
|
|
146
156
|
if (!fs.existsSync(a.src)) { console.log(` skip ${a.label} (template missing)`); continue }
|
|
147
157
|
placePointer(path.join(dest, a.dest), fs.readFileSync(a.src, 'utf8'), a.label, a.appendable)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Field kit for engineers embedded in client work - a real CLI (recon, memory, portfolio), one @fde skill with field judgment on top, 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/skills/fde/SKILL.md
CHANGED
|
@@ -28,6 +28,7 @@ This is what makes fdeops a second brain instead of a chat window.
|
|
|
28
28
|
4. **No invented facts - ever.** People, names, quotes, meetings, and numbers exist only if the FDE said them or the repo shows them. Never invent a stakeholder, a conversation, or a source to make the narrative richer - one fabricated name poisons every real citation around it. A missing fact is written as `unknown - ask: <the question>`, nothing else.
|
|
29
29
|
5. **On exit:** before the session ends, append three lines to `context.md`: where we are, what changed today, the next step. The `session-stop` hook backstops this deterministically (hooks resolve the engagement through the workspace registry - no env var needed), but you write the meaningful version.
|
|
30
30
|
6. **One customer, one folder.** Never merge two engagements into one `.fde/`. Confirm which engagement applies when multiple exist.
|
|
31
|
+
7. **Never delete a code-read section when rewriting an artifact.** `stakeholders.md`'s `## Signal history` holds dated `[signal:...]` tokens that `fde status`/`fde receipts`/the dashboard read verbatim; `risks.md`'s `## Retired` is read the same way. Rewriting either file as an artifact (land, audit, stakeholder-radar) is fine - dropping one of these sections is not. Carry existing entries forward untouched.
|
|
31
32
|
|
|
32
33
|
## Data boundary (confirm before touching their code)
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ This is what makes fdeops a second brain instead of a chat window.
|
|
|
37
38
|
- Data tagged `<private>` in `trust-profile.md` (sacred data, PHI, cardholder, classified) **never enters your context or any subagent prompt** - work around it, never with it.
|
|
38
39
|
- Locked-down engagement (no AI on their code)? Use the CLI + the fieldbook only. The memory layer is the FDE's own notes, not customer code.
|
|
39
40
|
|
|
40
|
-
**Engagement path - zero ceremony.** Run `fde resume` (fallback: `node ~/.claude/fdeops/fde.js resume`). The **workspace registry** (written once by `fde resume --init <name>`) is the normal path; resolution order is env var override → registry → pointer file → workspace-name match → `./.fde`. It prints a **bounded** view of `context.md` - the curated head (state, next action) plus the most recent activity, with the older session log collapsed (use `fde resume --full` when you genuinely need the whole history). If it reports NO ENGAGEMENT: confirm the client name in conversation (one question), then run `fde resume --init <name>` yourself - the one setup step; the FDE never runs setup commands. Never install fdeops on infrastructure the FDE does not control.
|
|
41
|
+
**Engagement path - zero ceremony.** Run `fde resume` (fallback: `node ~/.claude/fdeops/fde.js resume`). The **workspace registry** (written once by `fde resume --init <name>`) is the normal path; resolution order is env var override → registry → pointer file → workspace-name match (read-only) → `./.fde`. Writes require a bind (or `FDEOPS_ENGAGEMENT`), not folder name alone. It prints a **bounded** view of `context.md` - the curated head (state, next action) plus the most recent activity, with the older session log collapsed (use `fde resume --full` when you genuinely need the whole history). If it reports NO ENGAGEMENT: confirm the client name in conversation (one question), then run `fde resume --init <name>` yourself - the one setup step; the FDE never runs setup commands. Never install fdeops on infrastructure the FDE does not control.
|
|
41
42
|
|
|
42
43
|
**The `fde` CLI does the deterministic work - use it instead of improvising shell:**
|
|
43
44
|
|
|
@@ -69,6 +69,7 @@ Before the end of day 1, ship one visible thing: a small bug fix, a cleanup the
|
|
|
69
69
|
|-----|------|--------|-------|
|
|
70
70
|
| <name> | sponsor / champion / resistor / veto / passed-over | green/amber/red | <evidence, day> |
|
|
71
71
|
```
|
|
72
|
+
If `stakeholders.md` already has a `## Signal history` section (it does from the template), **never delete or overwrite it** when you rewrite this file - it holds the dated `[signal:...]` tokens `fde log contact --signal` and `fde debrief` write, and `fde status`/`fde receipts`/the dashboard read only from that section. Edit the table above it freely; keep the section below intact.
|
|
72
73
|
|
|
73
74
|
**`trust-profile.md`** - sacred data (`<private>` tagged), fears heard, AI policy, approval chain. Sensitive: never loaded for status reads, never into subagent prompts.
|
|
74
75
|
|
|
@@ -8,3 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
**Trust signal:** green | amber | red
|
|
10
10
|
**Last trust check:**
|
|
11
|
+
|
|
12
|
+
## Signal history
|
|
13
|
+
|
|
14
|
+
<!-- `fde log contact --signal <color>` and `fde debrief` write dated tokens here.
|
|
15
|
+
fde status/receipts read the LATEST dated [signal:...] token in this section
|
|
16
|
+
to drive the trust column - do not delete this heading, and if you rewrite
|
|
17
|
+
this file as an artifact, keep this section's entries intact. -->
|
|
18
|
+
|