fdeops 3.7.6 → 3.7.8
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/bin/fde.js +66 -6
- package/bin/install.js +10 -0
- package/hooks/session-start +4 -1
- package/package.json +1 -1
- package/skills/fde/SKILL.md +1 -0
- package/skills/fde/references/land.md +1 -0
- package/templates/.fde/stakeholders.md +8 -0
package/bin/fde.js
CHANGED
|
@@ -169,6 +169,45 @@ function sectionBody(md, heading) {
|
|
|
169
169
|
return body.join('\n').trim()
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// Append `entry` as the last line of a "## Heading" section, creating the
|
|
173
|
+
// section at end-of-file if it doesn't exist yet. Plain fs.appendFileSync
|
|
174
|
+
// would land the entry after ANY later section the agent added (e.g. a
|
|
175
|
+
// "## Notes" heading appended after "## Signal history"), silently moving a
|
|
176
|
+
// signal token outside the section the reader scans - this keeps it inside
|
|
177
|
+
// regardless of what follows.
|
|
178
|
+
function appendUnderSection(md, heading, entry) {
|
|
179
|
+
const lines = md.split('\n')
|
|
180
|
+
const start = lines.findIndex(l => new RegExp('^#{1,6}\\s+' + heading + '\\b', 'i').test(l.trim()))
|
|
181
|
+
if (start === -1) {
|
|
182
|
+
const sep = md.length && !md.endsWith('\n') ? '\n' : ''
|
|
183
|
+
return `${md}${sep}\n## ${heading}\n\n${entry}\n`
|
|
184
|
+
}
|
|
185
|
+
let end = lines.length
|
|
186
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
187
|
+
if (/^#{1,6}\s/.test(lines[i].trim())) { end = i; break }
|
|
188
|
+
}
|
|
189
|
+
const before = lines.slice(0, end)
|
|
190
|
+
const after = lines.slice(end)
|
|
191
|
+
while (before.length > start + 1 && before[before.length - 1].trim() === '') before.pop()
|
|
192
|
+
before.push(entry)
|
|
193
|
+
if (after.length) before.push('')
|
|
194
|
+
return before.concat(after).join('\n')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Shared by cmdLog and cmdDebrief so the two writers can't drift (the earlier
|
|
198
|
+
// bug: fde log's format and fde debrief's format both existed, only one of
|
|
199
|
+
// them matched what extractStakeholders actually read). A contact entry
|
|
200
|
+
// carrying a [signal:x] token - however it got there - lands inside
|
|
201
|
+
// "## Signal history"; everything else is a plain end-of-file append.
|
|
202
|
+
function appendLogEntry(eng, type, entry) {
|
|
203
|
+
const p = path.join(eng, LOG_FILES[type])
|
|
204
|
+
if (type === 'contact' && /\[signal:(red|amber|green)\]/i.test(entry)) {
|
|
205
|
+
fs.writeFileSync(p, appendUnderSection(readEng(eng, LOG_FILES[type]), 'Signal history', entry))
|
|
206
|
+
} else {
|
|
207
|
+
fs.appendFileSync(p, `\n${entry}\n`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
172
211
|
// phase / trust / top risk / freshness - identical heuristic for status + dashboard.
|
|
173
212
|
// Trust resolution: structured [signal:red|amber|green] tokens in stakeholders.md
|
|
174
213
|
// (written by `fde log contact --signal` and `fde debrief`) win - the latest dated
|
|
@@ -305,9 +344,20 @@ function extractStakeholders(eng) {
|
|
|
305
344
|
const notesIdx = colIndex(headers, /notes?/i)
|
|
306
345
|
|
|
307
346
|
const history = []
|
|
347
|
+
// Format-agnostic on token position: `fde log contact --signal` writes
|
|
348
|
+
// "[date] [signal:x] text" (token right after the date), but `fde debrief`
|
|
349
|
+
// appends the token at the END of whatever the agent wrote per the skill's
|
|
350
|
+
// own contact: convention - "[date] text [signal:x]". Both are subject-first
|
|
351
|
+
// once the token is stripped, so match the token anywhere on the line rather
|
|
352
|
+
// than requiring it immediately after the date; a debrief-written signal was
|
|
353
|
+
// silently invisible to per-stakeholder matching before this.
|
|
308
354
|
sectionBody(md, 'Signal history').split('\n').forEach(l => {
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
355
|
+
const dm = l.trim().match(/^-\s*\[(\d{4}-\d{2}-\d{2})\]\s*(.*)$/i)
|
|
356
|
+
if (!dm) return
|
|
357
|
+
const sm = dm[2].match(/\[signal:(red|amber|green)\]/i)
|
|
358
|
+
if (!sm) return
|
|
359
|
+
const text = dm[2].replace(/\[signal:(red|amber|green)\]/i, '').trim()
|
|
360
|
+
history.push({ date: dm[1], signal: sm[1].toLowerCase(), text })
|
|
311
361
|
})
|
|
312
362
|
|
|
313
363
|
return rows.map(cs => {
|
|
@@ -588,7 +638,12 @@ function resumeView(md) {
|
|
|
588
638
|
// matches the bash hook's `wc -l` and the two bounded views stay byte-aligned.
|
|
589
639
|
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
590
640
|
if (lines.length <= 160) return md
|
|
591
|
-
|
|
641
|
+
// Anchor on the "## Session end" heading, NOT the "<!-- fdeops auto-capture -->"
|
|
642
|
+
// comment: this text is read via readClean (stripPrivate strips HTML comments),
|
|
643
|
+
// so the comment is gone by the time we get here. The heading is written on the
|
|
644
|
+
// very next line by cmdCapture and the session-stop hook and survives redaction.
|
|
645
|
+
// The bash bounded_context() anchors on the same heading - keep them identical.
|
|
646
|
+
let headEnd = lines.findIndex(l => /^##\s+Session end\b/.test(l.trim()))
|
|
592
647
|
if (headEnd === -1) headEnd = 120
|
|
593
648
|
headEnd = Math.min(headEnd, 120)
|
|
594
649
|
const tailStart = Math.max(headEnd, lines.length - 40)
|
|
@@ -615,7 +670,8 @@ function cmdLog(args) {
|
|
|
615
670
|
const eng = resolveEngagement()
|
|
616
671
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
617
672
|
const date = new Date().toISOString().slice(0, 10)
|
|
618
|
-
|
|
673
|
+
const entry = `- [${date}] ${signal ? `[signal:${signal}] ` : ''}${text}`
|
|
674
|
+
appendLogEntry(eng, type, entry)
|
|
619
675
|
console.log(`logged → ${LOG_FILES[type]}${signal ? ` (signal:${signal})` : ''}`)
|
|
620
676
|
}
|
|
621
677
|
|
|
@@ -671,8 +727,12 @@ function cmdDebrief(args) {
|
|
|
671
727
|
const m = bare.match(/^(decision|risk|delivery|contact):\s*(.+)$/i)
|
|
672
728
|
if (m) {
|
|
673
729
|
const type = m[1].toLowerCase()
|
|
674
|
-
|
|
675
|
-
|
|
730
|
+
const entry = `- [${date}] ${m[2]}`
|
|
731
|
+
if (dry) console.log(`→ ${LOG_FILES[type]} ${entry}`)
|
|
732
|
+
// appendLogEntry, not a blind append: a contact: line may carry an
|
|
733
|
+
// inline [signal:x] token (the skill's own convention) and must land
|
|
734
|
+
// inside "## Signal history" the same way `fde log --signal` does.
|
|
735
|
+
else appendLogEntry(eng, type, entry)
|
|
676
736
|
counts[type]++
|
|
677
737
|
} else ctxLines.push(line)
|
|
678
738
|
}
|
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/hooks/session-start
CHANGED
|
@@ -126,7 +126,10 @@ bounded_context() {
|
|
|
126
126
|
total=$(wc -l < "$f" 2>/dev/null | tr -d ' ')
|
|
127
127
|
[ -z "$total" ] && { cat "$f"; return; }
|
|
128
128
|
if [ "$total" -le 160 ]; then cat "$f"; return; fi
|
|
129
|
-
|
|
129
|
+
# Anchor on the "## Session end" heading, matching resumeView() in bin/fde.js.
|
|
130
|
+
# The JS path reads via readClean (strips HTML comments), so both sides must
|
|
131
|
+
# anchor on a marker that survives redaction - the heading, not the comment.
|
|
132
|
+
head_end=$(grep -n -m1 '^## Session end' "$f" 2>/dev/null | cut -d: -f1)
|
|
130
133
|
if [ -n "$head_end" ]; then head_end=$((head_end - 1)); else head_end=120; fi
|
|
131
134
|
[ "$head_end" -gt 120 ] && head_end=120
|
|
132
135
|
[ "$head_end" -lt 0 ] && head_end=0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.8",
|
|
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
|
|
|
@@ -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
|
+
|