@dzhechkov/harness-core 0.4.1 → 0.4.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/.dz-manifest.json +136 -56
- package/README.md +4 -2
- package/dist/backlog.d.ts +35 -0
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +167 -3
- package/dist/backlog.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +48 -3
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +85 -24
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +9 -9
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan-graph.d.ts +49 -0
- package/dist/loop-plan-graph.d.ts.map +1 -0
- package/dist/loop-plan-graph.js +128 -0
- package/dist/loop-plan-graph.js.map +1 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +13 -15
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +95 -16
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +26 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +65 -1
- package/dist/loop-trace.js.map +1 -1
- package/dist/model-recommender.d.ts +91 -0
- package/dist/model-recommender.d.ts.map +1 -0
- package/dist/model-recommender.js +186 -0
- package/dist/model-recommender.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -1
- package/dist/registry.js.map +1 -1
- package/dist/statusline.d.ts +10 -2
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +122 -36
- package/dist/statusline.js.map +1 -1
- package/dist/trace-bundle.d.ts +209 -0
- package/dist/trace-bundle.d.ts.map +1 -0
- package/dist/trace-bundle.js +601 -0
- package/dist/trace-bundle.js.map +1 -0
- package/dist/usage.d.ts +7 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +30 -2
- package/dist/usage.js.map +1 -1
- package/package.json +5 -5
- package/sbom.json +255 -55
- package/src/backlog.ts +176 -3
- package/src/feature-adr-checkpoints.ts +103 -4
- package/src/index.ts +4 -1
- package/src/loop-blobs.generated.ts +9 -9
- package/src/loop-plan-graph.ts +132 -0
- package/src/loop-plan.ts +13 -15
- package/src/loop-render.ts +93 -17
- package/src/loop-trace.ts +78 -1
- package/src/model-recommender.ts +228 -0
- package/src/registry.ts +4 -1
- package/src/statusline.ts +117 -30
- package/src/trace-bundle.ts +743 -0
- package/src/usage.ts +42 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loop-plan-graph (idea d25a3c8a) — the COMPLETENESS leg of loop-plan/1's closed-world checking.
|
|
3
|
+
*
|
|
4
|
+
* What existed before this module (the round-7 cross-family reviewer's ONE not-met bar item,
|
|
5
|
+
* SIGNOFF's "B-not-A reason 1"): `KNOWN_KEYS === INJECT` and the honesty test's `SCANNED` roster
|
|
6
|
+
* all compare artifacts DOWNSTREAM of FIELD_DOMAINS — equality proves the rosters are consistent
|
|
7
|
+
* with each other, never that they are COMPLETE against the interface source. The reviewer's
|
|
8
|
+
* constructive counterexample: declare `LoopStep.extra?: ExtraPolicy`, add only the parent
|
|
9
|
+
* `{t:'record'}` domain entry, and `extra: { enabeld: true }` escapes every check while every
|
|
10
|
+
* equality guard stays green — "a new record kind cannot escape is unproven and demonstrably
|
|
11
|
+
* false" (verbatim). The shipped mitigation was a documented four-step extension discipline — a
|
|
12
|
+
* layer-4 instruction, exactly the layer the cost-of-detection ladder says such a check must not
|
|
13
|
+
* live on.
|
|
14
|
+
*
|
|
15
|
+
* THE FIX (this module, layer 1): walk the interface graph from `LoopPlan` in the SOURCE TEXT,
|
|
16
|
+
* transitively collect every reachable named interface, and let the honesty test require that the
|
|
17
|
+
* reachable set is exactly the wired set. An interface reachable from LoopPlan but absent from the
|
|
18
|
+
* wiring fails BY CONSTRUCTION, naming itself — no memory, no discipline, no fourth manual step.
|
|
19
|
+
*
|
|
20
|
+
* PURE: operates on source text handed in by the caller; no fs, no clock. That is what lets the
|
|
21
|
+
* acceptance test run the reviewer's counterexample against a SABOTAGED COPY of the source and
|
|
22
|
+
* require a red, while the real source stays green.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** One parsed field: its name and the DECLARED interface names its type text references. */
|
|
26
|
+
export interface GraphField {
|
|
27
|
+
readonly field: string;
|
|
28
|
+
readonly refs: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** interface name → its fields (index signatures like `[xKey: \`x-${string}\`]` are excluded:
|
|
32
|
+
* they open no named-interface edge and are the extension escape hatch by design). */
|
|
33
|
+
export type InterfaceGraph = ReadonlyMap<string, readonly GraphField[]>;
|
|
34
|
+
|
|
35
|
+
/** Brace-matched interface extraction. A regex-only scan truncates at the first nested brace
|
|
36
|
+
* (inline object fields are everywhere in this file), so bodies are cut by depth counting. */
|
|
37
|
+
export function parseInterfaceGraph(source: string): InterfaceGraph {
|
|
38
|
+
const names = new Set<string>();
|
|
39
|
+
const headRe = /(?:^|\n)\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/g;
|
|
40
|
+
for (let m = headRe.exec(source); m !== null; m = headRe.exec(source)) names.add(m[1]!);
|
|
41
|
+
|
|
42
|
+
const graph = new Map<string, GraphField[]>();
|
|
43
|
+
headRe.lastIndex = 0;
|
|
44
|
+
for (let m = headRe.exec(source); m !== null; m = headRe.exec(source)) {
|
|
45
|
+
const name = m[1]!;
|
|
46
|
+
const open = source.indexOf('{', m.index + m[0].length);
|
|
47
|
+
if (open === -1) continue;
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let close = -1;
|
|
50
|
+
for (let i = open; i < source.length; i += 1) {
|
|
51
|
+
const ch = source[i];
|
|
52
|
+
if (ch === '{') depth += 1;
|
|
53
|
+
else if (ch === '}') {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0) { close = i; break; }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (close === -1) continue;
|
|
59
|
+
const body = source.slice(open + 1, close);
|
|
60
|
+
|
|
61
|
+
// Split the body into top-level entries at depth 0 (`;` inside an inline `{...}` must not cut).
|
|
62
|
+
const entries: string[] = [];
|
|
63
|
+
let entry = '';
|
|
64
|
+
let d = 0;
|
|
65
|
+
for (const ch of body) {
|
|
66
|
+
if (ch === '{' || ch === '(' || ch === '<' || ch === '[') d += 1;
|
|
67
|
+
else if (ch === '}' || ch === ')' || ch === '>' || ch === ']') d -= 1;
|
|
68
|
+
if (ch === ';' && d === 0) { entries.push(entry); entry = ''; continue; }
|
|
69
|
+
entry += ch;
|
|
70
|
+
}
|
|
71
|
+
if (entry.trim() !== '') entries.push(entry);
|
|
72
|
+
|
|
73
|
+
const fields: GraphField[] = [];
|
|
74
|
+
for (const raw of entries) {
|
|
75
|
+
// strip comments, then match `readonly? name?: TYPE`
|
|
76
|
+
const text = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '').trim();
|
|
77
|
+
if (text === '' || text.startsWith('[')) continue; // index signature — by-design escape hatch
|
|
78
|
+
const fm = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*\??:\s*([\s\S]+)$/.exec(text);
|
|
79
|
+
if (fm === null) continue;
|
|
80
|
+
const typeText = fm[2]!;
|
|
81
|
+
const refs = new Set<string>();
|
|
82
|
+
const idRe = /[A-Za-z_$][\w$]*/g;
|
|
83
|
+
for (let im = idRe.exec(typeText); im !== null; im = idRe.exec(typeText)) {
|
|
84
|
+
if (names.has(im[0]) && im[0] !== name) refs.add(im[0]);
|
|
85
|
+
}
|
|
86
|
+
fields.push({ field: fm[1]!, refs: [...refs] });
|
|
87
|
+
}
|
|
88
|
+
graph.set(name, fields);
|
|
89
|
+
}
|
|
90
|
+
return graph;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Every interface reachable from `root` (inclusive), via any field's declared-interface refs —
|
|
94
|
+
* arrays, unions and nullables all count: `LoopStep[]`, `RetryProfile | null` open the same edge. */
|
|
95
|
+
export function reachableInterfaces(graph: InterfaceGraph, root: string): string[] {
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const queue = [root];
|
|
98
|
+
while (queue.length > 0) {
|
|
99
|
+
const name = queue.shift()!;
|
|
100
|
+
if (seen.has(name) || !graph.has(name)) continue;
|
|
101
|
+
seen.add(name);
|
|
102
|
+
for (const f of graph.get(name)!) for (const ref of f.refs) if (!seen.has(ref)) queue.push(ref);
|
|
103
|
+
}
|
|
104
|
+
return [...seen].sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface GraphWiringReport {
|
|
108
|
+
readonly ok: boolean;
|
|
109
|
+
/** Reachable from the root but NOT in the wired roster — each one is exactly the reviewer's
|
|
110
|
+
* counterexample: a record kind whose key space is open while every equality guard stays green. */
|
|
111
|
+
readonly unwired: string[];
|
|
112
|
+
readonly reachable: string[];
|
|
113
|
+
/** Wired but no longer reachable — a stale roster entry (the reverse rot). */
|
|
114
|
+
readonly stale: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The completeness check the equality guards could not perform: reachable(source) vs wired. */
|
|
118
|
+
export function checkGraphWiring(source: string, wired: readonly string[], root = 'LoopPlan'): GraphWiringReport {
|
|
119
|
+
const graph = parseInterfaceGraph(source);
|
|
120
|
+
const reachable = reachableInterfaces(graph, root);
|
|
121
|
+
const wiredSet = new Set(wired);
|
|
122
|
+
const reachableSet = new Set(reachable);
|
|
123
|
+
const unwired = reachable.filter((n) => !wiredSet.has(n));
|
|
124
|
+
// The wired roster (KNOWN_KEYS) legitimately mixes interface names with INLINE-record FIELD names
|
|
125
|
+
// (`artifacts`, `budget`, `checkpointing`, …) — those are the inlineSubFields machinery's
|
|
126
|
+
// business, not this check's. Staleness is judged only for entries that ARE declared interfaces
|
|
127
|
+
// in this source: a declared-but-unreachable interface in the roster is real rot; an inline field
|
|
128
|
+
// name is not an interface and must not be reported as one (caught on the first live run: five
|
|
129
|
+
// false stale entries, all inline fields).
|
|
130
|
+
const stale = [...wiredSet].filter((n) => graph.has(n) && !reachableSet.has(n)).sort();
|
|
131
|
+
return { ok: unwired.length === 0 && stale.length === 0, unwired, reachable, stale };
|
|
132
|
+
}
|
package/src/loop-plan.ts
CHANGED
|
@@ -389,22 +389,20 @@ export const FIELD_DOMAINS: Record<string, FieldDomain> = {
|
|
|
389
389
|
// domain entry fails the honesty test; adding it WITH one makes it known here automatically. There
|
|
390
390
|
// is exactly one roster, and it is the source's.
|
|
391
391
|
//
|
|
392
|
-
// WHAT THIS
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// `extra: { enabeld: true }` escapes closed-world checking WHILE the equality assertions stay green.
|
|
403
|
-
// That is a future-extension / proof-maintenance hole, not an input bypass that works today.
|
|
392
|
+
// WHAT THIS PROVES (updated 2026-08-17, idea d25a3c8a — the round-7 not-met bar item is now MET).
|
|
393
|
+
// PROVEN, and tested: every record path wired here is closed, the accepted roster is the source's,
|
|
394
|
+
// AND the roster is COMPLETE against the interface graph: `loop-plan-graph.ts` walks the interfaces
|
|
395
|
+
// reachable from LoopPlan in this file's SOURCE and the honesty test requires reachable == wired ==
|
|
396
|
+
// SCANNED. The reviewer's constructive counterexample (`LoopStep.extra?: ExtraPolicy` with a
|
|
397
|
+
// parent-only domain entry) is the ACCEPTANCE TEST: it goes red naming ExtraPolicy, by
|
|
398
|
+
// construction, before any hand step. Still out of scope, said plainly: interfaces referenced only
|
|
399
|
+
// through type ALIASES are followed one identifier deep (the graph collects declared-interface
|
|
400
|
+
// names from the field's type text); an alias chain that hides an interface behind a non-interface
|
|
401
|
+
// alias would need the alias declared in this file to be walked.
|
|
404
402
|
//
|
|
405
|
-
// EXTENSION
|
|
406
|
-
//
|
|
407
|
-
//
|
|
403
|
+
// EXTENSION CONVENIENCE (no longer load-bearing — the graph-completeness test reddens on a missed
|
|
404
|
+
// step by construction; this list just tells you what the red means). When you add a NEW nested
|
|
405
|
+
// record-typed field: 1) add `<NewIface>.<field>` entries to FIELD_DOMAINS for every field of the new interface
|
|
408
406
|
// (not just the `{t:'record'}` entry on its PARENT); 2) add the new interface to the honesty test's
|
|
409
407
|
// `SCANNED`; 3) add an `INJECT` site for it in the closed-world fuzz; 4) descend into it in
|
|
410
408
|
// `checkKeys`. A structural fix that derives 2–4 from the interface graph — so a new record kind is
|
package/src/loop-render.ts
CHANGED
|
@@ -149,6 +149,7 @@ function landedBarrier(id: string, phase: string, writes: string[], pad: string)
|
|
|
149
149
|
`${pad} const probeCmd = 'cd ' + shqRt(TRACE_DIR === null ? '.' : TRACE_DIR) + ${jsString(' && ' + testExpr + ' && echo LANDED || echo NOT-LANDED')}`,
|
|
150
150
|
`${pad} let landed = false`,
|
|
151
151
|
`${pad} for (let p = 0; p < 5 && !landed; p++) {`,
|
|
152
|
+
`${pad} __agentCalls++`,
|
|
152
153
|
`${pad} const probe = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + probeCmd, { label: ${jsString('landed:' + id)}, phase: ${jsString(phase)}, effort: 'low' }) // loop-lint: infra-agent`,
|
|
153
154
|
`${pad} landed = typeof probe === 'string' && probe.indexOf('NOT-LANDED') === -1 && probe.indexOf('LANDED') !== -1`,
|
|
154
155
|
`${pad} }`,
|
|
@@ -283,8 +284,8 @@ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
|
|
|
283
284
|
.map((d) => `r_${ident(d)}`);
|
|
284
285
|
const hashParts = `[P_${ident(id)}${depVars.length > 0 ? ', ' + depVars.join(', ') : ''}]`;
|
|
285
286
|
const artifactRel = writes.length > 0 ? JSON.stringify(writes) : 'null';
|
|
286
|
-
const tpLine = (pad: string): string =>
|
|
287
|
-
`${pad}await __tpCapture(${jsString(id)}, ${jsString(s.phase)}, P_${ident(id)}, r_${ident(id)}, ${typeof s.model === 'string' && s.model !== '' ? jsString(s.model) : 'null'})`;
|
|
287
|
+
const tpLine = (pad: string, resumed: boolean): string =>
|
|
288
|
+
`${pad}await __tpCapture(${jsString(id)}, ${jsString(s.phase)}, P_${ident(id)}, r_${ident(id)}, ${typeof s.model === 'string' && s.model !== '' ? jsString(s.model) : 'null'}, ${resumed ? 'true' : 'false'})`;
|
|
288
289
|
|
|
289
290
|
// ROUND-6 B3 SHAPE: `let r_x` + (optional) dispatch-fn declaration form the await-free
|
|
290
291
|
// preamble; then exactly ONE settle-routed try wraps EVERY await this step performs —
|
|
@@ -303,13 +304,14 @@ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
|
|
|
303
304
|
lines.push(` if (__live !== true && __ckptResume(${jsString(id)}, __h, ${artifactRel})) {`);
|
|
304
305
|
lines.push(` r_${ident(id)} = __ckptEntries[${jsString(id)}].result`);
|
|
305
306
|
lines.push(` log(${jsString(`checkpoint: step ${id} RESUMED (fingerprint+artifact match) — dispatch skipped`)})`);
|
|
307
|
+
if (tp) lines.push(tpLine(' ', true));
|
|
306
308
|
lines.push(` return r_${ident(id)}`);
|
|
307
309
|
lines.push(` }`);
|
|
308
310
|
}
|
|
309
311
|
lines.push(` r_${ident(id)} = await ${runExpr}`);
|
|
310
312
|
if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
|
|
311
313
|
if (ckpt) lines.push(` await __ckptAppend(${jsString(id)}, ${jsString(s.phase)}, __h, r_${ident(id)})`);
|
|
312
|
-
if (tp) lines.push(tpLine(' '));
|
|
314
|
+
if (tp) lines.push(tpLine(' ', false));
|
|
313
315
|
lines.push(` return r_${ident(id)}`);
|
|
314
316
|
lines.push(`}`);
|
|
315
317
|
}
|
|
@@ -320,16 +322,17 @@ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
|
|
|
320
322
|
lines.push(` if (__ckptResume(${jsString(id)}, __h_${ident(id)}, ${artifactRel})) {`);
|
|
321
323
|
lines.push(` r_${ident(id)} = __ckptEntries[${jsString(id)}].result`);
|
|
322
324
|
lines.push(` log(${jsString(`checkpoint: step ${id} RESUMED (fingerprint+artifact match) — dispatch skipped`)})`);
|
|
325
|
+
if (tp) lines.push(tpLine(' ', true));
|
|
323
326
|
lines.push(` } else {`);
|
|
324
327
|
lines.push(` r_${ident(id)} = await ${runExpr}`);
|
|
325
328
|
if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
|
|
326
329
|
lines.push(` await __ckptAppend(${jsString(id)}, ${jsString(s.phase)}, __h_${ident(id)}, r_${ident(id)})`);
|
|
327
|
-
if (tp) lines.push(tpLine(' '));
|
|
330
|
+
if (tp) lines.push(tpLine(' ', false));
|
|
328
331
|
lines.push(` }`);
|
|
329
332
|
} else {
|
|
330
333
|
lines.push(` r_${ident(id)} = await ${runExpr}`);
|
|
331
334
|
if (writes.length > 0) lines.push(...landedBarrier(id, s.phase, writes, ' '));
|
|
332
|
-
if (tp) lines.push(tpLine(' '));
|
|
335
|
+
if (tp) lines.push(tpLine(' ', false));
|
|
333
336
|
}
|
|
334
337
|
} else {
|
|
335
338
|
lines.push(` await __dispatch_${ident(id)}()`);
|
|
@@ -353,6 +356,7 @@ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
|
|
|
353
356
|
if (routeIsTerminal) {
|
|
354
357
|
lines.push(` if (__v_${ident(id)} !== 'pass') {`);
|
|
355
358
|
lines.push(` // typed terminal failure route (plan gates[].failRoute) — a NAMED phase, never a silent pass; settled durably through the single exit (round-5 B3)`);
|
|
359
|
+
lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString((route as string).slice('terminal:'.length))})`);
|
|
356
360
|
lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(route as string)}, gate: ${jsString(id)}, verdict: __v_${ident(id)} } })`);
|
|
357
361
|
lines.push(` }`);
|
|
358
362
|
} else {
|
|
@@ -373,8 +377,10 @@ function renderStep(v: StepPlanView, plan: LoopPlan, env: RenderEnv): string {
|
|
|
373
377
|
if (pause?.payloadSchema !== undefined) {
|
|
374
378
|
// enacts pauses[].payloadSchema: the pause return CARRIES the declared payload shape, so the
|
|
375
379
|
// re-invoking caller sees what the resume arg must contain.
|
|
380
|
+
lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString(pauseState)})`);
|
|
376
381
|
lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(pauseState)}, resumeArg: ${jsString(resumeArg)}, payloadSchema: ${JSON.stringify(pause.payloadSchema)} } })`);
|
|
377
382
|
} else {
|
|
383
|
+
lines.push(` await __ledgerAppend(${jsString(s.phase)}, ${jsString(pauseState)})`);
|
|
378
384
|
lines.push(` return await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(s.phase)}, outcome: 'terminal', value: { phase: ${jsString(pauseState)}, resumeArg: ${jsString(resumeArg)} } })`);
|
|
379
385
|
}
|
|
380
386
|
lines.push(`}`);
|
|
@@ -481,8 +487,17 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
481
487
|
lines.push(`const RUN_ID = (typeof A.runId === 'string' && /^[a-z0-9-]{1,40}$/.test(A.runId)) ? A.runId : 'run-1'`);
|
|
482
488
|
lines.push(`const TRACE_DIR = (typeof A.traceDir === 'string' && A.traceDir.charAt(0) === '/') ? A.traceDir.replace(/\\/+$/, '') : null`);
|
|
483
489
|
lines.push(`const TRACE_FILE = TRACE_DIR === null ? null : TRACE_DIR + '/trace.jsonl'`);
|
|
490
|
+
lines.push(`const REPO_DIR = (typeof A.repo === 'string' && A.repo.charAt(0) === '/') ? A.repo.replace(/\\/+$/, '') : null`);
|
|
491
|
+
lines.push(`const DZ_BIN = (typeof A.dz === 'string' && A.dz !== '') ? A.dz : 'dz'`);
|
|
492
|
+
lines.push(`const LOOP_SLUG = ${jsString(plan.name)}`);
|
|
484
493
|
lines.push(`// budget guard — spent BEFORE every spawn; retries consume budget (lint: budget-before-spawn)`);
|
|
485
494
|
lines.push(`const __budget = { left: ${budgetTotal} }`);
|
|
495
|
+
lines.push(`// Total agent invocations this run made — model dispatches AND infra agents. The ledger's`);
|
|
496
|
+
lines.push(`// \`agents\` column means agent_count from the completion notification (ALL subagents), so the`);
|
|
497
|
+
lines.push(`// automated row must count every dispatch, never the trace's model-dispatch subset (QE F1).`);
|
|
498
|
+
lines.push(`let __agentCalls = 0`);
|
|
499
|
+
lines.push(`let __ledgerDone = false`);
|
|
500
|
+
if (traceOn) lines.push(`let __faLegWarned = false`);
|
|
486
501
|
lines.push(`function __spendBudget(stepId) { if (__budget.left <= 0) { throw new Error('loop budget exhausted before ' + stepId) } __budget.left-- }`);
|
|
487
502
|
lines.push(`const __hooks = { onDispatch: null, onSettle: null }`);
|
|
488
503
|
lines.push(`const __settled = {}`);
|
|
@@ -580,7 +595,11 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
580
595
|
lines.push(`// SECONDARY event; it never replaces the primary outcome — success included (the ha-consilium`);
|
|
581
596
|
lines.push(`// totality lesson at the flush layer).`);
|
|
582
597
|
lines.push(`async function __settleStep(o) {`);
|
|
583
|
-
lines.push(` try { await __traceFlushNow(o.phase) } catch (_fe) { log('settle flush for ' + o.stepId + ' threw: ' + __errText(_fe) + ' — primary outcome preserved') }`);
|
|
598
|
+
lines.push(` try { await __traceFlushNow(o.phase, o.stepId) } catch (_fe) { log('settle flush for ' + o.stepId + ' threw: ' + __errText(_fe) + ' — primary outcome preserved') }`);
|
|
599
|
+
if (plan.subsystems?.trainingPairs === true) {
|
|
600
|
+
lines.push(` // The ONE producer of the captureFailures channel on terminal values — the four terminal call sites never carry the key, so future routes inherit it.`);
|
|
601
|
+
lines.push(` if (o.outcome === 'terminal' && o.value !== null && typeof o.value === 'object') { o.value.captureFailures = __captureFailures }`);
|
|
602
|
+
}
|
|
584
603
|
lines.push(` if (o.outcome === 'failed') { throw o.error }`);
|
|
585
604
|
lines.push(` return o.value`);
|
|
586
605
|
lines.push(`}`);
|
|
@@ -588,7 +607,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
588
607
|
lines.push(`// secondary event, never a replaced outcome (the naked await __traceFlushNow at phase`);
|
|
589
608
|
lines.push(`// boundaries was the round-5 success-replacement hole).`);
|
|
590
609
|
lines.push(`async function __phaseFlush(phaseName) {`);
|
|
591
|
-
lines.push(` try { await __traceFlushNow(phaseName) } catch (_fe) { log('phase flush threw: ' + __errText(_fe) + ' — outcome preserved (flush failure is secondary)') }`);
|
|
610
|
+
lines.push(` try { await __traceFlushNow(phaseName, null) } catch (_fe) { log('phase flush threw: ' + __errText(_fe) + ' — outcome preserved (flush failure is secondary)') }`);
|
|
592
611
|
lines.push(`}`);
|
|
593
612
|
lines.push(`// join failures route through the single exit too (joinRegion throws; the wrapper settles)`);
|
|
594
613
|
lines.push(`async function __joinSettled(joinStepId, phaseName, results, o) {`);
|
|
@@ -620,6 +639,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
620
639
|
lines.push(` let value = null`);
|
|
621
640
|
lines.push(` let outcome = 'ok'`);
|
|
622
641
|
lines.push(` try {`);
|
|
642
|
+
lines.push(` __agentCalls++`);
|
|
623
643
|
lines.push(` value = await thunk()`);
|
|
624
644
|
lines.push(` if (value === null || value === undefined) outcome = 'null'`);
|
|
625
645
|
lines.push(` } catch (err) {`);
|
|
@@ -706,15 +726,36 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
706
726
|
lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP)`);
|
|
707
727
|
lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
|
|
708
728
|
lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
|
|
709
|
-
lines.push(`async function __traceFlushNow(phaseName) {`);
|
|
729
|
+
lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
|
|
710
730
|
lines.push(` if (TRACE_FILE === null) { return }`);
|
|
711
|
-
lines.push(`
|
|
731
|
+
lines.push(` // cmd must be let: the trace payload stays LEFT and must never be replaced by the fa-record panel leg; both ride the SAME writer agent.`);
|
|
732
|
+
lines.push(` let cmd = traceFlushCmd(__traceState, TRACE_FILE)`);
|
|
712
733
|
lines.push(` if (cmd === null) { return }`);
|
|
734
|
+
lines.push(` const fa = traceFaRecordCmd(DZ_BIN, LOOP_SLUG, (typeof stepLabel === 'string' && stepLabel !== '') ? stepLabel : phaseName, REPO_DIR)`);
|
|
735
|
+
lines.push(` if (fa !== null) { cmd = cmd + ' && { ' + fa + ' || true; }' }`);
|
|
736
|
+
lines.push(` else if (REPO_DIR === null && !__faLegWarned) { __faLegWarned = true; log('fa-record leg skipped — the live panel was not updated because no args.repo was given (the trace flush still runs)') }`);
|
|
713
737
|
lines.push(` // the flush agent is infra, not a step (it would otherwise recurse) // loop-lint: infra-agent`);
|
|
738
|
+
lines.push(` __agentCalls++`);
|
|
714
739
|
lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'trace:flush', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
715
740
|
lines.push(`}`);
|
|
741
|
+
lines.push(`async function __ledgerAppend(phaseName, outcome) {`);
|
|
742
|
+
lines.push(` if (__ledgerDone) { return } __ledgerDone = true`);
|
|
743
|
+
lines.push(` // Ledger telemetry is SECONDARY: this whole body is total and can never fail the run.`);
|
|
744
|
+
lines.push(` try {`);
|
|
745
|
+
lines.push(` if (REPO_DIR === null) { log('ledger:append skipped — ledger row was not written because no args.repo was given'); return }`);
|
|
746
|
+
lines.push(` // + 1 is THIS ledger writer, which is about to be invoked and not yet counted.`);
|
|
747
|
+
lines.push(` const line = traceLedgerLine({ slug: LOOP_SLUG, runId: RUN_ID, planDigest: PLAN_DIGEST, agents: __agentCalls + 1, outcome: outcome, date: A.date })`);
|
|
748
|
+
lines.push(` if (line === null) { log('ledger:append skipped — traceLedgerLine returned null'); return }`);
|
|
749
|
+
lines.push(` const cmd = traceLedgerAppendCmd(REPO_DIR, line)`);
|
|
750
|
+
lines.push(` if (cmd === null) { log('ledger:append skipped — traceLedgerAppendCmd returned null'); return }`);
|
|
751
|
+
lines.push(` __agentCalls++`);
|
|
752
|
+
lines.push(` const reply = await agent('Run EXACTLY this one shell command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'ledger:append', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
753
|
+
lines.push(` if (!/LEDGER-OK/.test(String(reply))) { log('ledger:append UNVERIFIED — ledger row write was not confirmed; run continues') }`);
|
|
754
|
+
lines.push(` } catch (_le) { log('ledger:append failed as a SECONDARY event: ' + __errText(_le) + ' — run continues') }`);
|
|
755
|
+
lines.push(`}`);
|
|
716
756
|
} else {
|
|
717
|
-
lines.push(`async function __traceFlushNow(phaseName) { /* trace.emit=false — no trace plane; fitness-suite verification is NOT claimable for this loop */ }`);
|
|
757
|
+
lines.push(`async function __traceFlushNow(phaseName, stepLabel) { /* trace.emit=false — no trace plane; fitness-suite verification is NOT claimable for this loop */ }`);
|
|
758
|
+
lines.push(`async function __ledgerAppend(phaseName, outcome) { /* trace off — no agents counted, no ledger row */ }`);
|
|
718
759
|
}
|
|
719
760
|
if (ckptOn) {
|
|
720
761
|
lines.push(`// checkpoint wiring (blob-provided pure half; the read/write agents are infra) — the resume`);
|
|
@@ -738,6 +779,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
738
779
|
lines.push(` if (CKPT_DIR === null) { log('checkpointing enabled but no traceDir given — running LIVE; nothing resumes, nothing persists (named, never silent)'); return }`);
|
|
739
780
|
lines.push(` try {`);
|
|
740
781
|
lines.push(` const cmd = checkpointReadCmd(TRACE_DIR)`);
|
|
782
|
+
lines.push(` __agentCalls++`);
|
|
741
783
|
lines.push(` const out = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + cmd, { label: 'ckpt:read', phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
742
784
|
lines.push(` const parsed = parseCheckpointRead(typeof out === 'string' ? out : '')`);
|
|
743
785
|
lines.push(` __ckptEntries = parsed.entries`);
|
|
@@ -760,6 +802,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
760
802
|
lines.push(` if (line === null) { log('checkpoint: ' + stage + ' not persisted (null/oversize/unserializable — named, never silent)'); return }`);
|
|
761
803
|
lines.push(` try {`);
|
|
762
804
|
lines.push(` const cmd = checkpointAppendCmd(TRACE_DIR, line)`);
|
|
805
|
+
lines.push(` __agentCalls++`);
|
|
763
806
|
lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'ckpt:write:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
764
807
|
lines.push(` } catch (_ce) { log('checkpoint append for ' + stage + ' threw: ' + __errText(_ce) + ' — run continues (the step outcome stands; the next run re-runs this step)') }`);
|
|
765
808
|
lines.push(`}`);
|
|
@@ -776,15 +819,41 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
776
819
|
lines.push(`// SUCCESSFUL step (the step's own catch settled it as failed). The whole capture — pair`);
|
|
777
820
|
lines.push(`// construction, serialization and write — now rides ONE catch, the same discipline as`);
|
|
778
821
|
lines.push(`// __errText/__phaseFlush: a capture failure is a SECONDARY logged event, never an outcome.`);
|
|
779
|
-
lines.push(`
|
|
822
|
+
lines.push(`const __captureFailures = []`);
|
|
823
|
+
lines.push(`async function __tpCapture(stage, phaseName, input, output, model, resumed) {`);
|
|
780
824
|
lines.push(` if (TRACE_DIR === null) { return }`);
|
|
825
|
+
lines.push(` let __captureMode = null`);
|
|
781
826
|
lines.push(` try {`);
|
|
782
|
-
lines.push(`
|
|
827
|
+
lines.push(` // enabled is true because this entire wiring block is gated at render time by the subsystem opt-in.`);
|
|
828
|
+
lines.push(` const recordCount = output === null || output === undefined ? 0 : 1`);
|
|
829
|
+
lines.push(` const mode = decideCaptureMode({ enabled: true, resumed: resumed === true, recordCount: recordCount })`);
|
|
830
|
+
lines.push(` __captureMode = mode`);
|
|
831
|
+
lines.push(` if (mode === 'skip-disabled') { return }`);
|
|
832
|
+
lines.push(` if (mode === 'skip-empty') { log('training-pair: ' + stage + ' not captured (null/undefined output — named, never silent)'); __captureFailures.push(captureFailureRecord(stage, mode, 'empty-output', null)); return }`);
|
|
833
|
+
lines.push(` const pair = buildTrainingPair({ slug: RUN_ID, stage: stage, ts: null, input: input, output: output, evaluation: null, provenance: { model: model === null ? 'unknown' : model, role: stage }, captureMode: mode === 'backfill' ? 'backfill' : 'capture', resumed: resumed === true })`);
|
|
783
834
|
lines.push(` const line = serializeTrainingPair(pair)`);
|
|
784
|
-
lines.push(` if (line === null) { log('training-pair: ' + stage + ' not captured (unserializable) — named, never silent'); return }`);
|
|
785
|
-
lines.push(`
|
|
786
|
-
lines.push(`
|
|
787
|
-
lines.push(`
|
|
835
|
+
lines.push(` if (line === null) { log('training-pair: ' + stage + ' not captured (unserializable) — named, never silent'); __captureFailures.push(captureFailureRecord(stage, mode, 'unserializable', null)); return }`);
|
|
836
|
+
lines.push(` if (mode === 'capture') {`);
|
|
837
|
+
lines.push(` const cmd = trainingPairAppendCmd(TRACE_DIR, RUN_ID, stage, line)`);
|
|
838
|
+
lines.push(` __agentCalls++`);
|
|
839
|
+
lines.push(` await agent('Run EXACTLY this one shell command via your Bash tool and reply with only OK: ' + cmd, { label: 'tp:write:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
840
|
+
lines.push(` return`);
|
|
841
|
+
lines.push(` }`);
|
|
842
|
+
lines.push(` if (mode === 'backfill') {`);
|
|
843
|
+
lines.push(` // Exclude pair.slug (RUN_ID) and pair.ts (null) from the mark key: normalized input/output`);
|
|
844
|
+
lines.push(` // identify the pair across runIds, which is the cross-run at-most-once property.`);
|
|
845
|
+
lines.push(` const markKey = fnv1a64(stage + '\\0' + pair.input + '\\0' + pair.output)`);
|
|
846
|
+
lines.push(` const cmd = trainingPairBackfillCmd(TRACE_DIR, RUN_ID, stage, [line], markKey)`);
|
|
847
|
+
lines.push(` __agentCalls++`);
|
|
848
|
+
lines.push(` const readback = await agent('Run EXACTLY this one shell command via your Bash tool and reply with ONLY its raw stdout: ' + cmd, { label: 'tp:backfill:' + stage, phase: phaseName, effort: 'low' }) // loop-lint: infra-agent`);
|
|
849
|
+
lines.push(` const status = typeof readback === 'string' ? readback.trim() : ''`);
|
|
850
|
+
lines.push(` if (status === TP_BACKFILL_OK) { log('training-pair: ' + stage + ' backfilled from the checkpoint'); return }`);
|
|
851
|
+
lines.push(` if (status === TP_BACKFILL_SKIP) { log('training-pair: ' + stage + ' pair file already existed; nothing written'); return }`);
|
|
852
|
+
lines.push(` if (status === TP_BACKFILL_DUP) { log('training-pair: ' + stage + ' another run already captured this pair; nothing written'); return }`);
|
|
853
|
+
lines.push(` log('training-pair: ' + stage + ' checkpoint backfill UNVERIFIED: ' + __errText(readback))`);
|
|
854
|
+
lines.push(` __captureFailures.push(captureFailureRecord(stage, mode, 'backfill-unverified', readback))`);
|
|
855
|
+
lines.push(` }`);
|
|
856
|
+
lines.push(` } catch (_ce) { log('training-pair capture for ' + stage + ' threw: ' + __errText(_ce) + ' — run continues (capture is never load-bearing)'); __captureFailures.push(captureFailureRecord(stage, __captureMode, 'threw', __errText(_ce))) }`);
|
|
788
857
|
lines.push(`}`);
|
|
789
858
|
}
|
|
790
859
|
if (plan.steps.some((s) => s.kind === 'gate')) {
|
|
@@ -966,14 +1035,17 @@ export function renderPlan(plan: LoopPlan): RenderResult {
|
|
|
966
1035
|
|
|
967
1036
|
const header = `// ── LOOP-PLAN plan=loop-plan/1 digest=sha256:${digest} exec-fp=sha256:${execFp} generator=${LOOP_RENDER_GENERATOR} ──`;
|
|
968
1037
|
const runtime = renderRuntime(norm, digest, execFp, blobs);
|
|
1038
|
+
const completedValue = `{ phase: 'COMPLETED', runId: RUN_ID, planDigest: PLAN_DIGEST, execFp: EXEC_FP }`;
|
|
969
1039
|
|
|
970
1040
|
const ending = [
|
|
971
1041
|
G('epilogue'),
|
|
972
1042
|
`traceCloseIfOn()`,
|
|
973
1043
|
`function traceCloseIfOn() { ${norm.trace?.emit === true ? 'traceClose(__traceState)' : '/* trace off */'} }`,
|
|
1044
|
+
`await __phaseFlush(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')})`,
|
|
1045
|
+
`await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'completed')`,
|
|
974
1046
|
`// the COMPLETED return rides the single exit too (round-5 B3): the epilogue flush happens inside`,
|
|
975
1047
|
`// __settleStep, so a flush rejection is a logged secondary event, never a replaced COMPLETED.`,
|
|
976
|
-
`return await __settleStep({ stepId: '__epilogue__', phase: ${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, outcome: 'terminal', value: {
|
|
1048
|
+
`return await __settleStep({ stepId: '__epilogue__', phase: ${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, outcome: 'terminal', value: ${completedValue} })`,
|
|
977
1049
|
GE('epilogue'),
|
|
978
1050
|
].join('\n');
|
|
979
1051
|
|
|
@@ -982,8 +1054,12 @@ export function renderPlan(plan: LoopPlan): RenderResult {
|
|
|
982
1054
|
header,
|
|
983
1055
|
...blobChunks,
|
|
984
1056
|
runtime,
|
|
1057
|
+
`try {`,
|
|
985
1058
|
...stepChunks,
|
|
986
1059
|
ending,
|
|
1060
|
+
norm.subsystems?.trainingPairs === true
|
|
1061
|
+
? `} catch (__runErr) { await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'failed'); if (__captureFailures.length > 0) { log('training-pair capture failures this run: ' + __captureFailures.length + ' — ' + __captureFailures.map(function (f) { return f.stage + ':' + f.reason }).join(', ')) } throw __runErr }`
|
|
1062
|
+
: `} catch (__runErr) { await __ledgerAppend(${jsString(phaseOrder[phaseOrder.length - 1] ?? 'End')}, 'failed'); throw __runErr }`,
|
|
987
1063
|
'',
|
|
988
1064
|
].join('\n\n');
|
|
989
1065
|
|
package/src/loop-trace.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import type { TraceProjection } from './loop-plan.js';
|
|
27
27
|
|
|
28
28
|
/** Blob version stamp for the emitter half (read by scripts/gen-loop-blobs.mjs). */
|
|
29
|
-
export const LOOP_TRACE_BLOB_VERSION = '1.
|
|
29
|
+
export const LOOP_TRACE_BLOB_VERSION = '1.1.0';
|
|
30
30
|
|
|
31
31
|
export const LOOP_TRACE_SCHEMA_VERSION = 1;
|
|
32
32
|
|
|
@@ -211,6 +211,83 @@ export function traceFlushCmd(state: TraceState, traceFileAbs: string): string |
|
|
|
211
211
|
return 'mkdir -p ' + dir + ' && ' + printfs;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Build the feature-ADR live-panel telemetry leg. Totality comes from the caller's grouped splice:
|
|
216
|
+
* returning the bare command lets that splice preserve the trace flush's exit status while
|
|
217
|
+
* swallowing only the panel leg's failure. The `loop` producer marker stops a generated loop's
|
|
218
|
+
* high-frequency zero counters from displacing a live `/feature-adr` run's meaningful panel.
|
|
219
|
+
*/
|
|
220
|
+
export function traceFaRecordCmd(dzBin: unknown, slug: unknown, stepLabel: unknown, projectAbs: unknown): string | null {
|
|
221
|
+
if (typeof slug !== 'string' || slug === ''
|
|
222
|
+
|| typeof stepLabel !== 'string' || stepLabel === ''
|
|
223
|
+
|| typeof projectAbs !== 'string' || projectAbs === '') return null;
|
|
224
|
+
const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';
|
|
225
|
+
const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)
|
|
226
|
+
+ ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);
|
|
227
|
+
return cmd + ' >/dev/null 2>&1';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Build one feature-ADR run-cost row without manufacturing wall-clock data in JavaScript. */
|
|
231
|
+
export function traceLedgerLine(opts: {
|
|
232
|
+
slug: unknown;
|
|
233
|
+
runId?: unknown;
|
|
234
|
+
planDigest?: unknown;
|
|
235
|
+
/**
|
|
236
|
+
* `agents` is the TOTAL number of agent invocations this run made — model dispatches AND infra
|
|
237
|
+
* agents (trace flush, checkpoint read/write, training-pair write/backfill, landed-barrier probes,
|
|
238
|
+
* and this ledger writer itself) — counted at write time. It is NOT the trace's model-dispatch
|
|
239
|
+
* count; the ledger's `agents` column means `agent_count` from the Workflow completion
|
|
240
|
+
* notification, and this row must not silently redefine it.
|
|
241
|
+
*/
|
|
242
|
+
agents?: unknown;
|
|
243
|
+
date?: unknown;
|
|
244
|
+
outcome?: unknown;
|
|
245
|
+
}): string | null {
|
|
246
|
+
try {
|
|
247
|
+
if (typeof opts.slug !== 'string' || opts.slug === '') return null;
|
|
248
|
+
const agents = typeof opts.agents === 'number'
|
|
249
|
+
&& Number.isFinite(opts.agents)
|
|
250
|
+
&& Number.isInteger(opts.agents)
|
|
251
|
+
&& opts.agents >= 0
|
|
252
|
+
? opts.agents
|
|
253
|
+
: 0;
|
|
254
|
+
const date = typeof opts.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(opts.date) ? opts.date : null;
|
|
255
|
+
const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';
|
|
256
|
+
const line = JSON.stringify({
|
|
257
|
+
slug: opts.slug,
|
|
258
|
+
stage: 'loop-run',
|
|
259
|
+
tier: null,
|
|
260
|
+
tokens: null,
|
|
261
|
+
minutes: null,
|
|
262
|
+
agents,
|
|
263
|
+
coder: null,
|
|
264
|
+
grade: null,
|
|
265
|
+
date,
|
|
266
|
+
auto: true,
|
|
267
|
+
outcome,
|
|
268
|
+
runId: typeof opts.runId === 'string' ? opts.runId : null,
|
|
269
|
+
planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,
|
|
270
|
+
});
|
|
271
|
+
return line.length <= 4000 ? line : null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Build the single command that appends a run-cost row and confirms the write. */
|
|
278
|
+
export function traceLedgerAppendCmd(repoAbs: unknown, line: unknown): string | null {
|
|
279
|
+
if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '') return null;
|
|
280
|
+
const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');
|
|
281
|
+
const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');
|
|
282
|
+
// The field token has a fixed position, and JSON-escaped scalar values (including slug,
|
|
283
|
+
// runId, and planDigest) cannot introduce the raw `"date":null` token targeted by sed.
|
|
284
|
+
return 'mkdir -p ' + dir
|
|
285
|
+
+ " && printf '%s' " + traceShellQuote(line)
|
|
286
|
+
+ ' | sed "s/\\"date\\":null/\\"date\\":\\"$(date -u +%Y-%m-%d)\\"/" >> ' + file
|
|
287
|
+
+ " && printf '\\n' >> " + file
|
|
288
|
+
+ ' && echo LEDGER-OK';
|
|
289
|
+
}
|
|
290
|
+
|
|
214
291
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
215
292
|
// READER HALF — parseTrace / runInvariants / assembleTimeline / renderTimelineHtml.
|
|
216
293
|
// Pure over strings; the CLI does the fs.
|