fdeops 3.7.7 → 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 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 m = l.trim().match(/^-\s*\[(\d{4}-\d{2}-\d{2})\]\s*\[signal:(red|amber|green)\]\s*(.+)$/i)
310
- if (m) history.push({ date: m[1], signal: m[2].toLowerCase(), text: m[3] })
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 => {
@@ -620,7 +670,8 @@ function cmdLog(args) {
620
670
  const eng = resolveEngagement()
621
671
  if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
622
672
  const date = new Date().toISOString().slice(0, 10)
623
- fs.appendFileSync(path.join(eng, LOG_FILES[type]), `\n- [${date}] ${signal ? `[signal:${signal}] ` : ''}${text}\n`)
673
+ const entry = `- [${date}] ${signal ? `[signal:${signal}] ` : ''}${text}`
674
+ appendLogEntry(eng, type, entry)
624
675
  console.log(`logged → ${LOG_FILES[type]}${signal ? ` (signal:${signal})` : ''}`)
625
676
  }
626
677
 
@@ -676,8 +727,12 @@ function cmdDebrief(args) {
676
727
  const m = bare.match(/^(decision|risk|delivery|contact):\s*(.+)$/i)
677
728
  if (m) {
678
729
  const type = m[1].toLowerCase()
679
- if (dry) console.log(`→ ${LOG_FILES[type]} - [${date}] ${m[2]}`)
680
- else fs.appendFileSync(path.join(eng, LOG_FILES[type]), `\n- [${date}] ${m[2]}\n`)
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)
681
736
  counts[type]++
682
737
  } else ctxLines.push(line)
683
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.7.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",
@@ -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
+