fdeops 3.9.10 → 3.9.12
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/CLAUDE.md.template +1 -1
- package/README.md +1 -1
- package/adapters/AGENTS.md +1 -1
- package/adapters/GEMINI.md +1 -1
- package/adapters/copilot-instructions.md +1 -1
- package/adapters/cursor.fde.mdc +1 -1
- package/bin/check.js +31 -2
- package/bin/fde.js +46 -9
- package/hooks/pre-compact +25 -52
- package/hooks/session-start +3 -2
- package/hooks/session-stop +36 -42
- package/package.json +1 -1
- package/skills/fde/SKILL.md +1 -1
- package/skills/fde/references/healthcare.md +2 -2
- package/skills/fde/references/land.md +2 -2
- package/templates/.fde/trust-profile.md +2 -2
package/AGENTS.md
CHANGED
|
@@ -15,4 +15,4 @@ Route via **`@fde`** - read `skills/fde/SKILL.md` (the single source of truth),
|
|
|
15
15
|
|
|
16
16
|
## Boundaries
|
|
17
17
|
|
|
18
|
-
The `fde` CLI never reaches the network.
|
|
18
|
+
The `fde` CLI never reaches the network. `<private>` blocks are redacted from CLI/dashboard/hook outputs; do not load raw private blocks into a model via file tools or paste. fdeops installs on the FDE's own machine, never on customer infrastructure.
|
package/CLAUDE.md.template
CHANGED
|
@@ -22,4 +22,4 @@ Experienced FDE peer: direct, no fluff, no assumptions. Ask once if a fact chang
|
|
|
22
22
|
|
|
23
23
|
## Pause before
|
|
24
24
|
|
|
25
|
-
Production changes, irreversible actions, surprises that affect trust. `<private>`
|
|
25
|
+
Production changes, irreversible actions, surprises that affect trust. `<private>` is redacted from CLI/hook context; do not paste raw private blocks into prompts.
|
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ Open the system of record in a browser - trust, phase, next action, and the full
|
|
|
163
163
|
|
|
164
164
|
- **Local only.** Pure `git` + file reads - no network calls, no telemetry, no account. Works air-gapped.
|
|
165
165
|
- **Plain markdown.** No database, no lock-in.
|
|
166
|
-
- **No new data path.** The AI sees client code only when *you* point your agent at it
|
|
166
|
+
- **No new data path.** The AI sees client code only when *you* point your agent at it. `<private>` tags are redacted from CLI/dashboard/hook output; do not feed raw private blocks into the model (file tools bypass that redaction).
|
|
167
167
|
- **Nothing enters the record unreviewed.** The model drafts, you confirm (`fde debrief --dry-run` shows the routing first); the hooks record only git facts. Your fieldbook stays yours to defend.
|
|
168
168
|
- **Know your sync surface.** `~/fde-engagements` lives in your home directory - your backup and cloud-sync setup now covers client notes. `fde resume --init` warns if the folder sits in a synced path. Read [PRIVACY.md](PRIVACY.md) before your first NDA'd engagement.
|
|
169
169
|
|
package/adapters/AGENTS.md
CHANGED
|
@@ -21,4 +21,4 @@ A 20-year FDE peer: direct, no fluff, no assumptions. One sharp question when a
|
|
|
21
21
|
|
|
22
22
|
## Pause before
|
|
23
23
|
|
|
24
|
-
Production changes, irreversible actions, anything that affects client trust.
|
|
24
|
+
Production changes, irreversible actions, anything that affects client trust. `<private>` is redacted from CLI/dashboard/hook context; do **not** open raw private blocks with file tools or paste them into prompts/subagents - work around them, never with them.
|
package/adapters/GEMINI.md
CHANGED
|
@@ -21,4 +21,4 @@ A 20-year FDE peer: direct, no fluff, no assumptions. One sharp question when a
|
|
|
21
21
|
|
|
22
22
|
## Pause before
|
|
23
23
|
|
|
24
|
-
Production changes, irreversible actions, anything that affects client trust.
|
|
24
|
+
Production changes, irreversible actions, anything that affects client trust. `<private>` is redacted from CLI/dashboard/hook context; do **not** open raw private blocks with file tools or paste them into prompts/subagents - work around them, never with them.
|
|
@@ -21,4 +21,4 @@ A 20-year FDE peer: direct, no fluff, no assumptions. One sharp question when a
|
|
|
21
21
|
|
|
22
22
|
## Pause before
|
|
23
23
|
|
|
24
|
-
Production changes, irreversible actions, anything that affects client trust.
|
|
24
|
+
Production changes, irreversible actions, anything that affects client trust. `<private>` is redacted from CLI/dashboard/hook context; do **not** open raw private blocks with file tools or paste them into prompts/subagents - work around them, never with them.
|
package/adapters/cursor.fde.mdc
CHANGED
|
@@ -35,4 +35,4 @@ A 20-year FDE peer: direct, no fluff, no assumptions. One sharp question when a
|
|
|
35
35
|
|
|
36
36
|
## Pause before
|
|
37
37
|
|
|
38
|
-
Production changes, irreversible actions, anything that affects client trust.
|
|
38
|
+
Production changes, irreversible actions, anything that affects client trust. `<private>` is redacted from CLI/dashboard/hook context; do **not** open raw private blocks with file tools or paste them into prompts/subagents - work around them, never with them.
|
package/bin/check.js
CHANGED
|
@@ -202,12 +202,16 @@ if (fs.existsSync(pmf) && !read('docs/internal/PMF_360_REVIEW.md').includes('INT
|
|
|
202
202
|
} else if (fs.existsSync(pmf)) ok('internal PMF banner')
|
|
203
203
|
|
|
204
204
|
const hook = read('hooks/session-start')
|
|
205
|
+
const hookCode = hook.replace(/^[ \t]*#.*$/gm, '')
|
|
205
206
|
if (!hook.includes('FDEOPS_ENGAGEMENT')) {
|
|
206
207
|
fail('session-start hook must read FDEOPS_ENGAGEMENT env var')
|
|
207
208
|
} else ok('hook FDEOPS_ENGAGEMENT')
|
|
209
|
+
if (!hookCode.includes('FDEOPS_ENGAGEMENT="$ENG_DIR" fde triage') ||
|
|
210
|
+
!hookCode.includes('FDEOPS_ENGAGEMENT="$ENG_DIR" node "$FDE_CMD" triage')) {
|
|
211
|
+
fail('session-start must run triage with its resolved engagement')
|
|
212
|
+
}
|
|
208
213
|
// Token discipline: SessionStart must not dump the full skill (L1 progressive disclosure).
|
|
209
214
|
// Strip comments before scanning for a real `cat …SKILL.md` / BOOTSTRAP inject.
|
|
210
|
-
const hookCode = hook.replace(/^[ \t]*#.*$/gm, '')
|
|
211
215
|
if (/\$\(cat\s+"\$BOOTSTRAP"\)|cat\s+"\$BOOTSTRAP"|cat\s+[^\n]*SKILL\.md/.test(hookCode)) {
|
|
212
216
|
fail('session-start must not cat SKILL.md - inject TRIAGE + bounded context + pointer only')
|
|
213
217
|
}
|
|
@@ -240,10 +244,35 @@ if (!fs.existsSync(path.join(root, 'hooks', 'session-stop'))) {
|
|
|
240
244
|
fail('hooks/session-stop missing (write-side memory backstop)')
|
|
241
245
|
} else {
|
|
242
246
|
const stopHook = read('hooks/session-stop')
|
|
247
|
+
const stopHookCode = stopHook.replace(/^[ \t]*#.*$/gm, '')
|
|
243
248
|
if (!stopHook.includes('FDEOPS_ENGAGEMENT')) fail('session-stop must resolve FDEOPS_ENGAGEMENT')
|
|
244
|
-
if (!
|
|
249
|
+
if (!stopHookCode.includes('resolve_fde') || !stopHookCode.includes('command -v fde')) {
|
|
250
|
+
fail('session-stop must resolve PATH fde before plugin copies')
|
|
251
|
+
}
|
|
252
|
+
if (!/FDEOPS_ENGAGEMENT="\$ENG_DIR" (?:fde|node "\$FDE_CMD") capture/.test(stopHookCode)
|
|
253
|
+
&& !stopHookCode.includes('run_fde "$FDE_CMD" capture')) {
|
|
254
|
+
fail('session-stop must delegate capture with its resolved engagement')
|
|
255
|
+
}
|
|
245
256
|
ok('session-stop write side')
|
|
246
257
|
}
|
|
258
|
+
const compactHook = read('hooks/pre-compact')
|
|
259
|
+
const compactHookCode = compactHook.replace(/^[ \t]*#.*$/gm, '')
|
|
260
|
+
if (!compactHookCode.includes('resolve_fde') || !compactHookCode.includes('command -v fde')) {
|
|
261
|
+
fail('pre-compact must resolve PATH fde before plugin copies')
|
|
262
|
+
}
|
|
263
|
+
if (!/FDEOPS_ENGAGEMENT="\$ENG_DIR" (?:fde|node "\$FDE_CMD") preserve/.test(compactHookCode)) {
|
|
264
|
+
fail('pre-compact must delegate preserve with its resolved engagement')
|
|
265
|
+
}
|
|
266
|
+
for (const h of ['session-stop', 'pre-compact']) {
|
|
267
|
+
const body = read('hooks/' + h).replace(/^[ \t]*#.*$/gm, '')
|
|
268
|
+
const contextTarget = /(?:"?\$(?:\{)?CONTEXT_FILE(?:\})?"?|"?\$(?:\{)?ENG_DIR(?:\})?\/context\.md"?)/.source
|
|
269
|
+
const redirectsToContext = new RegExp(`>{1,2}\\s*${contextTarget}`)
|
|
270
|
+
const teesToContext = new RegExp(`\\btee\\b[^\\n]*${contextTarget}`)
|
|
271
|
+
if (redirectsToContext.test(body) || teesToContext.test(body)) {
|
|
272
|
+
fail(`hooks/${h} must not append context.md directly`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
ok('mutation hooks delegate CLI writes')
|
|
247
276
|
for (const h of ['session-start', 'session-stop', 'pre-compact']) {
|
|
248
277
|
const mode = fs.statSync(path.join(root, 'hooks', h)).mode
|
|
249
278
|
if (!(mode & 0o111)) fail(`hooks/${h} lost its executable bit`)
|
package/bin/fde.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* fde owner [set …] who keeps this engagement record
|
|
23
23
|
* fde receipts <term> "what did we agree?" - search memory with dates
|
|
24
24
|
* fde capture session-end snapshot → context.md (hooks use this)
|
|
25
|
+
* fde preserve pre-compaction context snapshot (hook-internal; hooks use this)
|
|
25
26
|
* fde status [--all] current engagement (default) or full portfolio (--all)
|
|
26
27
|
* fde dashboard [--all] current engagement fieldbook (default) or all (--all)
|
|
27
28
|
*/
|
|
@@ -559,7 +560,7 @@ function parseSignalHistoryEntries(eng) {
|
|
|
559
560
|
// Format-agnostic on token position: CLI writes "[date] [signal:x] text";
|
|
560
561
|
// debrief may put the token at the end. Author tags [@x] are stripped for matching.
|
|
561
562
|
const md = readClean(eng, 'stakeholders.md')
|
|
562
|
-
const histText = sectionBody(md, 'Signal history') + '\n' +
|
|
563
|
+
const histText = sectionBody(md, 'Signal history') + '\n' + readClean(eng, SIGNAL_LEDGER)
|
|
563
564
|
const history = []
|
|
564
565
|
histText.split('\n').forEach(l => {
|
|
565
566
|
const dm = l.trim().match(/^-\s*\[(\d{4}-\d{2}-\d{2})\]\s*(.*)$/i)
|
|
@@ -1181,6 +1182,11 @@ function routeDebriefInput(eng, input, { dry, force }) {
|
|
|
1181
1182
|
if (m) {
|
|
1182
1183
|
const type = m[1].toLowerCase()
|
|
1183
1184
|
let body = m[2]
|
|
1185
|
+
const hit = findSecretHit(body)
|
|
1186
|
+
if (hit && !force) {
|
|
1187
|
+
console.error(`skipped ${type} line - looks like a ${hit}. Redact it, or re-run with --force.`)
|
|
1188
|
+
continue
|
|
1189
|
+
}
|
|
1184
1190
|
if (type === 'next') {
|
|
1185
1191
|
if (dry) console.log(`→ context.md ## Next action - ${body}`)
|
|
1186
1192
|
else nextAction = body
|
|
@@ -1189,11 +1195,6 @@ function routeDebriefInput(eng, input, { dry, force }) {
|
|
|
1189
1195
|
}
|
|
1190
1196
|
const sigInline = (body.match(/\[signal:(red|amber|green)\]/i) || [])[1]
|
|
1191
1197
|
if (sigInline) body = body.replace(/\[signal:(red|amber|green)\]/i, '').trim()
|
|
1192
|
-
const hit = findSecretHit(body)
|
|
1193
|
-
if (hit && !force) {
|
|
1194
|
-
console.error(`skipped ${type} line - looks like a ${hit}. Redact it, or re-run with --force.`)
|
|
1195
|
-
continue
|
|
1196
|
-
}
|
|
1197
1198
|
const entry = datedEntry(eng, date, body, type === 'contact' && sigInline ? sigInline.toLowerCase() : '')
|
|
1198
1199
|
if (dry) console.log(`→ ${LOG_FILES[type]} ${entry}`)
|
|
1199
1200
|
else appendLogEntry(eng, type, entry, { skipCommit: true })
|
|
@@ -1335,7 +1336,12 @@ function cmdCapture() {
|
|
|
1335
1336
|
}).join(' ')
|
|
1336
1337
|
if (!changed && !updated) process.exit(0) // idle session - keep memory clean
|
|
1337
1338
|
const d = new Date()
|
|
1338
|
-
const
|
|
1339
|
+
const localDate = [
|
|
1340
|
+
d.getFullYear(),
|
|
1341
|
+
String(d.getMonth() + 1).padStart(2, '0'),
|
|
1342
|
+
String(d.getDate()).padStart(2, '0'),
|
|
1343
|
+
].join('-')
|
|
1344
|
+
const stamp = `${localDate} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
1339
1345
|
let block = `\n<!-- fdeops auto-capture -->\n## Session end - ${stamp}\n`
|
|
1340
1346
|
if (branch) block += `- workspace: \`${branch}\` @ ${lastCommit || 'no commits yet'}\n`
|
|
1341
1347
|
if (changed) block += `- uncommitted: ${changed}\n`
|
|
@@ -1347,6 +1353,36 @@ function cmdCapture() {
|
|
|
1347
1353
|
} catch (_) {}
|
|
1348
1354
|
}
|
|
1349
1355
|
|
|
1356
|
+
function cmdPreserve() {
|
|
1357
|
+
try {
|
|
1358
|
+
const eng = resolveEngagement({ forWrite: true })
|
|
1359
|
+
if (!eng || !fs.existsSync(path.join(eng, 'context.md'))) return
|
|
1360
|
+
const marker = '[fdeops context preserved'
|
|
1361
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
1362
|
+
const decisionLines = readClean(eng, 'decisions.md').split('\n')
|
|
1363
|
+
if (decisionLines[decisionLines.length - 1] === '') decisionLines.pop()
|
|
1364
|
+
const recentDecisions = decisionLines.slice(-20).join('\n')
|
|
1365
|
+
const openRisks = readClean(eng, 'risks.md').split('\n')
|
|
1366
|
+
.filter(line => /open|active|unresolved/i.test(line))
|
|
1367
|
+
.slice(0, 8)
|
|
1368
|
+
.join('\n')
|
|
1369
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
|
|
1370
|
+
const block = `\n---\n${marker} at ${timestamp}]\nRecent decisions (tail):\n${recentDecisions}\n\nOpen risks:\n${openRisks}\n---\n`
|
|
1371
|
+
|
|
1372
|
+
ensureMemoryGit(eng)
|
|
1373
|
+
const contextPath = path.join(eng, 'context.md')
|
|
1374
|
+
const blocked = refuseSymlinkWrite(contextPath, { soft: true })
|
|
1375
|
+
if (blocked) throw Object.assign(new Error(blocked), { code: 'ESYMLINK' })
|
|
1376
|
+
withFileLock(contextPath, () => {
|
|
1377
|
+
const context = readEng(eng, 'context.md')
|
|
1378
|
+
const preservedToday = context.split('\n')
|
|
1379
|
+
.some(line => line.includes(marker) && line.includes(today))
|
|
1380
|
+
if (!preservedToday) fs.appendFileSync(contextPath, block)
|
|
1381
|
+
}, { soft: true })
|
|
1382
|
+
commitMemory(eng, 'context preserve', { files: ['context.md'] })
|
|
1383
|
+
} catch (_) {}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1350
1386
|
function cmdTriage() {
|
|
1351
1387
|
const eng = resolveEngagement()
|
|
1352
1388
|
if (!eng) {
|
|
@@ -1830,8 +1866,7 @@ function cmdDashboard(args) {
|
|
|
1830
1866
|
|
|
1831
1867
|
try {
|
|
1832
1868
|
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
1833
|
-
|
|
1834
|
-
fs.writeFileSync(outPath, html)
|
|
1869
|
+
atomicWriteFile(outPath, html)
|
|
1835
1870
|
} catch (e) {
|
|
1836
1871
|
failFs(e, 'write fieldbook', outPath)
|
|
1837
1872
|
}
|
|
@@ -1868,6 +1903,7 @@ function printUsage() {
|
|
|
1868
1903
|
fde owner [set email] who keeps this engagement record
|
|
1869
1904
|
fde receipts <term> "what did we agree?" with dates
|
|
1870
1905
|
fde capture session-end memory snapshot (hooks use this)
|
|
1906
|
+
fde preserve pre-compaction context snapshot (hook-internal; hooks use this)
|
|
1871
1907
|
fde status [--all] current engagement status (pass --all for full portfolio)
|
|
1872
1908
|
fde dashboard [--all] current engagement fieldbook (pass --all for every client)
|
|
1873
1909
|
env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
|
|
@@ -1889,6 +1925,7 @@ switch (cmd) {
|
|
|
1889
1925
|
case 'owner': cmdOwner(args); break
|
|
1890
1926
|
case 'receipts': cmdReceipts(args); break
|
|
1891
1927
|
case 'capture': cmdCapture(); break
|
|
1928
|
+
case 'preserve': cmdPreserve(); break
|
|
1892
1929
|
case 'status': cmdStatus(args); break
|
|
1893
1930
|
case 'dashboard': cmdDashboard(args); break
|
|
1894
1931
|
case 'help':
|
package/hooks/pre-compact
CHANGED
|
@@ -36,6 +36,25 @@ registry_engagement_dir() {
|
|
|
36
36
|
return 1
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# Prefer PATH `fde`, then plugin/repo copies - same order as session-start.
|
|
40
|
+
resolve_fde() {
|
|
41
|
+
if command -v fde >/dev/null 2>&1; then
|
|
42
|
+
printf '%s\n' "fde"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
for candidate in \
|
|
46
|
+
"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/bin/fde.js}" \
|
|
47
|
+
"$(dirname "$0")/../bin/fde.js" \
|
|
48
|
+
"$HOME/.claude/fdeops/fde.js" \
|
|
49
|
+
"$HOME/.claude/plugins/fdeops/bin/fde.js"; do
|
|
50
|
+
if [ -n "$candidate" ] && [ -f "$candidate" ]; then
|
|
51
|
+
printf '%s\n' "$candidate"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
|
|
40
59
|
|
|
41
60
|
if [ -z "$ENG_DIR" ]; then
|
|
@@ -58,59 +77,13 @@ fi
|
|
|
58
77
|
|
|
59
78
|
[ -z "$ENG_DIR" ] && exit 0
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Avoid unbounded growth: skip if we already preserved today.
|
|
69
|
-
if grep -Fq "$MARKER" "$CONTEXT_FILE" 2>/dev/null; then
|
|
70
|
-
LAST=$(grep -F "$MARKER" "$CONTEXT_FILE" | tail -1)
|
|
71
|
-
if echo "$LAST" | grep -q "$(date -u +%Y-%m-%d)"; then
|
|
72
|
-
exit 0
|
|
80
|
+
FDE_CMD=$(resolve_fde || true)
|
|
81
|
+
if [ -n "$FDE_CMD" ]; then
|
|
82
|
+
if [ "$FDE_CMD" = "fde" ]; then
|
|
83
|
+
FDEOPS_ENGAGEMENT="$ENG_DIR" fde preserve >/dev/null 2>&1 || true
|
|
84
|
+
else
|
|
85
|
+
FDEOPS_ENGAGEMENT="$ENG_DIR" node "$FDE_CMD" preserve >/dev/null 2>&1 || true
|
|
73
86
|
fi
|
|
74
87
|
fi
|
|
75
88
|
|
|
76
|
-
# Redact <private> before extracting lines - this content is appended to
|
|
77
|
-
# context.md, which session-start loads into the model. A closed private block
|
|
78
|
-
# collapses to one placeholder line, so its inner text can't be tail'd or
|
|
79
|
-
# grepped out tag-stripped. Case-insensitive, mirrors the JS /gi redactor.
|
|
80
|
-
strip_private() {
|
|
81
|
-
awk '
|
|
82
|
-
BEGIN { inblock = 0 }
|
|
83
|
-
{
|
|
84
|
-
line = $0
|
|
85
|
-
lc = tolower(line)
|
|
86
|
-
if (inblock) { if (index(lc, "</private>") > 0) { inblock = 0 } next }
|
|
87
|
-
if (index(lc, "<private>") > 0) {
|
|
88
|
-
print "(private - redacted)"
|
|
89
|
-
if (index(lc, "</private>") == 0) { inblock = 1 }
|
|
90
|
-
next
|
|
91
|
-
}
|
|
92
|
-
print line
|
|
93
|
-
}
|
|
94
|
-
' "$1"
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
98
|
-
LAST_DECISIONS=""
|
|
99
|
-
OPEN_RISKS=""
|
|
100
|
-
|
|
101
|
-
[ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(strip_private "$DECISIONS_FILE" 2>/dev/null | tail -20)
|
|
102
|
-
[ -f "$RISKS_FILE" ] && OPEN_RISKS=$(strip_private "$RISKS_FILE" 2>/dev/null | grep -iE "open|active|unresolved" | head -8)
|
|
103
|
-
|
|
104
|
-
cat >> "$CONTEXT_FILE" 2>/dev/null << EOF
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
$MARKER at $TIMESTAMP]
|
|
108
|
-
Recent decisions (tail):
|
|
109
|
-
$LAST_DECISIONS
|
|
110
|
-
|
|
111
|
-
Open risks:
|
|
112
|
-
$OPEN_RISKS
|
|
113
|
-
---
|
|
114
|
-
EOF
|
|
115
|
-
|
|
116
89
|
exit 0
|
package/hooks/session-start
CHANGED
|
@@ -68,6 +68,7 @@ fi
|
|
|
68
68
|
|
|
69
69
|
# 5) Optional in-repo .fde (customer-approved only)
|
|
70
70
|
if [ -z "$CONTEXT_FILE" ] && [ -f ".fde/context.md" ]; then
|
|
71
|
+
ENG_DIR=".fde"
|
|
71
72
|
CONTEXT_FILE=".fde/context.md"
|
|
72
73
|
fi
|
|
73
74
|
|
|
@@ -164,9 +165,9 @@ if [ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ]; then
|
|
|
164
165
|
FDE_CMD=$(resolve_fde || true)
|
|
165
166
|
if [ -n "$FDE_CMD" ]; then
|
|
166
167
|
if [ "$FDE_CMD" = "fde" ]; then
|
|
167
|
-
TRIAGE=$(fde triage 2>/dev/null || true)
|
|
168
|
+
TRIAGE=$(FDEOPS_ENGAGEMENT="$ENG_DIR" fde triage 2>/dev/null || true)
|
|
168
169
|
else
|
|
169
|
-
TRIAGE=$(node "$FDE_CMD" triage 2>/dev/null || true)
|
|
170
|
+
TRIAGE=$(FDEOPS_ENGAGEMENT="$ENG_DIR" node "$FDE_CMD" triage 2>/dev/null || true)
|
|
170
171
|
fi
|
|
171
172
|
if [ -n "$TRIAGE" ]; then
|
|
172
173
|
CONTENT="$CONTENT---\n$TRIAGE\n\n"
|
package/hooks/session-stop
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
# fdeops SessionEnd - write-side memory backstop.
|
|
3
3
|
#
|
|
4
4
|
# The skill's memory contract says the agent appends a meaningful
|
|
5
|
-
# "where we left off" to context.md before the session ends. This hook
|
|
6
|
-
#
|
|
7
|
-
# and which engagement artifacts moved. Next session-start loads it back.
|
|
5
|
+
# "where we left off" to context.md before the session ends. This hook delegates
|
|
6
|
+
# the deterministic floor to the CLI, then refreshes the local dashboard.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
8
|
+
# No model calls, no network, exits silently.
|
|
10
9
|
|
|
11
10
|
cat >/dev/null 2>&1 || true # drain hook stdin; payload unused
|
|
12
11
|
|
|
@@ -44,6 +43,35 @@ registry_engagement_dir() {
|
|
|
44
43
|
return 1
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
# Prefer PATH `fde`, then plugin/repo copies - same order as session-start.
|
|
47
|
+
resolve_fde() {
|
|
48
|
+
if command -v fde >/dev/null 2>&1; then
|
|
49
|
+
printf '%s\n' "fde"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
for candidate in \
|
|
53
|
+
"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/bin/fde.js}" \
|
|
54
|
+
"$(dirname "$0")/../bin/fde.js" \
|
|
55
|
+
"$HOME/.claude/fdeops/fde.js" \
|
|
56
|
+
"$HOME/.claude/plugins/fdeops/bin/fde.js"; do
|
|
57
|
+
if [ -n "$candidate" ] && [ -f "$candidate" ]; then
|
|
58
|
+
printf '%s\n' "$candidate"
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
done
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
run_fde() {
|
|
66
|
+
local cmd="$1"
|
|
67
|
+
shift
|
|
68
|
+
if [ "$cmd" = "fde" ]; then
|
|
69
|
+
FDEOPS_ENGAGEMENT="$ENG_DIR" fde "$@" >/dev/null 2>&1 || true
|
|
70
|
+
else
|
|
71
|
+
FDEOPS_ENGAGEMENT="$ENG_DIR" node "$cmd" "$@" >/dev/null 2>&1 || true
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
ENG_DIR=""
|
|
48
76
|
|
|
49
77
|
# 1) Environment variable (any agent)
|
|
@@ -72,45 +100,11 @@ if [ -z "$ENG_DIR" ] && [ -d ".fde" ]; then
|
|
|
72
100
|
fi
|
|
73
101
|
|
|
74
102
|
[ -z "$ENG_DIR" ] && exit 0
|
|
75
|
-
CONTEXT_FILE="$ENG_DIR/context.md"
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
BRANCH=$(git -C "$PWD" branch --show-current 2>/dev/null)
|
|
82
|
-
LAST_COMMIT=$(git -C "$PWD" log -1 --format="%h %s" 2>/dev/null | head -c 100)
|
|
83
|
-
CHANGED=$(git -C "$PWD" status --porcelain 2>/dev/null | head -8 | sed 's/^...//' | tr '\n' ' ')
|
|
84
|
-
|
|
85
|
-
# Engagement artifacts updated in the last 12h (the deliverable=memory trail)
|
|
86
|
-
UPDATED=$(find "$ENG_DIR" -maxdepth 1 -name "*.md" ! -name "context.md" -mmin -720 2>/dev/null \
|
|
87
|
-
| while read -r f; do basename "$f"; done | tr '\n' ' ')
|
|
88
|
-
|
|
89
|
-
# Idle session (no edits, no artifact updates) → leave the memory clean.
|
|
90
|
-
# LAST_COMMIT alone is not "movement": it exists in any repo with history.
|
|
91
|
-
[ -z "$CHANGED" ] && [ -z "$UPDATED" ] && exit 0
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
printf '\n<!-- fdeops auto-capture -->\n'
|
|
95
|
-
printf '## Session end - %s %s\n' "$TODAY" "$NOW"
|
|
96
|
-
[ -n "$BRANCH" ] && printf -- '- workspace: `%s` @ %s\n' "$BRANCH" "${LAST_COMMIT:-no commits yet}"
|
|
97
|
-
[ -n "$CHANGED" ] && printf -- '- uncommitted: %s\n' "$CHANGED"
|
|
98
|
-
[ -n "$UPDATED" ] && printf -- '- engagement files updated: %s\n' "$UPDATED"
|
|
99
|
-
} 2>/dev/null >> "$CONTEXT_FILE" || true
|
|
100
|
-
|
|
101
|
-
# Refresh the local fieldbook.html so the portfolio view is current next time
|
|
102
|
-
# it's opened. Deterministic render of .fde/ - zero tokens, best-effort, never
|
|
103
|
-
# allowed to break the session.
|
|
104
|
-
if command -v node >/dev/null 2>&1; then
|
|
105
|
-
for FDE_CLI in \
|
|
106
|
-
"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/bin/fde.js}" \
|
|
107
|
-
"$(dirname "$0")/../bin/fde.js" \
|
|
108
|
-
"$HOME/.claude/fdeops/fde.js"; do
|
|
109
|
-
if [ -n "$FDE_CLI" ] && [ -f "$FDE_CLI" ]; then
|
|
110
|
-
node "$FDE_CLI" dashboard >/dev/null 2>&1 || true
|
|
111
|
-
break
|
|
112
|
-
fi
|
|
113
|
-
done
|
|
104
|
+
FDE_CMD=$(resolve_fde || true)
|
|
105
|
+
if [ -n "$FDE_CMD" ]; then
|
|
106
|
+
run_fde "$FDE_CMD" capture
|
|
107
|
+
run_fde "$FDE_CMD" dashboard
|
|
114
108
|
fi
|
|
115
109
|
|
|
116
110
|
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.12",
|
|
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
|
@@ -59,7 +59,7 @@ When NOT to interrogate or challenge: unambiguous one-liners, mechanical ops, FD
|
|
|
59
59
|
- The `fde` CLI is **local only** - `git` + file reads, no AI, no network. Safe in any environment.
|
|
60
60
|
- **You (the AI) only ever see customer code when the FDE points you at it** inside the agent they are already authorized to run. fdeops adds no new data path.
|
|
61
61
|
- **Before reading or generating against customer code, the AI policy must be known.** New engagement, policy unknown → ask it (land phase: "policy on AI-generated code? data that must never touch AI?") *before* loading their code into context. Default to "not permitted" until the FDE confirms.
|
|
62
|
-
- Data tagged `<private>`
|
|
62
|
+
- Data tagged `<private>` (sacred data, PHI, cardholder, classified) is **redacted from CLI, dashboard, and hook-injected context**. Do **not** open raw `<private>` blocks with file tools (that bypasses redaction) or paste them into prompts/subagents - work around them, never with them.
|
|
63
63
|
- 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.
|
|
64
64
|
|
|
65
65
|
**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.
|
|
@@ -10,7 +10,7 @@ A PHI breach can end an operating licence; an audit-trail gap can invalidate a c
|
|
|
10
10
|
|
|
11
11
|
> "Walk me through what patient data this system touches, stores, transmits - and who has access."
|
|
12
12
|
|
|
13
|
-
PHI is broader than names: IPs, device identifiers, sub-state geography, and dates (other than year) can all qualify in context. When in doubt, treat as PHI. Tag
|
|
13
|
+
PHI is broader than names: IPs, device identifiers, sub-state geography, and dates (other than year) can all qualify in context. When in doubt, treat as PHI. Tag it in `trust-profile.md` under `<private>`. Operational rule: **no PHI in prompts, test fixtures, or logs** - CLI/dashboard redact the tags; agent file tools do not.
|
|
14
14
|
|
|
15
15
|
## AI policy before a single line
|
|
16
16
|
|
|
@@ -38,7 +38,7 @@ Clinical systems need emergency override (a clinician must reach a record even w
|
|
|
38
38
|
|
|
39
39
|
## Principles
|
|
40
40
|
|
|
41
|
-
- PHI
|
|
41
|
+
- No PHI in prompts, fixtures, or logs (tag `<private>`; do not read raw private blocks into the model).
|
|
42
42
|
- Audit trails before features.
|
|
43
43
|
- Unclear AI policy = prohibited until confirmed.
|
|
44
44
|
- De-identification is a legal definition (18 identifiers).
|
|
@@ -86,7 +86,7 @@ Before the end of day 1, ship one visible thing: a small bug fix, a cleanup the
|
|
|
86
86
|
```
|
|
87
87
|
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.
|
|
88
88
|
|
|
89
|
-
**`trust-profile.md`** - sacred data (`<private>` tagged), fears heard, AI policy, approval chain. Sensitive:
|
|
89
|
+
**`trust-profile.md`** - sacred data (`<private>` tagged), fears heard, AI policy, approval chain. Sensitive: skip for status reads; use CLI/redacted surfaces; never paste raw `<private>` into prompts or subagents.
|
|
90
90
|
|
|
91
91
|
**`assumptions.md`** - seed every unverified claim from the brief (and the day-1 hypothesis) as rows with blast radius CRITICAL / LOAD-BEARING / CONVENIENCE and status `OPEN`. Do not wait for assumption-audit - land makes the register exist. Example:
|
|
92
92
|
|
|
@@ -107,7 +107,7 @@ If remote: trust-building takes ~40% longer - push for a short video call before
|
|
|
107
107
|
## Principles
|
|
108
108
|
|
|
109
109
|
- Never start technical work before `success.md` exists.
|
|
110
|
-
- Sacred data
|
|
110
|
+
- Sacred data tagged `<private>` stays out of model context: use CLI/redacted reads; never paste raw private blocks.
|
|
111
111
|
- The brief is a hypothesis; discover confirms it. Seed `assumptions.md` on day one.
|
|
112
112
|
- The passed-over internal team is the best source of truth, not an obstacle.
|
|
113
113
|
- If the customer cannot define success, that is the first problem to solve.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Trust profile
|
|
2
2
|
|
|
3
|
-
<!-- Sacred data, AI policy, approvals. Use <private>...</private> for
|
|
3
|
+
<!-- Sacred data, AI policy, approvals. Use <private>...</private> for content CLI/dashboard must redact; do not paste raw into prompts. -->
|
|
4
4
|
|
|
5
5
|
**AI code policy:**
|
|
6
6
|
**Approval chain:**
|
|
7
|
-
**Data
|
|
7
|
+
**Data kept out of model context (tag below; do not paste raw):**
|
|
8
8
|
|
|
9
9
|
<private>
|
|
10
10
|
<!-- Example: cardholder data scope, production credentials paths -->
|