fdeops 3.7.2 → 3.7.3

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
@@ -111,9 +111,17 @@ function resolveEngagement() {
111
111
  }
112
112
  } catch (_) {}
113
113
  }
114
- // 4) workspace dir name matches an engagement slug
114
+ // 4) workspace dir name matches an engagement slug. This is a convenience,
115
+ // NOT a binding - an unbound directory that merely happens to be named like a
116
+ // client (a fork, a demo, a second client with the same codename) would
117
+ // otherwise attach to that client's memory silently and get written into.
118
+ // Never silent: warn on stderr so cross-client contamination can't happen
119
+ // unnoticed, and tell the user how to make the binding explicit.
115
120
  const guess = path.join(ENGAGEMENTS_ROOT, slugify(path.basename(cwd)), '.fde')
116
- if (fs.existsSync(guess)) return guess
121
+ if (fs.existsSync(guess)) {
122
+ process.stderr.write(`⚠ resolved engagement by directory name ("${slugify(path.basename(cwd))}"), not a saved binding. If this is the right client, run \`fde resume --init ${slugify(path.basename(cwd))}\` here to bind it; if not, you are about to read/write the WRONG client's memory.\n`)
123
+ return guess
124
+ }
117
125
  // 5) in-repo .fde (engagement-approved only)
118
126
  if (fs.existsSync(path.join(cwd, '.fde'))) return path.join(cwd, '.fde')
119
127
  return null
@@ -157,7 +165,9 @@ function sectionBody(md, heading) {
157
165
  // zero-effort floor when NO token exists anywhere - prose like "escalated to CTO,
158
166
  // resolved amicably" must not flip a client amber forever.
159
167
  function computeSignals(eng) {
160
- const ctx = readEng(eng, 'context.md'); const stake = readEng(eng, 'stakeholders.md'); const risks = readEng(eng, 'risks.md')
168
+ // readClean, not readEng: status/dashboard echo topRisk and stakeholder lines
169
+ // to the terminal and the rendered HTML - a <private> risk must never surface.
170
+ const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md'); const risks = readClean(eng, 'risks.md')
161
171
  const phase = (ctx.match(/phase[:* ]+\**([a-z-]+)/i) || [])[1] || '?'
162
172
  let latest = null
163
173
  for (const l of stake.split('\n')) {
@@ -463,7 +473,7 @@ function cmdScan() {
463
473
  const sec = grepFiles(confFiles, /(api[_-]?key|secret|password|token)\s*[:=]\s*['"][^'"]{8,}/i, 10)
464
474
  .filter(h => !/example|template|test|sample|placeholder/i.test(h.file + h.text))
465
475
  sec.length
466
- ? sec.forEach(h => out.push(` ${h.file}:${h.line} ${h.text.replace(/(['"])([^'"]{4})[^'"]+(['"])/, '$1$2…REDACTED$3')}`))
476
+ ? sec.forEach(h => out.push(` ${h.file}:${h.line} ${h.text.replace(/(['"])[^'"]+(['"])/, '$1REDACTED$2')}`))
467
477
  : out.push(' none found')
468
478
  out.push(' (grep-grade check - run gitleaks or trufflehog for real secret coverage)')
469
479
 
@@ -661,7 +671,10 @@ function cmdReceipts(args) {
661
671
  for (const f of ['decisions.md', 'delivery.md', 'risks.md', 'stakeholders.md', 'context.md', 'success.md', 'brief.md', 'reality.md']) {
662
672
  const p = path.join(eng, f)
663
673
  if (!fs.existsSync(p)) continue
664
- fs.readFileSync(p, 'utf8').split('\n').forEach((l, i) => {
674
+ // readClean, not raw read: receipts must not grep sealed <private> notes back
675
+ // out. Redaction can shift line numbers past a multi-line block; the file:line
676
+ // is advisory, and not leaking a sealed secret is worth an approximate number.
677
+ readClean(eng, f).split('\n').forEach((l, i) => {
665
678
  if (rx.test(l)) { console.log(`${f}:${i + 1} ${l.trim().slice(0, 160)}`); found++ }
666
679
  })
667
680
  }
@@ -725,9 +738,14 @@ function escapeHtml(s) {
725
738
  // Closed pairs are redacted; an unclosed <private> redacts to end-of-text so a
726
739
  // forgotten closing tag can never leak the rest of the file.
727
740
  function stripPrivate(md) {
741
+ // Redact <private> before <!-- --> so a private block that itself contains a
742
+ // comment can't survive. Closed pairs first; then any unclosed <private> to
743
+ // end-of-text so a forgotten close tag never leaks the rest of the file.
744
+ // Case-insensitive (<PRIVATE> too). This is the ONE redactor every read path
745
+ // that can reach the model or a shared artifact must go through - see readClean.
728
746
  return md
729
- .replace(/<private>[\s\S]*?<\/private>/gi, '(private - redacted from dashboard)')
730
- .replace(/<private>[\s\S]*$/i, '(private - redacted from dashboard)')
747
+ .replace(/<private>[\s\S]*?<\/private>/gi, '(private - redacted)')
748
+ .replace(/<private>[\s\S]*$/i, '(private - redacted)')
731
749
  .replace(/<!--[\s\S]*?-->/g, '')
732
750
  }
733
751
 
package/hooks/pre-compact CHANGED
@@ -56,19 +56,40 @@ MARKER="[fdeops context preserved"
56
56
  [ -f "$CONTEXT_FILE" ] || exit 0
57
57
 
58
58
  # Avoid unbounded growth: skip if we already preserved today.
59
- if grep -q "$MARKER" "$CONTEXT_FILE" 2>/dev/null; then
60
- LAST=$(grep "$MARKER" "$CONTEXT_FILE" | tail -1)
59
+ if grep -Fq "$MARKER" "$CONTEXT_FILE" 2>/dev/null; then
60
+ LAST=$(grep -F "$MARKER" "$CONTEXT_FILE" | tail -1)
61
61
  if echo "$LAST" | grep -q "$(date -u +%Y-%m-%d)"; then
62
62
  exit 0
63
63
  fi
64
64
  fi
65
65
 
66
+ # Redact <private> before extracting lines - this content is appended to
67
+ # context.md, which session-start loads into the model. A closed private block
68
+ # collapses to one placeholder line, so its inner text can't be tail'd or
69
+ # grepped out tag-stripped. Case-insensitive, mirrors the JS /gi redactor.
70
+ strip_private() {
71
+ awk '
72
+ BEGIN { inblock = 0 }
73
+ {
74
+ line = $0
75
+ lc = tolower(line)
76
+ if (inblock) { if (index(lc, "</private>") > 0) { inblock = 0 } next }
77
+ if (index(lc, "<private>") > 0) {
78
+ print "(private - redacted)"
79
+ if (index(lc, "</private>") == 0) { inblock = 1 }
80
+ next
81
+ }
82
+ print line
83
+ }
84
+ ' "$1"
85
+ }
86
+
66
87
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
67
88
  LAST_DECISIONS=""
68
89
  OPEN_RISKS=""
69
90
 
70
- [ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(tail -20 "$DECISIONS_FILE" 2>/dev/null)
71
- [ -f "$RISKS_FILE" ] && OPEN_RISKS=$(grep -iE "open|active|unresolved" "$RISKS_FILE" 2>/dev/null | head -8)
91
+ [ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(strip_private "$DECISIONS_FILE" 2>/dev/null | tail -20)
92
+ [ -f "$RISKS_FILE" ] && OPEN_RISKS=$(strip_private "$RISKS_FILE" 2>/dev/null | grep -iE "open|active|unresolved" | head -8)
72
93
 
73
94
  cat >> "$CONTEXT_FILE" 2>/dev/null << EOF
74
95
 
@@ -86,17 +86,21 @@ fi
86
86
  # closing tag), so a line-based state machine matches the real usage; an
87
87
  # unclosed <private> redacts to end-of-file, same as the JS regex fallback.
88
88
  strip_private() {
89
+ # Detection is case-insensitive (matches the JS /gi redactor) - <PRIVATE>,
90
+ # <Private>, <private> all redact. Detect on a lowercased copy of the line;
91
+ # never print the original when a tag is present.
89
92
  awk '
90
93
  BEGIN { inblock = 0 }
91
94
  {
92
95
  line = $0
96
+ lc = tolower(line)
93
97
  if (inblock) {
94
- if (index(line, "</private>") > 0) { inblock = 0 }
98
+ if (index(lc, "</private>") > 0) { inblock = 0 }
95
99
  next
96
100
  }
97
- if (index(line, "<private>") > 0) {
101
+ if (index(lc, "<private>") > 0) {
98
102
  print "(private - redacted)"
99
- if (index(line, "</private>") == 0) { inblock = 1 }
103
+ if (index(lc, "</private>") == 0) { inblock = 1 }
100
104
  next
101
105
  }
102
106
  print line
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.7.2",
3
+ "version": "3.7.3",
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",
7
7
  "fde": "bin/fde.js"
8
8
  },
9
9
  "scripts": {
10
- "check": "node bin/check.js",
11
- "prepublishOnly": "node bin/check.js"
10
+ "check": "node bin/check.js && npm test",
11
+ "test": "node --test test/*.test.js",
12
+ "prepublishOnly": "node bin/check.js && npm test"
12
13
  },
13
14
  "files": [
14
15
  "bin/",