fdeops 3.5.6 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -113
- 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 +13 -1
- package/bin/fde.js +234 -46
- package/bin/install.js +6 -0
- package/hooks/pre-compact +19 -0
- package/hooks/session-start +24 -3
- package/hooks/session-stop +23 -3
- package/package.json +1 -1
- package/skills/fde/SKILL.md +10 -9
- package/README.md.bak +0 -227
package/bin/fde.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* fde resume --full same, but the complete context.md (no bound)
|
|
14
14
|
* fde resume --init <n> create + bind an engagement for this workspace
|
|
15
15
|
* fde log <type> <text> structured append (decision|risk|delivery|contact)
|
|
16
|
+
* fde debrief [file] meeting notes → structured memory (stdin if no file)
|
|
16
17
|
* fde receipts <term> "what did we agree?" - search memory with dates
|
|
17
18
|
* fde capture session-end snapshot → context.md (hooks use this)
|
|
18
19
|
* fde status portfolio across ~/fde-engagements (red/amber/green)
|
|
@@ -28,6 +29,8 @@ const ENGAGEMENTS_ROOT = path.join(HOME, 'fde-engagements')
|
|
|
28
29
|
const REGISTRY = path.join(ENGAGEMENTS_ROOT, '.registry')
|
|
29
30
|
const CODE_EXT = ['.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.go', '.rb', '.cs', '.php']
|
|
30
31
|
const CONF_EXT = CODE_EXT.concat(['.env', '.yaml', '.yml', '.json'])
|
|
32
|
+
// one routing table for structured appends - cmdLog and cmdDebrief share it
|
|
33
|
+
const LOG_FILES = { decision: 'decisions.md', risk: 'risks.md', delivery: 'delivery.md', contact: 'stakeholders.md' }
|
|
31
34
|
|
|
32
35
|
// constant-command runner - never receives user input
|
|
33
36
|
function sh(cmd, cwd) {
|
|
@@ -143,12 +146,34 @@ function sectionBody(md, heading) {
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
// phase / trust / top risk / freshness - identical heuristic for status + dashboard.
|
|
149
|
+
// Trust resolution: structured [signal:red|amber|green] tokens in stakeholders.md
|
|
150
|
+
// (written by `fde log contact --signal` and `fde debrief`) win - the latest dated
|
|
151
|
+
// one. Older than 21 days → stale: shown with a "?" marker + age so a forgotten
|
|
152
|
+
// signal never silently drives triage. The keyword grep survives only as the
|
|
153
|
+
// zero-effort floor when NO token exists anywhere - prose like "escalated to CTO,
|
|
154
|
+
// resolved amicably" must not flip a client amber forever.
|
|
146
155
|
function computeSignals(eng) {
|
|
147
156
|
const ctx = readEng(eng, 'context.md'); const stake = readEng(eng, 'stakeholders.md'); const risks = readEng(eng, 'risks.md')
|
|
148
157
|
const phase = (ctx.match(/phase[:* ]+\**([a-z-]+)/i) || [])[1] || '?'
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
158
|
+
let latest = null
|
|
159
|
+
for (const l of stake.split('\n')) {
|
|
160
|
+
const sm = l.match(/\[signal:(red|amber|green)\]/i)
|
|
161
|
+
if (!sm) continue
|
|
162
|
+
const date = (l.match(/\[(\d{4}-\d{2}-\d{2})\]/) || [])[1] || ''
|
|
163
|
+
if (!latest || date >= latest.date) latest = { date, sig: sm[1].toLowerCase() }
|
|
164
|
+
}
|
|
165
|
+
let trust, signalAge = null, stale = false
|
|
166
|
+
if (latest) {
|
|
167
|
+
trust = latest.sig === 'red' ? 'RED' : latest.sig
|
|
168
|
+
if (latest.date) {
|
|
169
|
+
signalAge = Math.max(0, Math.floor((Date.now() - Date.parse(latest.date)) / 86400000))
|
|
170
|
+
stale = signalAge > 21
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
const sLines = stake.split('\n').filter(l => !(/green/i.test(l) && /red|amber/i.test(l)))
|
|
174
|
+
trust = sLines.some(l => /\bred\b/i.test(l)) ? 'RED'
|
|
175
|
+
: sLines.some(l => /amber|gone quiet|routing around|escalat/i.test(l)) ? 'amber' : 'green'
|
|
176
|
+
}
|
|
152
177
|
const topRisk = (risks.split('\n').find(l => {
|
|
153
178
|
const t = l.trim()
|
|
154
179
|
return /^[-|]/.test(t) && t.length > 20 && !/^\|?[-\s|]+$/.test(t) &&
|
|
@@ -159,7 +184,7 @@ function computeSignals(eng) {
|
|
|
159
184
|
ageDays = Math.floor((Date.now() - fs.statSync(path.join(eng, 'context.md')).mtimeMs) / 86400000)
|
|
160
185
|
updated = ageDays === 0 ? 'today' : `${ageDays}d ago`
|
|
161
186
|
} catch (_) {}
|
|
162
|
-
return { phase, trust, topRisk, updated, ageDays }
|
|
187
|
+
return { phase, trust, signalAge, stale, topRisk, updated, ageDays }
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
// ---------- commands ----------
|
|
@@ -183,6 +208,7 @@ function cmdScan() {
|
|
|
183
208
|
|
|
184
209
|
// churn × tests = the load-bearing walls
|
|
185
210
|
out.push('\nHOTSPOTS (churn 90d × test coverage) - handle with care:')
|
|
211
|
+
let firstUntested = ''
|
|
186
212
|
if (isGit) {
|
|
187
213
|
const churn = sh("git log --since='90 days ago' --name-only --pretty=format:") || ''
|
|
188
214
|
const counts = {}
|
|
@@ -193,6 +219,7 @@ function cmdScan() {
|
|
|
193
219
|
for (const [f, n] of top) {
|
|
194
220
|
const base = path.basename(f).replace(/\.[^.]+$/, '')
|
|
195
221
|
const tested = testFiles.some(t => t.includes(base))
|
|
222
|
+
if (!tested && !firstUntested) firstUntested = f
|
|
196
223
|
out.push(` ${String(n).padStart(3)} commits/90d ${f} ${tested ? '' : '⚠ NO TEST NEIGHBOR'}`)
|
|
197
224
|
}
|
|
198
225
|
} else out.push(' (not a git repo - churn unavailable)')
|
|
@@ -205,7 +232,10 @@ function cmdScan() {
|
|
|
205
232
|
|
|
206
233
|
// AI components - they fail silently
|
|
207
234
|
out.push('\nAI COMPONENTS (no exception fires when these drift):')
|
|
208
|
-
|
|
235
|
+
// NOTE: bare "inference" is banned from this regex - TypeScript codebases are
|
|
236
|
+
// full of "type inference" comments and the false positives poison the day-1
|
|
237
|
+
// questions. Model inference only, in explicit forms.
|
|
238
|
+
const ai = grepFiles(codeFiles, /openai|anthropic|\bllm\b|gpt-|claude|embedding|vector store|model inference|inference (?:api|endpoint|server|engine)/i, 10)
|
|
209
239
|
ai.length ? ai.forEach(h => out.push(` ${h.file}:${h.line} ${h.text}`)) : out.push(' none found')
|
|
210
240
|
|
|
211
241
|
// secrets (redacted)
|
|
@@ -216,6 +246,7 @@ function cmdScan() {
|
|
|
216
246
|
sec.length
|
|
217
247
|
? sec.forEach(h => out.push(` ${h.file}:${h.line} ${h.text.replace(/(['"])([^'"]{4})[^'"]+(['"])/, '$1$2…REDACTED$3')}`))
|
|
218
248
|
: out.push(' none found')
|
|
249
|
+
out.push(' (grep-grade check - run gitleaks or trufflehog for real secret coverage)')
|
|
219
250
|
|
|
220
251
|
// previous attempts - the political archaeology
|
|
221
252
|
out.push('\nPREVIOUS ATTEMPTS (ask who ran these, and what happened):')
|
|
@@ -229,6 +260,18 @@ function cmdScan() {
|
|
|
229
260
|
const testCount = files.filter(f => /test|spec/i.test(f)).length
|
|
230
261
|
out.push(`\nTEST LANDSCAPE ${testCount} test file(s) across ${codeFiles.length} code files`)
|
|
231
262
|
|
|
263
|
+
// day-1 questions - each one earned by a finding above, skipped when empty
|
|
264
|
+
out.push('\nASK ON DAY 1:')
|
|
265
|
+
const asks = []
|
|
266
|
+
if (reverts || readmeHits.length) asks.push('Who ran the previous attempt(s), and what happened to them?')
|
|
267
|
+
if (firstUntested) asks.push(`What breaks when ${firstUntested} changes, and who owns it?`)
|
|
268
|
+
if (ai.length) asks.push(`How would anyone notice if ${ai[0].file}'s model output drifted?`)
|
|
269
|
+
if (sec.length) asks.push('What is the secret-rotation story?')
|
|
270
|
+
if (tmp.length) asks.push("Which of these 'temporary' fixes are now load-bearing contracts?")
|
|
271
|
+
asks.length
|
|
272
|
+
? asks.slice(0, 5).forEach((q, i) => out.push(` ${i + 1}. ${q}`))
|
|
273
|
+
: out.push(' (clean scan - ask what the last engineer wished they had known)')
|
|
274
|
+
|
|
232
275
|
out.push('\n' + '-'.repeat(60))
|
|
233
276
|
out.push("Facts only - interpretation is the FDE's (or @fde's) job.")
|
|
234
277
|
console.log(out.join('\n'))
|
|
@@ -250,11 +293,30 @@ function cmdResume(args) {
|
|
|
250
293
|
else if (!fs.existsSync(dst)) fs.copyFileSync(src, dst)
|
|
251
294
|
}
|
|
252
295
|
fs.mkdirSync(path.join(fdeDir, 'retrospectives'), { recursive: true })
|
|
253
|
-
// bind THIS workspace to the engagement (zero ceremony next time)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
296
|
+
// bind THIS workspace to the engagement (zero ceremony next time).
|
|
297
|
+
// A workspace binds to exactly ONE engagement: rebinding REPLACES the old
|
|
298
|
+
// line - resolution is first-match-wins, so appending a second line would
|
|
299
|
+
// leave the stale binding winning and silently write to the wrong client.
|
|
300
|
+
const cwd = process.cwd()
|
|
301
|
+
const prev = readRegistry().find(r => r.workspace === cwd)
|
|
302
|
+
const kept = readRegistry().filter(r => r.workspace !== cwd).map(r => `${r.workspace} ${r.slug}`)
|
|
303
|
+
kept.push(`${cwd} ${slug}`)
|
|
304
|
+
fs.writeFileSync(REGISTRY, kept.join('\n') + '\n')
|
|
305
|
+
console.log(`ENGAGEMENT READY: ${fdeDir}\nbound to workspace: ${cwd}`)
|
|
306
|
+
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}"`)
|
|
307
|
+
// NDA surface: engagement notes must not silently leave the machine via file sync
|
|
308
|
+
const syncHit = /icloud|mobile documents|dropbox|onedrive|google drive|box sync/i.exec(ENGAGEMENTS_ROOT)
|
|
309
|
+
if (syncHit) console.log(`⚠ engagements root is inside a synced folder ("${syncHit[0]}") - client notes will leave this machine via sync. See PRIVACY.md.`)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (args[0] === '--bind') {
|
|
313
|
+
// inspection: what does THIS workspace resolve to, and why
|
|
314
|
+
const cwd = process.cwd()
|
|
315
|
+
const reg = readRegistry().find(r => r.workspace === cwd)
|
|
316
|
+
const eng = resolveEngagement()
|
|
317
|
+
console.log(`workspace: ${cwd}`)
|
|
318
|
+
console.log(`registry: ${reg ? `${reg.slug} (${path.join(ENGAGEMENTS_ROOT, reg.slug, '.fde')})` : '(not bound)'}`)
|
|
319
|
+
console.log(`resolves: ${eng || '(nothing - run: fde resume --init <client>)'}`)
|
|
258
320
|
return
|
|
259
321
|
}
|
|
260
322
|
const eng = resolveEngagement()
|
|
@@ -296,14 +358,77 @@ function resumeView(md) {
|
|
|
296
358
|
}
|
|
297
359
|
|
|
298
360
|
function cmdLog(args) {
|
|
299
|
-
|
|
361
|
+
args = args.slice()
|
|
362
|
+
// --signal red|amber|green (contact only) → structured token computeSignals trusts
|
|
363
|
+
let signal = ''
|
|
364
|
+
const sigIdx = args.indexOf('--signal')
|
|
365
|
+
if (sigIdx !== -1) {
|
|
366
|
+
signal = (args[sigIdx + 1] || '').toLowerCase()
|
|
367
|
+
if (!['red', 'amber', 'green'].includes(signal)) { console.error('usage: fde log contact <text> --signal red|amber|green'); process.exit(1) }
|
|
368
|
+
args.splice(sigIdx, 2)
|
|
369
|
+
}
|
|
300
370
|
const type = args[0]; const text = args.slice(1).join(' ')
|
|
301
|
-
if (!
|
|
371
|
+
if (!LOG_FILES[type] || !text) { console.error('usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green]'); process.exit(1) }
|
|
372
|
+
if (signal && type !== 'contact') { console.error('--signal only applies to: fde log contact'); process.exit(1) }
|
|
302
373
|
const eng = resolveEngagement()
|
|
303
374
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
304
375
|
const date = new Date().toISOString().slice(0, 10)
|
|
305
|
-
fs.appendFileSync(path.join(eng,
|
|
306
|
-
console.log(`logged → ${
|
|
376
|
+
fs.appendFileSync(path.join(eng, LOG_FILES[type]), `\n- [${date}] ${signal ? `[signal:${signal}] ` : ''}${text}\n`)
|
|
377
|
+
console.log(`logged → ${LOG_FILES[type]}${signal ? ` (signal:${signal})` : ''}`)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Meeting notes → structured memory. Deterministic routing, zero AI: lines that
|
|
381
|
+
// start with decision:/risk:/delivery:/contact: (case-insensitive) go to their
|
|
382
|
+
// LOG_FILES target as dated bullets; everything else lands in context.md as one
|
|
383
|
+
// dated debrief block. contact: lines may carry an inline [signal:x] token
|
|
384
|
+
// anywhere in the text - preserved verbatim so computeSignals can trust it.
|
|
385
|
+
// Real notes arrive as markdown: "- decision: ...", "* contact: ...",
|
|
386
|
+
// "**Decision:** ..." - strip bullet/bold dressing before matching, or the
|
|
387
|
+
// prefix silently misses and a [signal:x] token lands in context.md, which
|
|
388
|
+
// signal parsing never reads. Silent loss is the one failure a memory tool
|
|
389
|
+
// cannot have. --dry-run prints the routing without writing anything.
|
|
390
|
+
function cmdDebrief(args) {
|
|
391
|
+
args = args.slice()
|
|
392
|
+
const dryIdx = args.indexOf('--dry-run')
|
|
393
|
+
const dry = dryIdx !== -1
|
|
394
|
+
if (dry) args.splice(dryIdx, 1)
|
|
395
|
+
const eng = resolveEngagement()
|
|
396
|
+
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
397
|
+
let input = ''
|
|
398
|
+
if (args[0]) {
|
|
399
|
+
try { input = fs.readFileSync(args[0].replace(/^~/, HOME), 'utf8') }
|
|
400
|
+
catch (_) { console.error(`cannot read ${args[0]}`); process.exit(1) }
|
|
401
|
+
} else {
|
|
402
|
+
try { input = fs.readFileSync(0, 'utf8') } catch (_) {} // stdin until EOF
|
|
403
|
+
}
|
|
404
|
+
const d = new Date()
|
|
405
|
+
const date = d.toISOString().slice(0, 10)
|
|
406
|
+
const counts = { decision: 0, risk: 0, delivery: 0, contact: 0 }
|
|
407
|
+
const ctxLines = []
|
|
408
|
+
for (const raw of input.split('\n')) {
|
|
409
|
+
let line = raw.trim()
|
|
410
|
+
if (!line) continue
|
|
411
|
+
// markdown dressing: leading bullets (-, *, +) and bold around the prefix
|
|
412
|
+
const bare = line.replace(/^[-*+]\s+/, '').replace(/^\*\*(decision|risk|delivery|contact):?\*\*:?\s*/i, '$1: ')
|
|
413
|
+
const m = bare.match(/^(decision|risk|delivery|contact):\s*(.+)$/i)
|
|
414
|
+
if (m) {
|
|
415
|
+
const type = m[1].toLowerCase()
|
|
416
|
+
if (dry) console.log(`→ ${LOG_FILES[type]} - [${date}] ${m[2]}`)
|
|
417
|
+
else fs.appendFileSync(path.join(eng, LOG_FILES[type]), `\n- [${date}] ${m[2]}\n`)
|
|
418
|
+
counts[type]++
|
|
419
|
+
} else ctxLines.push(line)
|
|
420
|
+
}
|
|
421
|
+
if (ctxLines.length) {
|
|
422
|
+
const stamp = `${date} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
423
|
+
if (dry) ctxLines.forEach(l => console.log(`→ context.md - ${l}`))
|
|
424
|
+
else fs.appendFileSync(path.join(eng, 'context.md'), `\n## Debrief - ${stamp}\n${ctxLines.map(l => `- ${l}`).join('\n')}\n`)
|
|
425
|
+
}
|
|
426
|
+
const plural = { decision: 'decisions', risk: 'risks', delivery: 'deliveries', contact: 'contacts' }
|
|
427
|
+
const parts = Object.keys(counts).filter(t => counts[t])
|
|
428
|
+
.map(t => `${counts[t]} ${counts[t] === 1 ? t : plural[t]}`)
|
|
429
|
+
if (ctxLines.length) parts.push(`${ctxLines.length} context line${ctxLines.length === 1 ? '' : 's'}`)
|
|
430
|
+
const verb = dry ? 'debrief would route' : 'debrief routed'
|
|
431
|
+
console.log(parts.length ? `${verb} → ${parts.join(', ')}` : 'debrief empty - nothing routed')
|
|
307
432
|
}
|
|
308
433
|
|
|
309
434
|
function cmdReceipts(args) {
|
|
@@ -320,7 +445,7 @@ function cmdReceipts(args) {
|
|
|
320
445
|
if (rx.test(l)) { console.log(`${f}:${i + 1} ${l.trim().slice(0, 160)}`); found++ }
|
|
321
446
|
})
|
|
322
447
|
}
|
|
323
|
-
if (!found) console.log(`no record of "${term}" -
|
|
448
|
+
if (!found) console.log(`no record of "${term}" - nothing was ever logged about it. A gap in the record, not proof of absence: if it WAS agreed, log it now, dated today.`)
|
|
324
449
|
}
|
|
325
450
|
|
|
326
451
|
function cmdCapture() {
|
|
@@ -353,16 +478,19 @@ function cmdStatus() {
|
|
|
353
478
|
const eng = path.join(ENGAGEMENTS_ROOT, d, '.fde')
|
|
354
479
|
if (!fs.existsSync(eng)) continue
|
|
355
480
|
const s = computeSignals(eng)
|
|
356
|
-
rows.push({ name: d, phase: s.phase, trust: s.trust, updated: s.updated, topRisk: s.topRisk.slice(0, 60) })
|
|
481
|
+
rows.push({ name: d, phase: s.phase, trust: s.trust, signalAge: s.signalAge, stale: s.stale, updated: s.updated, topRisk: s.topRisk.slice(0, 60) })
|
|
357
482
|
}
|
|
358
483
|
if (!rows.length) { console.log('no engagements yet'); return }
|
|
359
484
|
const order = { RED: 0, amber: 1, green: 2 }
|
|
360
485
|
rows.sort((a, b) => order[a.trust] - order[b.trust])
|
|
361
486
|
console.log('FDE PORTFOLIO - trust-first triage (heuristic: red > amber > green)\n')
|
|
362
487
|
for (const r of rows) {
|
|
363
|
-
|
|
488
|
+
// "amber?" = structured signal went stale (>21d) - reconfirm before trusting it
|
|
489
|
+
const label = r.trust + (r.stale ? '?' : '')
|
|
490
|
+
const sig = r.signalAge != null ? `signal ${r.signalAge}d old${r.stale ? ' (STALE - reconfirm)' : ''} ` : ''
|
|
491
|
+
console.log(` [${label.padEnd(6)}] ${r.name.padEnd(24)} phase:${r.phase.padEnd(10)} updated:${r.updated.padEnd(8)} ${sig}${r.topRisk}`)
|
|
364
492
|
}
|
|
365
|
-
console.log('\
|
|
493
|
+
console.log('\ntrust: latest [signal:x] token in stakeholders.md wins (fde log contact --signal, fde debrief); keyword heuristic only when none exists - verify before acting.')
|
|
366
494
|
}
|
|
367
495
|
|
|
368
496
|
// ---------- dashboard (deterministic markdown → one local HTML) ----------
|
|
@@ -475,12 +603,21 @@ const DASH_SECTIONS = [
|
|
|
475
603
|
function dashStyles() {
|
|
476
604
|
return [
|
|
477
605
|
// design system: quiet chrome, high density, daily-use second brain
|
|
606
|
+
// light (default) + dark (opt-in via [data-fde-theme="dark"], set pre-paint - see cmdDashboard head script)
|
|
478
607
|
':root{--bg:#f9fafb;--card:#fff;--ink:#111827;--2:#374151;--3:#4b5563;--muted:#6b7280;--line:#f3f4f6;--line2:#e5e7eb;',
|
|
479
608
|
'--green:#10b981;--amber:#f59e0b;--red:#ef4444;--accent:#6366f1;',
|
|
609
|
+
'--red-bg:#fef2f2;--red-border:#fecaca;--red-ink:#7f1d1d;--red-ink2:#991b1b;',
|
|
610
|
+
'--amber-bg:#fffbeb;--amber-border:#fde68a;--amber-ink:#78350f;--amber-ink2:#92400e;',
|
|
480
611
|
'--shadow-s:0 1px 2px rgba(0,0,0,.04);--shadow:0 1px 3px rgba(0,0,0,.06);--shadow-l:0 4px 12px rgba(0,0,0,.08);',
|
|
481
612
|
'--r:8px}',
|
|
613
|
+
'html[data-fde-theme="dark"]{--bg:#0b0d12;--card:#161a22;--ink:#e5e7eb;--2:#cbd5e1;--3:#9ca3af;--muted:#7b8494;--line:#1f2430;--line2:#262c3a;',
|
|
614
|
+
'--green:#34d399;--amber:#fbbf24;--red:#f87171;--accent:#818cf8;',
|
|
615
|
+
'--red-bg:#2a1517;--red-border:#5b2328;--red-ink:#fecaca;--red-ink2:#fca5a5;',
|
|
616
|
+
'--amber-bg:#2a2210;--amber-border:#5b4a1f;--amber-ink:#fde68a;--amber-ink2:#fcd34d;',
|
|
617
|
+
'--shadow-s:0 1px 2px rgba(0,0,0,.3);--shadow:0 1px 3px rgba(0,0,0,.35);--shadow-l:0 4px 16px rgba(0,0,0,.45)}',
|
|
482
618
|
'*{box-sizing:border-box;margin:0}',
|
|
483
|
-
'body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased}',
|
|
619
|
+
'body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased;transition:background .15s,color .15s}',
|
|
620
|
+
'.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}',
|
|
484
621
|
// header - compact, purposeful
|
|
485
622
|
'header{background:#111827;color:#fff;padding:20px 32px}',
|
|
486
623
|
'header .inner{max-width:960px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}',
|
|
@@ -490,27 +627,31 @@ function dashStyles() {
|
|
|
490
627
|
'.stats{display:flex;align-items:center;gap:16px}',
|
|
491
628
|
'.stat{font-size:12px;color:#9ca3af;display:flex;align-items:center;gap:5px}',
|
|
492
629
|
'.stat b{color:#e5e7eb;font-weight:600}',
|
|
493
|
-
'.stat .sdot{width:6px;height:6px;border-radius:50%;flex-shrink:0}',
|
|
494
|
-
'.stat .sdot.green{background:#34d399}.stat .sdot.amber{background:#fbbf24}.stat .sdot.red{background
|
|
630
|
+
'.stat .sdot{width:6px;height:6px;border-radius:50%;flex-shrink:0;box-sizing:border-box}',
|
|
631
|
+
'.stat .sdot.green{background:#34d399}.stat .sdot.amber{background:#fbbf24;border-radius:1.5px}.stat .sdot.red{background:transparent;border:1.5px solid #f87171}',
|
|
632
|
+
'.theme-toggle{border:1px solid #374151;background:transparent;color:#d1d5db;font-size:11px;padding:5px 10px;border-radius:6px;cursor:pointer;font-family:inherit;line-height:1.4}',
|
|
633
|
+
'.theme-toggle:hover{border-color:#6b7280;color:#fff}',
|
|
495
634
|
// layout
|
|
496
635
|
'.wrap{max-width:960px;margin:0 auto;padding:24px 32px 64px}',
|
|
497
636
|
// search - integrated, not floating
|
|
498
|
-
'.search-wrap{position:relative;margin-bottom:
|
|
637
|
+
'.search-wrap{position:relative;margin-bottom:8px}',
|
|
499
638
|
'.search-wrap svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;stroke:var(--muted);stroke-width:2;fill:none}',
|
|
500
|
-
'.search{width:100%;padding:10px 14px 10px 36px;font-size:13px;border:1px solid var(--line2);border-radius:var(--r);background:var(--card);transition:border-color .15s,box-shadow .15s;outline:none}',
|
|
501
|
-
'.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,102,241,.
|
|
639
|
+
'.search{width:100%;padding:10px 14px 10px 36px;font-size:13px;border:1px solid var(--line2);border-radius:var(--r);background:var(--card);color:var(--ink);transition:border-color .15s,box-shadow .15s;outline:none}',
|
|
640
|
+
'.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,102,241,.15)}',
|
|
502
641
|
'.search::placeholder{color:var(--muted)}',
|
|
642
|
+
'.search-hint{display:block;margin:6px 2px 20px;font-size:11px;color:var(--muted)}',
|
|
643
|
+
'.search-hint kbd{font-family:inherit;border:1px solid var(--line2);border-radius:4px;padding:1px 5px;font-size:10.5px;background:var(--bg)}',
|
|
503
644
|
// directive - the one line that says where to start today
|
|
504
|
-
'.directive{display:flex;align-items:center;gap:10px;margin:0 0 20px;padding:12px 16px;background
|
|
505
|
-
'.directive b{font-weight:700;color
|
|
506
|
-
'.directive.amber{background
|
|
507
|
-
'.directive.amber b{color
|
|
645
|
+
'.directive{display:flex;align-items:center;gap:10px;margin:0 0 20px;padding:12px 16px;background:var(--red-bg);border:1px solid var(--red-border);border-radius:var(--r);font-size:13.5px;line-height:1.5;color:var(--red-ink)}',
|
|
646
|
+
'.directive b{font-weight:700;color:var(--red-ink)}',
|
|
647
|
+
'.directive.amber{background:var(--amber-bg);border-color:var(--amber-border);color:var(--amber-ink)}',
|
|
648
|
+
'.directive.amber b{color:var(--amber-ink)}',
|
|
508
649
|
'.directive-dot{width:9px;height:9px;border-radius:50%;background:var(--red);flex-shrink:0;box-shadow:0 0 0 3px rgba(239,68,68,.18)}',
|
|
509
650
|
'.directive.amber .directive-dot{background:var(--amber);box-shadow:0 0 0 3px rgba(245,158,11,.18)}',
|
|
510
651
|
// attention rows - red engagements dominate the page, full width
|
|
511
652
|
'.attn-stack{display:flex;flex-direction:column;gap:10px}',
|
|
512
|
-
'.attn{background
|
|
513
|
-
'.attn:hover{border-color
|
|
653
|
+
'.attn{background:var(--red-bg);border:1px solid var(--red-border);border-left:4px solid var(--red);border-radius:var(--r);padding:16px 18px;cursor:pointer;transition:box-shadow .15s,border-color .15s}',
|
|
654
|
+
'.attn:hover{border-color:var(--red);box-shadow:var(--shadow-l)}',
|
|
514
655
|
'.attn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
515
656
|
'.attn-head h3{font-size:15px;font-weight:700;color:var(--ink)}',
|
|
516
657
|
'.attn .risk{margin-top:10px}',
|
|
@@ -520,22 +661,27 @@ function dashStyles() {
|
|
|
520
661
|
// cards - clean, scannable
|
|
521
662
|
'.card{background:var(--card);border:1px solid var(--line2);border-radius:var(--r);padding:16px 18px;cursor:pointer;transition:border-color .15s,box-shadow .15s;position:relative;overflow:hidden}',
|
|
522
663
|
'.card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px}',
|
|
523
|
-
'.card:hover{border-color
|
|
664
|
+
'.card:hover{border-color:var(--muted);box-shadow:var(--shadow-l)}',
|
|
524
665
|
'.card.green::before{background:var(--green)}.card.amber::before{background:var(--amber)}.card.red::before{background:var(--red)}',
|
|
525
666
|
'.card h3{font-size:14px;font-weight:600;margin-bottom:6px}',
|
|
526
667
|
'.row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
|
|
527
|
-
'.badge{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;padding:2px 7px;border-radius:4px;background
|
|
528
|
-
|
|
529
|
-
'.dot
|
|
668
|
+
'.badge{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;padding:2px 7px;border-radius:4px;background:var(--line);color:var(--3);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}',
|
|
669
|
+
// colorblind-safe: shape carries the signal too, color reinforces it - circle/square/ring
|
|
670
|
+
'.dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;box-sizing:border-box}',
|
|
671
|
+
'.dot.green{background:var(--green)}.dot.amber{background:var(--amber);border-radius:2px}.dot.red{background:transparent;border:2px solid var(--red)}',
|
|
530
672
|
'.trust-label{font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.3px}',
|
|
531
673
|
'.t-green{color:var(--green)}.t-amber{color:var(--amber)}.t-red{color:var(--red)}',
|
|
532
674
|
'.meta{color:var(--muted);font-size:11px}',
|
|
533
675
|
'.card .next{margin-top:10px;font-size:12.5px;line-height:1.5;color:var(--2)}.card .next b{color:var(--3);font-weight:600;font-size:10px;text-transform:uppercase;letter-spacing:.3px;margin-right:2px}',
|
|
534
|
-
'.card .risk{margin-top:8px;font-size:11.5px;color
|
|
535
|
-
'.card .risk.amber-risk{color
|
|
676
|
+
'.card .risk{margin-top:8px;font-size:11.5px;color:var(--red-ink2);padding:5px 8px;background:var(--red-bg);border-radius:4px;border-left:2px solid var(--red);line-height:1.4}',
|
|
677
|
+
'.card .risk.amber-risk{color:var(--amber-ink2);background:var(--amber-bg);border-left-color:var(--amber)}',
|
|
536
678
|
// demote placeholder cards (no next action) - quiet until they earn attention
|
|
537
679
|
'.card.muted{opacity:.55}',
|
|
538
680
|
'.card.muted:hover{opacity:1}',
|
|
681
|
+
// search match context - why this card/row matched, in the card's own words
|
|
682
|
+
'.match-snippet{margin-top:8px;font-size:11.5px;line-height:1.4;color:var(--3);background:var(--bg);border-radius:4px;padding:5px 8px}',
|
|
683
|
+
'.match-snippet:empty{display:none;padding:0;margin:0}',
|
|
684
|
+
'.match-snippet mark{background:rgba(99,102,241,.25);color:inherit;border-radius:2px;padding:0 1px}',
|
|
539
685
|
// section labels
|
|
540
686
|
'h2.section{margin:28px 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}',
|
|
541
687
|
// detail accordion - tight, content-forward
|
|
@@ -559,10 +705,10 @@ function dashStyles() {
|
|
|
559
705
|
'th{background:var(--bg);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--3);border-bottom:1px solid var(--line2)}',
|
|
560
706
|
'tr:last-child td{border-bottom:none}',
|
|
561
707
|
'tr:hover td{background:var(--bg)}',
|
|
562
|
-
'code{background
|
|
708
|
+
'code{background:var(--line);padding:1px 5px;border-radius:4px;font-size:12px;color:var(--3)}',
|
|
563
709
|
// footer
|
|
564
710
|
'footer{max-width:960px;margin:0 auto;padding:0 32px 48px;color:var(--muted);font-size:11.5px;line-height:1.5}',
|
|
565
|
-
'footer code{color:var(--3);background
|
|
711
|
+
'footer code{color:var(--3);background:var(--line)}',
|
|
566
712
|
'.hide{display:none!important}',
|
|
567
713
|
// responsive
|
|
568
714
|
'@media(max-width:640px){header{padding:16px}.header .inner{flex-direction:column;align-items:flex-start}.wrap{padding:16px 16px 48px}.grid{grid-template-columns:1fr}.search{padding-left:32px}}',
|
|
@@ -573,19 +719,47 @@ function dashScript() {
|
|
|
573
719
|
return [
|
|
574
720
|
"var q=document.getElementById('q');",
|
|
575
721
|
"function norm(s){return (s||'').toLowerCase();}",
|
|
722
|
+
"function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}",
|
|
723
|
+
// why a card matched - a short excerpt from its own text, term highlighted
|
|
724
|
+
"function snippetFor(raw,term){",
|
|
725
|
+
" var i=raw.indexOf(term);",
|
|
726
|
+
" if(i<0)return '';",
|
|
727
|
+
" var start=Math.max(0,i-28),end=Math.min(raw.length,i+term.length+46);",
|
|
728
|
+
" var pre=esc(raw.slice(start,i)),hit=esc(raw.slice(i,i+term.length)),post=esc(raw.slice(i+term.length,end));",
|
|
729
|
+
" return (start>0?'\u2026':'')+pre+'<mark>'+hit+'</mark>'+post+(end<raw.length?'\u2026':'');",
|
|
730
|
+
"}",
|
|
576
731
|
"if(q){q.addEventListener('input',function(){",
|
|
577
732
|
" var term=norm(q.value);",
|
|
578
733
|
" document.querySelectorAll('[data-search]').forEach(function(el){",
|
|
579
|
-
" var
|
|
734
|
+
" var raw=norm(el.getAttribute('data-search'));",
|
|
735
|
+
" var hit=!term||raw.indexOf(term)>-1;",
|
|
580
736
|
" el.classList.toggle('hide',!hit);",
|
|
737
|
+
" var snip=el.querySelector('.match-snippet');",
|
|
738
|
+
" if(snip)snip.innerHTML=(hit&&term)?snippetFor(raw,term):'';",
|
|
581
739
|
" });",
|
|
582
740
|
"});}",
|
|
583
|
-
"document.querySelectorAll('.card').forEach(function(c){",
|
|
741
|
+
"document.querySelectorAll('.card,.attn').forEach(function(c){",
|
|
584
742
|
" c.addEventListener('click',function(){",
|
|
585
743
|
" var d=document.getElementById(c.getAttribute('data-target'));",
|
|
586
744
|
" if(d){d.open=true;d.scrollIntoView({behavior:'smooth',block:'start'});}",
|
|
587
745
|
" });",
|
|
588
746
|
"});",
|
|
747
|
+
// '/' jumps to search from anywhere, unless already typing somewhere
|
|
748
|
+
"document.addEventListener('keydown',function(e){",
|
|
749
|
+
" var t=e.target,typing=t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable);",
|
|
750
|
+
" if(!typing&&e.key==='/'&&q){e.preventDefault();q.focus();}",
|
|
751
|
+
"});",
|
|
752
|
+
// theme toggle - local only, no network; pre-paint script in <head> avoids a flash
|
|
753
|
+
"var themeBtn=document.getElementById('fde-theme-btn');",
|
|
754
|
+
"function currentTheme(){return document.documentElement.getAttribute('data-fde-theme')==='dark'?'dark':'light';}",
|
|
755
|
+
"function setThemeLabel(){if(themeBtn)themeBtn.textContent=(currentTheme()==='dark'?'light':'dark')+' mode';}",
|
|
756
|
+
"setThemeLabel();",
|
|
757
|
+
"if(themeBtn){themeBtn.addEventListener('click',function(){",
|
|
758
|
+
" var next=currentTheme()==='dark'?'light':'dark';",
|
|
759
|
+
" try{localStorage.setItem('fde-fieldbook-theme',next);}catch(_){}",
|
|
760
|
+
" document.documentElement.setAttribute('data-fde-theme',next);",
|
|
761
|
+
" setThemeLabel();",
|
|
762
|
+
"});}",
|
|
589
763
|
].join('\n')
|
|
590
764
|
}
|
|
591
765
|
|
|
@@ -628,16 +802,21 @@ function cmdDashboard(args) {
|
|
|
628
802
|
directiveClass = reds.length ? 'directive' : 'directive amber'
|
|
629
803
|
}
|
|
630
804
|
|
|
805
|
+
// signal provenance: how old the structured trust token is, "?" when stale (>21d)
|
|
806
|
+
const signalMeta = e => e.signals.signalAge != null
|
|
807
|
+
? `<span class="meta">signal ${e.signals.signalAge}d old${e.signals.stale ? ' - reconfirm' : ''}</span>` : ''
|
|
808
|
+
|
|
631
809
|
// red engagements: full-width attention rows that dominate the page
|
|
632
810
|
const attention = reds.map(e => {
|
|
633
811
|
const id = 'eng-' + slugify(e.name)
|
|
634
812
|
return [
|
|
635
813
|
`<div class="attn" data-target="${id}" data-search="${e.searchBlob}">`,
|
|
636
|
-
`<div class="attn-head"><span class="dot red"></span><span class="trust-label t-red">RED</span>`,
|
|
814
|
+
`<div class="attn-head"><span class="dot red"></span><span class="trust-label t-red">RED${e.signals.stale ? '?' : ''}</span>`,
|
|
637
815
|
`<h3>${inlineMd(e.name)}</h3>`,
|
|
638
|
-
`<span class="badge">${inlineMd(e.signals.phase)}</span><span class="meta">updated ${e.signals.updated}</span
|
|
816
|
+
`<span class="badge">${inlineMd(e.signals.phase)}</span><span class="meta">updated ${e.signals.updated}</span>${signalMeta(e)}</div>`,
|
|
639
817
|
e.signals.topRisk ? `<div class="risk">${inlineMd(e.signals.topRisk)}</div>` : '',
|
|
640
818
|
e.next ? `<div class="next"><b>Next</b> ${inlineMd(e.next)}</div>` : '',
|
|
819
|
+
`<div class="match-snippet"></div>`,
|
|
641
820
|
`</div>`,
|
|
642
821
|
].join('')
|
|
643
822
|
}).join('\n')
|
|
@@ -650,10 +829,11 @@ function cmdDashboard(args) {
|
|
|
650
829
|
return [
|
|
651
830
|
`<div class="card ${trustClass}${muted}" data-target="${id}" data-search="${e.searchBlob}">`,
|
|
652
831
|
`<h3>${inlineMd(e.name)}</h3>`,
|
|
653
|
-
`<div class="row"><span class="dot ${trustClass}"></span><span class="trust-label t-${trustClass}">${trustClass}</span>`,
|
|
654
|
-
`<span class="badge">${inlineMd(e.signals.phase)}</span><span class="meta">updated ${e.signals.updated}</span
|
|
832
|
+
`<div class="row"><span class="dot ${trustClass}"></span><span class="trust-label t-${trustClass}">${trustClass}${e.signals.stale ? '?' : ''}</span>`,
|
|
833
|
+
`<span class="badge">${inlineMd(e.signals.phase)}</span><span class="meta">updated ${e.signals.updated}</span>${signalMeta(e)}</div>`,
|
|
655
834
|
e.next ? `<div class="next"><b>Next</b> ${inlineMd(e.next)}</div>` : '<div class="next meta">next action not set</div>',
|
|
656
835
|
e.signals.topRisk ? `<div class="risk amber-risk">${inlineMd(e.signals.topRisk)}</div>` : '',
|
|
836
|
+
`<div class="match-snippet"></div>`,
|
|
657
837
|
`</div>`,
|
|
658
838
|
].join('')
|
|
659
839
|
}).join('\n')
|
|
@@ -691,6 +871,9 @@ function cmdDashboard(args) {
|
|
|
691
871
|
const html = [
|
|
692
872
|
'<!doctype html><html lang="en"><head><meta charset="utf-8">',
|
|
693
873
|
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
874
|
+
// pre-paint theme: read the one localStorage key before first render so there is no flash.
|
|
875
|
+
// No network calls - this file still opens and works fully offline.
|
|
876
|
+
'<script>try{var t=localStorage.getItem("fde-fieldbook-theme");if(t==="dark"||(!t&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.setAttribute("data-fde-theme","dark");}catch(e){}</script>',
|
|
694
877
|
'<title>FDE Fieldbook</title><style>' + dashStyles() + '</style></head><body>',
|
|
695
878
|
'<header><div class="inner"><div class="brand"><h1>FDE Fieldbook</h1>',
|
|
696
879
|
`<span class="tagline">${engagements.length} engagement${engagements.length === 1 ? '' : 's'}</span></div>`,
|
|
@@ -698,9 +881,11 @@ function cmdDashboard(args) {
|
|
|
698
881
|
`<span class="stat"><span class="sdot green"></span> <b>${counts.green}</b> green</span>`,
|
|
699
882
|
`<span class="stat"><span class="sdot amber"></span> <b>${counts.amber}</b> amber</span>`,
|
|
700
883
|
`<span class="stat"><span class="sdot red"></span> <b>${counts.RED}</b> red</span>`,
|
|
884
|
+
'<button id="fde-theme-btn" class="theme-toggle" type="button">dark mode</button>',
|
|
701
885
|
'</div></div></header>',
|
|
702
886
|
'<div class="wrap">',
|
|
703
887
|
engagements.length ? '<div class="search-wrap"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><input id="q" class="search" placeholder="Search across all engagements..."></div>' : '',
|
|
888
|
+
engagements.length ? '<span class="search-hint">press <kbd>/</kbd> to search - a matching card shows why, right on the card</span>' : '',
|
|
704
889
|
directive ? `<div class="${directiveClass}"><span class="directive-dot"></span><span>${directive}</span></div>` : '',
|
|
705
890
|
reds.length ? '<h2 class="section">Needs attention</h2><div class="attn-stack">' + attention + '</div>' : '',
|
|
706
891
|
engagements.length ? `<h2 class="section">${reds.length ? 'Rest of portfolio' : 'Portfolio'}</h2>` : '',
|
|
@@ -732,6 +917,7 @@ switch (cmd) {
|
|
|
732
917
|
case 'scan': cmdScan(); break
|
|
733
918
|
case 'resume': cmdResume(args); break
|
|
734
919
|
case 'log': cmdLog(args); break
|
|
920
|
+
case 'debrief': cmdDebrief(args); break
|
|
735
921
|
case 'receipts': cmdReceipts(args); break
|
|
736
922
|
case 'capture': cmdCapture(); break
|
|
737
923
|
case 'status': cmdStatus(); break
|
|
@@ -741,8 +927,10 @@ switch (cmd) {
|
|
|
741
927
|
fde scan day-1 recon of this repo (facts, no AI)
|
|
742
928
|
fde resume load this workspace's engagement memory (bounded)
|
|
743
929
|
fde resume --full load the complete context.md (no bound)
|
|
744
|
-
fde resume --init <name> create + bind engagement for this workspace
|
|
745
|
-
fde
|
|
930
|
+
fde resume --init <name> create + bind engagement for this workspace (rebind replaces)
|
|
931
|
+
fde resume --bind show what this workspace is bound to, and what resolves
|
|
932
|
+
fde log <type> <text> append decision|risk|delivery|contact (contact takes --signal red|amber|green)
|
|
933
|
+
fde debrief [file] meeting notes → memory: decision:/risk:/delivery:/contact: lines route, rest → context.md (stdin if no file; --dry-run previews)
|
|
746
934
|
fde receipts <term> "what did we agree?" with dates
|
|
747
935
|
fde capture session-end memory snapshot (hooks use this)
|
|
748
936
|
fde status portfolio across all engagements
|
package/bin/install.js
CHANGED
|
@@ -210,11 +210,17 @@ function cmdInstall() {
|
|
|
210
210
|
console.log('')
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// `npx fdeops scan` must recon, not install - any fde subcommand passes straight
|
|
214
|
+
// through to the CLI (fde.js reads process.argv itself, so require() is enough).
|
|
215
|
+
const FDE_SUBCOMMANDS = ['scan', 'resume', 'log', 'debrief', 'receipts', 'capture', 'status', 'dashboard']
|
|
216
|
+
|
|
213
217
|
const arg = process.argv[2]
|
|
214
218
|
if (arg === 'init') {
|
|
215
219
|
cmdInit(process.argv[3])
|
|
216
220
|
} else if (arg === 'adapters') {
|
|
217
221
|
cmdAdapters(process.argv[3])
|
|
222
|
+
} else if (FDE_SUBCOMMANDS.includes(arg)) {
|
|
223
|
+
require(path.join(__dirname, 'fde.js'))
|
|
218
224
|
} else {
|
|
219
225
|
cmdInstall()
|
|
220
226
|
}
|
package/hooks/pre-compact
CHANGED
|
@@ -11,8 +11,27 @@ resolve_engagement_dir() {
|
|
|
11
11
|
return 1
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
# Workspace registry written by `fde resume --init` - mirrors resolveEngagement()
|
|
15
|
+
# step 2 in bin/fde.js; keep the resolution order identical in bash and JS.
|
|
16
|
+
# Lines are "<workspace-path> <slug>": the path may contain spaces, the slug
|
|
17
|
+
# never does, so split on the LAST space (same as the JS lastIndexOf parse).
|
|
18
|
+
registry_engagement_dir() {
|
|
19
|
+
local reg="$HOME/fde-engagements/.registry" slug
|
|
20
|
+
[ -f "$reg" ] || return 1
|
|
21
|
+
slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]}
|
|
22
|
+
{ i = match($0, / [^ ]*$/)
|
|
23
|
+
if (i > 0 && substr($0, 1, i - 1) == ws) { print substr($0, i + 1); exit } }' "$reg" 2>/dev/null)
|
|
24
|
+
[ -z "$slug" ] && return 1
|
|
25
|
+
[ -d "$HOME/fde-engagements/$slug/.fde" ] && printf '%s\n' "$HOME/fde-engagements/$slug/.fde" && return 0
|
|
26
|
+
return 1
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
|
|
15
30
|
|
|
31
|
+
if [ -z "$ENG_DIR" ]; then
|
|
32
|
+
ENG_DIR=$(registry_engagement_dir)
|
|
33
|
+
fi
|
|
34
|
+
|
|
16
35
|
if [ -z "$ENG_DIR" ] && [ -f "CLAUDE.md" ]; then
|
|
17
36
|
ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
|
|
18
37
|
ENG_DIR=$(resolve_engagement_dir "$ENG")
|
package/hooks/session-start
CHANGED
|
@@ -23,27 +23,48 @@ resolve_engagement_dir() {
|
|
|
23
23
|
return 1
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
# Workspace registry written by `fde resume --init` - mirrors resolveEngagement()
|
|
27
|
+
# step 2 in bin/fde.js; keep the resolution order identical in bash and JS.
|
|
28
|
+
# Lines are "<workspace-path> <slug>": the path may contain spaces, the slug
|
|
29
|
+
# never does, so split on the LAST space (same as the JS lastIndexOf parse).
|
|
30
|
+
registry_engagement_dir() {
|
|
31
|
+
local reg="$HOME/fde-engagements/.registry" slug
|
|
32
|
+
[ -f "$reg" ] || return 1
|
|
33
|
+
slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]}
|
|
34
|
+
{ i = match($0, / [^ ]*$/)
|
|
35
|
+
if (i > 0 && substr($0, 1, i - 1) == ws) { print substr($0, i + 1); exit } }' "$reg" 2>/dev/null)
|
|
36
|
+
[ -z "$slug" ] && return 1
|
|
37
|
+
[ -d "$HOME/fde-engagements/$slug/.fde" ] && printf '%s\n' "$HOME/fde-engagements/$slug/.fde" && return 0
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
# 1) Environment variable (any agent)
|
|
27
42
|
if [ -z "$CONTEXT_FILE" ]; then
|
|
28
43
|
ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
|
|
29
44
|
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
|
|
30
45
|
fi
|
|
31
46
|
|
|
32
|
-
# 2)
|
|
47
|
+
# 2) Workspace registry binding (written by fde resume --init)
|
|
48
|
+
if [ -z "$CONTEXT_FILE" ]; then
|
|
49
|
+
ENG_DIR=$(registry_engagement_dir)
|
|
50
|
+
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 3) Project CLAUDE.md
|
|
33
54
|
if [ -z "$CONTEXT_FILE" ] && [ -f "CLAUDE.md" ]; then
|
|
34
55
|
ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
|
|
35
56
|
ENG_DIR=$(resolve_engagement_dir "$ENG")
|
|
36
57
|
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
|
|
37
58
|
fi
|
|
38
59
|
|
|
39
|
-
#
|
|
60
|
+
# 4) Global fdeops pointer file
|
|
40
61
|
if [ -z "$CONTEXT_FILE" ] && [ -f "$HOME/.claude/FDEOPS-CLAUDE.md" ]; then
|
|
41
62
|
ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' "$HOME/.claude/FDEOPS-CLAUDE.md" 2>/dev/null | cut -d= -f2-)
|
|
42
63
|
ENG_DIR=$(resolve_engagement_dir "$ENG")
|
|
43
64
|
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
|
|
44
65
|
fi
|
|
45
66
|
|
|
46
|
-
#
|
|
67
|
+
# 5) Optional in-repo .fde (customer-approved only)
|
|
47
68
|
if [ -z "$CONTEXT_FILE" ] && [ -f ".fde/context.md" ]; then
|
|
48
69
|
CONTEXT_FILE=".fde/context.md"
|
|
49
70
|
fi
|