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 +25 -7
- package/hooks/pre-compact +25 -4
- package/hooks/session-start +7 -3
- package/package.json +4 -3
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))
|
|
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
|
-
|
|
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(/(['"])
|
|
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
|
-
|
|
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
|
|
730
|
-
.replace(/<private>[\s\S]*$/i, '(private - redacted
|
|
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 -
|
|
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=$(
|
|
71
|
-
[ -f "$RISKS_FILE" ] && OPEN_RISKS=$(grep -iE "open|active|unresolved"
|
|
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
|
|
package/hooks/session-start
CHANGED
|
@@ -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(
|
|
98
|
+
if (index(lc, "</private>") > 0) { inblock = 0 }
|
|
95
99
|
next
|
|
96
100
|
}
|
|
97
|
-
if (index(
|
|
101
|
+
if (index(lc, "<private>") > 0) {
|
|
98
102
|
print "(private - redacted)"
|
|
99
|
-
if (index(
|
|
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.
|
|
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
|
-
"
|
|
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/",
|