fdeops 4.0.3 → 4.0.4
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 +24 -11
- package/bin/lib/context.js +8 -1
- package/bin/lib/provenance.js +10 -0
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
package/bin/fde.js
CHANGED
|
@@ -36,7 +36,7 @@ const { createMemoryApi } = require('./lib/memory')
|
|
|
36
36
|
const { createTrustApi } = require('./lib/trust')
|
|
37
37
|
const vault = require('./lib/vault')
|
|
38
38
|
const context = require('./lib/context')
|
|
39
|
-
const { sourceReference, hasSource, datedDecisions } = require('./lib/provenance')
|
|
39
|
+
const { sourceReference, sourceReferences, hasSource, datedDecisions } = require('./lib/provenance')
|
|
40
40
|
const { deliverySummary } = require('./lib/delivery-gaps')
|
|
41
41
|
|
|
42
42
|
const HOME = os.homedir()
|
|
@@ -67,8 +67,13 @@ function maskReport(value) {
|
|
|
67
67
|
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskReport(item)]))
|
|
68
68
|
return value
|
|
69
69
|
}
|
|
70
|
-
function maskedSections(sections, maxBytes) {
|
|
71
|
-
|
|
70
|
+
function maskedSections(sections, maxBytes = context.DEFAULT_BYTES, heading = '') {
|
|
71
|
+
// Reserve identity before sharing the remaining budget between excerpts.
|
|
72
|
+
// Pathological path lengths remain explicitly clipped rather than exhausting policy space.
|
|
73
|
+
const maskedHeading = masking.mask(heading)
|
|
74
|
+
const prefix = heading ? (Buffer.byteLength(maskedHeading) <= 1024 ? maskedHeading
|
|
75
|
+
: context.clipMaskedUtf8(maskedHeading, 900) + '\n[Identity path truncated; use fde resume --bind to inspect.]') + '\n\n' : ''
|
|
76
|
+
return prefix + context.boundedSections(sections.map(text => masking.mask(text)), maxBytes - Buffer.byteLength(prefix))
|
|
72
77
|
}
|
|
73
78
|
const DEBRIEF_MAX_BYTES = 256 * 1024
|
|
74
79
|
const CODE_EXT = ['.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.go', '.rb', '.cs', '.php']
|
|
@@ -1459,12 +1464,12 @@ function cmdResume(args) {
|
|
|
1459
1464
|
process.stdout.write(maskedSections([
|
|
1460
1465
|
policy ? `CLIENT POLICY - trust-profile.md\n${policy}` : '',
|
|
1461
1466
|
preferences.work ? `WORKING PREFERENCES: ${preferences.work}. Starting help: ${preferences.start}.\n${setup.nextStep(preferences)}\nThis is a starting preference, not a client fact; current instructions and engagement state take precedence.` : '',
|
|
1462
|
-
|
|
1467
|
+
intro,
|
|
1463
1468
|
success ? `CURRENT GOALS & ACCEPTANCE - success.md\n${success}` : '',
|
|
1464
1469
|
risks ? `OPEN RISKS - risks.md\n${extractRisks(eng).map(r => r.text).join('\n') || '(none recorded)'}` : '',
|
|
1465
1470
|
`VALUE LEDGER - delivery.md\n${parseValueLedger(eng).rows.map(r => formatValueLedgerLine(r) + '; source: ' + (r.evidence || '(missing)')).join('\n') || '(none recorded)'}`,
|
|
1466
1471
|
`WORKING CONTEXT - context.md\n${ctx ? resumeView(ctx) : '(no context.md yet)'}`,
|
|
1467
|
-
], maxBytes))
|
|
1472
|
+
], maxBytes, `ENGAGEMENT: ${eng}`))
|
|
1468
1473
|
}
|
|
1469
1474
|
|
|
1470
1475
|
// Token discipline: context.md grows every session (the session-stop hook
|
|
@@ -2083,7 +2088,10 @@ function withDebriefRecords(eng, apply, files = ['decisions.md', 'risks.md', 'de
|
|
|
2083
2088
|
} catch (_) { failed.push(path.basename(target)) }
|
|
2084
2089
|
}
|
|
2085
2090
|
if (failed.length) throw new Error(`${error.message}; recovery could not restore ${failed.join(', ')}. Inspect these records and the pending proposal before retrying.`)
|
|
2086
|
-
|
|
2091
|
+
const recovery = snapshots.get(path.join(eng, DEBRIEF_PROPOSE))
|
|
2092
|
+
? 'The proposal is retained; inspect it with fde debrief --review before retrying.'
|
|
2093
|
+
: 'No pending proposal was retained; review your input before retrying.'
|
|
2094
|
+
throw new Error(`${error.message}; no record changes kept. ${recovery}`)
|
|
2087
2095
|
} finally { debriefTransactionActive = false }
|
|
2088
2096
|
}
|
|
2089
2097
|
return lockAt(0)
|
|
@@ -2156,7 +2164,7 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [], allowReplay =
|
|
|
2156
2164
|
})
|
|
2157
2165
|
}
|
|
2158
2166
|
const repeats = repeatedDebriefStatements(eng, input)
|
|
2159
|
-
if (repeats.length && !dry && !allowReplay) throw new Error('source-backed statement already recorded; review the existing record and newer next action. Explicitly confirm a repeat with --allow-replay, or remove the repeated statement from
|
|
2167
|
+
if (repeats.length && !dry && !allowReplay) throw new Error('source-backed statement already recorded; review the existing record and newer next action. Explicitly confirm a repeat with --allow-replay, or remove the repeated statement from your input or pending proposal.')
|
|
2160
2168
|
const d = new Date()
|
|
2161
2169
|
const date = d.toISOString().slice(0, 10)
|
|
2162
2170
|
const counts = { decision: 0, risk: 0, delivery: 0, contact: 0, next: 0, signer: 0 }
|
|
@@ -2547,15 +2555,20 @@ function cmdReceipts(args) {
|
|
|
2547
2555
|
const records = [], claims = []
|
|
2548
2556
|
for (const file of [...recordFiles, ...workingFiles]) {
|
|
2549
2557
|
const document = readClean(eng, file)
|
|
2558
|
+
const scaffold = new Set((templatesDir() ? readClean(templatesDir(), file) : '').split('\n').map(line => line.trim()))
|
|
2550
2559
|
const decisionSources = new Map()
|
|
2551
2560
|
if (file === 'decisions.md') for (const entry of datedDecisions(document)) {
|
|
2552
|
-
|
|
2553
|
-
for (let line = entry.line; line < entry.line + entry.text.split('\n').length; line++) decisionSources.set(line, source)
|
|
2561
|
+
for (let line = entry.line; line < entry.line + entry.text.split('\n').length; line++) decisionSources.set(line, entry.text)
|
|
2554
2562
|
}
|
|
2555
2563
|
document.split('\n').forEach((line, i) => {
|
|
2556
2564
|
if (!line.toLowerCase().includes(term.toLowerCase())) return
|
|
2557
|
-
const
|
|
2558
|
-
const
|
|
2565
|
+
const sourceText = decisionSources.get(i + 1) || line
|
|
2566
|
+
const source = sourceReference(sourceText)
|
|
2567
|
+
if (!source && scaffold.has(line.trim())) return
|
|
2568
|
+
const sources = sourceReferences(sourceText)
|
|
2569
|
+
const attribution = masking.mask(sources.join('; '))
|
|
2570
|
+
const displayed = Buffer.byteLength(attribution) <= 320 ? attribution : context.clipMaskedUtf8(attribution, 240) + '… [sources truncated; use fde recall]'
|
|
2571
|
+
const hit = ` ${file}:${i + 1} ${context.clipMaskedUtf8(masking.mask(line.trim()), 160)}${source ? ` [${sources.length > 1 ? 'sources' : 'source'}: ${displayed}]` : ' [source missing]'}${dirty.has(file) ? ' dirty file - review manual edits' : ''}`
|
|
2559
2572
|
;(recordFiles.includes(file) && source ? records : claims).push({ file, hit })
|
|
2560
2573
|
})
|
|
2561
2574
|
}
|
package/bin/lib/context.js
CHANGED
|
@@ -14,6 +14,13 @@ function clipUtf8(text, bytes) {
|
|
|
14
14
|
while (end > 0 && (buf[end] & 0xc0) === 0x80) end--
|
|
15
15
|
return buf.subarray(0, end).toString('utf8')
|
|
16
16
|
}
|
|
17
|
+
// Masking aliases must stay whole so they remain recognizable and reversible.
|
|
18
|
+
function clipMaskedUtf8(text, bytes) {
|
|
19
|
+
const clipped = clipUtf8(text, bytes)
|
|
20
|
+
if (clipped === text) return text
|
|
21
|
+
const open = clipped.lastIndexOf('[[')
|
|
22
|
+
return open > clipped.lastIndexOf(']]') ? clipped.slice(0, open) : clipped
|
|
23
|
+
}
|
|
17
24
|
function budgetArgs(args, defaultBytes = DEFAULT_BYTES) {
|
|
18
25
|
const rest = [...args]
|
|
19
26
|
let maxBytes = defaultBytes
|
|
@@ -91,4 +98,4 @@ function recallSections(documents, query, maxHits = 12, outputText = text => tex
|
|
|
91
98
|
sections: selected.map(h => `${h.file}:${h.line}-${h.end} (lines in redacted view)\n${h.text}`),
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
module.exports = { DEFAULT_BYTES, clipUtf8, budgetArgs, boundedSections, recallSections }
|
|
101
|
+
module.exports = { DEFAULT_BYTES, clipUtf8, clipMaskedUtf8, budgetArgs, boundedSections, recallSections }
|
package/bin/lib/provenance.js
CHANGED
|
@@ -40,3 +40,13 @@ function datedDecisions(value) {
|
|
|
40
40
|
return entries.sort((a, b) => a.date.localeCompare(b.date) || a.line - b.line)
|
|
41
41
|
}
|
|
42
42
|
module.exports.datedDecisions = datedDecisions
|
|
43
|
+
|
|
44
|
+
// Display every explicit attribution without selecting a primary source.
|
|
45
|
+
// Keep classification through sourceReference unchanged.
|
|
46
|
+
function sourceReferences(value) {
|
|
47
|
+
const explicit = String(value || '').match(/\[source:[^\]\n]+\]/gi) || []
|
|
48
|
+
if (explicit.length) return [...new Set(explicit.map(sourceReference).filter(Boolean))]
|
|
49
|
+
const fallback = sourceReference(value)
|
|
50
|
+
return fallback ? [fallback] : []
|
|
51
|
+
}
|
|
52
|
+
module.exports.sourceReferences = sourceReferences
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"description": "Client delivery tools for Forward Deployed Engineers. One @fde skill, local Markdown engagement records, and an offline dashboard for decisions, evidence, approvals, and next actions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fdeops": "bin/install.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "fdeops",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.4",
|
|
5
5
|
"description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code. You confirm; then it lands in .fde/ on your laptop.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|