fdeops 3.9.14 → 3.9.15
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 +82 -20
- package/package.json +1 -1
package/bin/fde.js
CHANGED
|
@@ -107,9 +107,13 @@ function resolveEngagement(opts = {}) {
|
|
|
107
107
|
// bind - env, registry, pointer, or in-repo .fde. Basename matching is
|
|
108
108
|
// read-only convenience; writing on a folder-name guess contaminates clients.
|
|
109
109
|
const forWrite = !!opts.forWrite
|
|
110
|
+
const accept = (p) => acceptEngagementPath(p, { forWrite })
|
|
110
111
|
// 1) explicit env (back-compat: accept old FDEOS_ENGAGEMENT too)
|
|
111
112
|
const env = (process.env.FDEOPS_ENGAGEMENT || process.env.FDEOS_ENGAGEMENT || '').replace(/^~/, HOME).trim()
|
|
112
|
-
if (env
|
|
113
|
+
if (env) {
|
|
114
|
+
const ok = accept(env)
|
|
115
|
+
if (ok) return ok
|
|
116
|
+
}
|
|
113
117
|
// 2) workspace registry binding (written by resume --init). Match the cwd OR
|
|
114
118
|
// any ancestor of it - FDEs run commands from src/, packages/api/, etc., not
|
|
115
119
|
// just the repo root where they bound. Nearest (deepest) registered ancestor
|
|
@@ -122,8 +126,8 @@ function resolveEngagement(opts = {}) {
|
|
|
122
126
|
.filter(r => cwd === r.workspace || cwd.startsWith(r.workspace + path.sep))
|
|
123
127
|
.sort((a, b) => b.workspace.length - a.workspace.length)[0]
|
|
124
128
|
if (reg) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
129
|
+
const ok = accept(path.join(ENGAGEMENTS_ROOT, reg.slug, '.fde'))
|
|
130
|
+
if (ok) return ok
|
|
127
131
|
}
|
|
128
132
|
// 3) global pointer file (back-compat: try old FDEOS-CLAUDE.md too)
|
|
129
133
|
for (const ptrName of ['FDEOPS-CLAUDE.md', 'FDEOS-CLAUDE.md']) {
|
|
@@ -131,8 +135,8 @@ function resolveEngagement(opts = {}) {
|
|
|
131
135
|
const ptr = fs.readFileSync(path.join(HOME, '.claude', ptrName), 'utf8')
|
|
132
136
|
const m = ptr.match(/^(?:FDEOPS|FDEOS)_ENGAGEMENT=(.+)$/m)
|
|
133
137
|
if (m) {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
138
|
+
const ok = accept(m[1].trim().replace(/^~/, HOME))
|
|
139
|
+
if (ok) return ok
|
|
136
140
|
}
|
|
137
141
|
} catch (_) {}
|
|
138
142
|
}
|
|
@@ -150,14 +154,37 @@ function resolveEngagement(opts = {}) {
|
|
|
150
154
|
)
|
|
151
155
|
return null
|
|
152
156
|
}
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
const ok = accept(guess)
|
|
158
|
+
if (ok) {
|
|
159
|
+
process.stderr.write(`⚠ resolved engagement by directory name ("${slugGuess}"), not a saved binding (read-only). If this is the right client, run \`fde resume --init ${slugGuess}\` here to bind it before logging or debriefing.\n`)
|
|
160
|
+
return ok
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
// 5) in-repo .fde (engagement-approved only)
|
|
157
|
-
|
|
164
|
+
const inRepo = accept(path.join(cwd, '.fde'))
|
|
165
|
+
if (inRepo) return inRepo
|
|
158
166
|
return null
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
// Engagement memory must be a directory. A file named .fde used to yield a
|
|
170
|
+
// healthy-looking green TRIAGE then raw ENOTDIR on write - refuse loudly.
|
|
171
|
+
function acceptEngagementPath(p, opts = {}) {
|
|
172
|
+
if (!p || !fs.existsSync(p)) return null
|
|
173
|
+
try {
|
|
174
|
+
const st = fs.statSync(p)
|
|
175
|
+
if (st.isDirectory()) return p
|
|
176
|
+
const msg =
|
|
177
|
+
`engagement path is not a directory (memory missing/broken): ${p}\n` +
|
|
178
|
+
' repair: remove that file, then re-run: fde resume --init <name>'
|
|
179
|
+
console.error(msg)
|
|
180
|
+
if (opts.forWrite) process.exit(1)
|
|
181
|
+
return null
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (opts.forWrite) failFs(e, 'open', p)
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
function templatesDir() {
|
|
162
189
|
for (const c of [path.join(__dirname, '..', 'templates', '.fde'), path.join(__dirname, 'templates', '.fde')]) {
|
|
163
190
|
if (fs.existsSync(c)) return c
|
|
@@ -172,11 +199,19 @@ function readEng(eng, f) {
|
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
// Redact private notes and template hints from every model-facing read.
|
|
202
|
+
// Also strip terminal control chars so poisoned memory cannot smuggle ANSI
|
|
203
|
+
// into triage/prep/status (C0/C1 except tab/LF/CR).
|
|
204
|
+
function stripControlChars(s) {
|
|
205
|
+
return String(s || '').replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
|
|
206
|
+
}
|
|
207
|
+
|
|
175
208
|
function stripPrivate(md) {
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
return stripControlChars(
|
|
210
|
+
String(md || '')
|
|
211
|
+
.replace(/<private>[\s\S]*?<\/private>/gi, '(private - redacted)')
|
|
212
|
+
.replace(/<private>[\s\S]*$/i, '(private - redacted)')
|
|
213
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
214
|
+
)
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
// Read + redact in one step - the default way dashboard code should ever touch
|
|
@@ -239,6 +274,9 @@ function formatFsError(err, action, target) {
|
|
|
239
274
|
const code = err && err.code
|
|
240
275
|
const where = path.basename(String(target || '')) || String(target || 'path')
|
|
241
276
|
if (code === 'ENOSPC') return `cannot ${action} ${where} - disk full`
|
|
277
|
+
if (code === 'ENOTDIR') {
|
|
278
|
+
return `cannot ${action} ${where} - engagement path is not a directory (memory missing/broken); remove the file and re-run fde resume --init`
|
|
279
|
+
}
|
|
242
280
|
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') {
|
|
243
281
|
return `cannot ${action} ${where} - permission denied (read-only or locked down)`
|
|
244
282
|
}
|
|
@@ -392,7 +430,7 @@ function datedEntry(eng, date, text, signal) {
|
|
|
392
430
|
const bits = [`- [${date}]`]
|
|
393
431
|
if (who) bits.push(`[${who}]`)
|
|
394
432
|
if (signal) bits.push(`[signal:${signal}]`)
|
|
395
|
-
bits.push(text)
|
|
433
|
+
bits.push(stripControlChars(text))
|
|
396
434
|
return bits.join(' ')
|
|
397
435
|
}
|
|
398
436
|
|
|
@@ -1136,7 +1174,7 @@ function smartProposeText(input) {
|
|
|
1136
1174
|
|
|
1137
1175
|
function setNextAction(eng, text) {
|
|
1138
1176
|
ensureMemoryGit(eng)
|
|
1139
|
-
const bullet = `- ${String(text).replace(/^[-*]\s+/, '').trim()}`
|
|
1177
|
+
const bullet = `- ${stripControlChars(String(text).replace(/^[-*]\s+/, '').trim())}`
|
|
1140
1178
|
const p = path.join(eng, 'context.md')
|
|
1141
1179
|
let md = readEng(eng, 'context.md')
|
|
1142
1180
|
if (!md) md = '# Engagement context\n\n'
|
|
@@ -1148,6 +1186,24 @@ function setNextAction(eng, text) {
|
|
|
1148
1186
|
withFileLock(p, () => { atomicWriteFile(p, md.endsWith('\n') ? md : md + '\n') })
|
|
1149
1187
|
}
|
|
1150
1188
|
|
|
1189
|
+
function looksLikeBinaryNoise(text) {
|
|
1190
|
+
const s = String(text || '')
|
|
1191
|
+
if (!s) return false
|
|
1192
|
+
if (s.includes('\0')) return true
|
|
1193
|
+
const sample = s.slice(0, 8192)
|
|
1194
|
+
let ctrl = 0
|
|
1195
|
+
let replacement = 0
|
|
1196
|
+
for (let i = 0; i < sample.length; i++) {
|
|
1197
|
+
const c = sample.charCodeAt(i)
|
|
1198
|
+
if (c === 0xfffd) replacement++
|
|
1199
|
+
if (c === 9 || c === 10 || c === 13) continue
|
|
1200
|
+
if (c < 32 || (c >= 0x7f && c <= 0x9f)) ctrl++
|
|
1201
|
+
}
|
|
1202
|
+
if (!sample.length) return false
|
|
1203
|
+
// Mostly-control or high U+FFFD density = urandom / binary mistyped as text.
|
|
1204
|
+
return (ctrl / sample.length) > 0.05 || (replacement / sample.length) > 0.1
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1151
1207
|
function readDebriefInput(args) {
|
|
1152
1208
|
let input = ''
|
|
1153
1209
|
if (args[0]) {
|
|
@@ -1160,19 +1216,25 @@ function readDebriefInput(args) {
|
|
|
1160
1216
|
}
|
|
1161
1217
|
let buf
|
|
1162
1218
|
try { buf = fs.readFileSync(notesPath) } catch (_) { console.error(`cannot read ${args[0]}`); process.exit(1) }
|
|
1163
|
-
if (buf.includes(0)) {
|
|
1164
|
-
console.error(`debrief refused: ${args[0]} looks binary
|
|
1219
|
+
if (buf.includes(0) || looksLikeBinaryNoise(buf.toString('utf8'))) {
|
|
1220
|
+
console.error(`debrief refused: ${args[0]} looks binary or mostly non-printable. Paste text notes only.`)
|
|
1165
1221
|
process.exit(1)
|
|
1166
1222
|
}
|
|
1167
1223
|
input = buf.toString('utf8')
|
|
1168
1224
|
} else {
|
|
1169
|
-
|
|
1170
|
-
|
|
1225
|
+
let buf
|
|
1226
|
+
try { buf = fs.readFileSync(0) } catch (_) { buf = Buffer.alloc(0) }
|
|
1227
|
+
if (Buffer.byteLength(buf) > DEBRIEF_MAX_BYTES) {
|
|
1171
1228
|
console.error(`debrief refused: stdin is over ${DEBRIEF_MAX_BYTES} bytes. Split the notes.`)
|
|
1172
1229
|
process.exit(1)
|
|
1173
1230
|
}
|
|
1231
|
+
if (buf.includes(0) || looksLikeBinaryNoise(buf.toString('utf8'))) {
|
|
1232
|
+
console.error('debrief refused: stdin looks binary or mostly non-printable. Paste text notes only.')
|
|
1233
|
+
process.exit(1)
|
|
1234
|
+
}
|
|
1235
|
+
input = buf.toString('utf8')
|
|
1174
1236
|
}
|
|
1175
|
-
return input
|
|
1237
|
+
return stripControlChars(input)
|
|
1176
1238
|
}
|
|
1177
1239
|
|
|
1178
1240
|
function previewLine(text, max = 240) {
|
|
@@ -1250,7 +1312,7 @@ function cmdDebrief(args) {
|
|
|
1250
1312
|
|
|
1251
1313
|
let input = ''
|
|
1252
1314
|
if (apply && !smart && !args[0]) {
|
|
1253
|
-
try { input = fs.readFileSync(path.join(eng, DEBRIEF_PROPOSE), 'utf8') } catch (_) {
|
|
1315
|
+
try { input = stripControlChars(fs.readFileSync(path.join(eng, DEBRIEF_PROPOSE), 'utf8')) } catch (_) {
|
|
1254
1316
|
console.error('nothing to apply - run: fde debrief --smart <notes.md> then fde debrief --apply')
|
|
1255
1317
|
process.exit(1)
|
|
1256
1318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.15",
|
|
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",
|