@dzhechkov/harness-core 0.4.1 → 0.4.2
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 +41 -37
- package/README.md +3 -2
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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-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/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/package.json +5 -5
- package/sbom.json +46 -36
- package/src/feature-adr-checkpoints.ts +103 -4
- package/src/index.ts +1 -1
- package/src/loop-blobs.generated.ts +9 -9
- package/src/loop-render.ts +93 -17
- package/src/loop-trace.ts +78 -1
- package/src/statusline.ts +117 -30
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.
|
package/src/statusline.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* @packageDocumentation
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
18
|
-
import {
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
|
|
21
21
|
import { listBrain } from './brain.js';
|
|
@@ -28,6 +28,8 @@ import { RECALL_USAGE_LOG_RELATIVE, aggregateRecallUsage, parseRecallUsageLog }
|
|
|
28
28
|
* path (readonly, best-effort) by `readFeatureAdrState`.
|
|
29
29
|
*/
|
|
30
30
|
export interface FeatureAdrState {
|
|
31
|
+
/** Producer of this panel state. Missing/invalid legacy values are treated as `feature-adr`. */
|
|
32
|
+
readonly kind?: 'feature-adr' | 'loop';
|
|
31
33
|
/** The feature slug the pipeline is working on (kebab-case). */
|
|
32
34
|
readonly slug: string;
|
|
33
35
|
/** Human-readable step label (e.g. "Step 0", "Step 8 QE"). */
|
|
@@ -70,9 +72,29 @@ function consolidateWatermarkPath(projectRoot: string): string {
|
|
|
70
72
|
return join(projectRoot, '.dz', 'memory', 'consolidate.json');
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/**
|
|
74
|
-
export function
|
|
75
|
-
return join(projectRoot, '.dz', 'feature-adr', 'learning-state
|
|
75
|
+
/** Directory of the per-slug live `/feature-adr` learning-state slots. */
|
|
76
|
+
export function featureAdrStateDir(projectRoot: string): string {
|
|
77
|
+
return join(projectRoot, '.dz', 'feature-adr', 'learning-state');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Make a slug safe as one bounded filename component (never `/`, `..`, or leading dot/dash). */
|
|
81
|
+
function featureAdrStateSlug(slug: string): string {
|
|
82
|
+
const safe = slug
|
|
83
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
84
|
+
.replace(/^[.-]+/, '_')
|
|
85
|
+
.replace(/\.{2,}/g, '_')
|
|
86
|
+
.slice(0, 60);
|
|
87
|
+
return safe.length > 0 ? safe : '_unnamed';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Path of a live `/feature-adr` learning-state file. With a slug this is its namespaced slot;
|
|
92
|
+
* without one this remains the legacy single-slot path for backward-compatible readers/callers.
|
|
93
|
+
*/
|
|
94
|
+
export function featureAdrStatePath(projectRoot: string, slug?: string): string {
|
|
95
|
+
return slug === undefined
|
|
96
|
+
? join(projectRoot, '.dz', 'feature-adr', 'learning-state.json')
|
|
97
|
+
: join(featureAdrStateDir(projectRoot), `${featureAdrStateSlug(slug)}.json`);
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
/**
|
|
@@ -181,35 +203,84 @@ function consolidatedAgeHours(projectRoot: string, now: number): number | undefi
|
|
|
181
203
|
* @param now Injectable clock (epoch ms) for the freshness check — defaults to `Date.now()`.
|
|
182
204
|
*/
|
|
183
205
|
export function readFeatureAdrState(projectRoot: string, now: number = Date.now()): FeatureAdrState | undefined {
|
|
184
|
-
const
|
|
185
|
-
|
|
206
|
+
const root = resolve(projectRoot);
|
|
207
|
+
const candidates: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Keep the legacy single slot in the candidate set: an older dz may still be writing it while a
|
|
210
|
+
// newer statusline renders. Directory discovery is guarded separately because this is the hot,
|
|
211
|
+
// readonly ~300ms render path; it never performs housekeeping or any other write.
|
|
186
212
|
try {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
const legacyPath = featureAdrStatePath(root);
|
|
214
|
+
if (existsSync(legacyPath)) candidates.push(legacyPath);
|
|
215
|
+
} catch { /* best-effort candidate discovery */ }
|
|
216
|
+
try {
|
|
217
|
+
const remaining = 64 - candidates.length;
|
|
218
|
+
if (remaining > 0) {
|
|
219
|
+
const dir = featureAdrStateDir(root);
|
|
220
|
+
const names = readdirSync(dir)
|
|
221
|
+
.filter((name) => name.endsWith('.json'))
|
|
222
|
+
.map((name) => {
|
|
223
|
+
let mtimeMs = -Infinity;
|
|
224
|
+
try {
|
|
225
|
+
mtimeMs = statSync(join(dir, name)).mtimeMs;
|
|
226
|
+
} catch { /* a disappearing/unreadable entry sorts last */ }
|
|
227
|
+
return { name, mtimeMs };
|
|
228
|
+
})
|
|
229
|
+
// Truncation may only drop the least recent slots: kind-rank arbitration cannot rescue a non-candidate.
|
|
230
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
231
|
+
.slice(0, remaining);
|
|
232
|
+
for (const { name } of names) candidates.push(join(dir, name));
|
|
233
|
+
}
|
|
234
|
+
} catch { /* absent/unreadable per-slug directory is normal */ }
|
|
235
|
+
|
|
236
|
+
const parseCandidate = (path: string): { state: FeatureAdrState; tsMs: number; rank: number } | undefined => {
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as Partial<FeatureAdrState>;
|
|
239
|
+
if (typeof parsed.slug !== 'string' || parsed.slug.length === 0) return undefined;
|
|
240
|
+
if (typeof parsed.step !== 'string' || parsed.step.length === 0) return undefined;
|
|
241
|
+
if (typeof parsed.ts !== 'string') return undefined;
|
|
242
|
+
const tsMs = Date.parse(parsed.ts);
|
|
243
|
+
if (Number.isNaN(tsMs)) return undefined;
|
|
244
|
+
if (now - tsMs > FEATURE_ADR_FRESH_MS) return undefined; // stale run — do not surface a panel
|
|
245
|
+
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
246
|
+
// The panel exists to surface the /feature-adr Pattern-memory loop. A generated loop writes
|
|
247
|
+
// zero recalled/stored counters far more often, so freshest-wins would recreate F5 by making
|
|
248
|
+
// a live pipeline's meaningful counters disappear. Missing/invalid legacy markers therefore
|
|
249
|
+
// retain the historical `feature-adr` rank, which outranks every loop slot regardless of ts.
|
|
250
|
+
const kind: 'feature-adr' | 'loop' = parsed.kind === 'loop' ? 'loop' : 'feature-adr';
|
|
251
|
+
const state: FeatureAdrState = {
|
|
252
|
+
kind,
|
|
253
|
+
slug: parsed.slug,
|
|
254
|
+
step: parsed.step,
|
|
255
|
+
pool: num(parsed.pool),
|
|
256
|
+
recalled: num(parsed.recalled),
|
|
257
|
+
stored: num(parsed.stored),
|
|
258
|
+
...(num(parsed.reinforced) > 0 ? { reinforced: num(parsed.reinforced) } : {}),
|
|
259
|
+
ts: parsed.ts,
|
|
260
|
+
...(typeof parsed.mode === 'string' && parsed.mode.length > 0 ? { mode: parsed.mode } : {}),
|
|
261
|
+
};
|
|
262
|
+
return { state, tsMs, rank: kind === 'feature-adr' ? 1 : 0 };
|
|
263
|
+
} catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
let winner: ReturnType<typeof parseCandidate>;
|
|
269
|
+
for (const path of candidates) {
|
|
270
|
+
const candidate = parseCandidate(path);
|
|
271
|
+
if (candidate === undefined) continue;
|
|
272
|
+
if (winner === undefined
|
|
273
|
+
|| candidate.rank > winner.rank
|
|
274
|
+
|| (candidate.rank === winner.rank && candidate.tsMs > winner.tsMs)) {
|
|
275
|
+
winner = candidate;
|
|
276
|
+
}
|
|
208
277
|
}
|
|
278
|
+
return winner?.state;
|
|
209
279
|
}
|
|
210
280
|
|
|
211
281
|
/** Fields the `/feature-adr` pipeline supplies when recording its live learning state. */
|
|
212
282
|
export interface WriteFeatureAdrStateInput {
|
|
283
|
+
readonly kind?: 'feature-adr' | 'loop';
|
|
213
284
|
readonly slug: string;
|
|
214
285
|
readonly step: string;
|
|
215
286
|
readonly recalled: number;
|
|
@@ -239,6 +310,7 @@ export function writeFeatureAdrState(
|
|
|
239
310
|
pool = 0;
|
|
240
311
|
}
|
|
241
312
|
const state: FeatureAdrState = {
|
|
313
|
+
kind: input.kind === 'loop' ? 'loop' : 'feature-adr',
|
|
242
314
|
slug: input.slug,
|
|
243
315
|
step: input.step,
|
|
244
316
|
pool,
|
|
@@ -249,8 +321,23 @@ export function writeFeatureAdrState(
|
|
|
249
321
|
...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}),
|
|
250
322
|
};
|
|
251
323
|
try {
|
|
252
|
-
const
|
|
253
|
-
mkdirSync(
|
|
324
|
+
const dir = featureAdrStateDir(root);
|
|
325
|
+
mkdirSync(dir, { recursive: true });
|
|
326
|
+
|
|
327
|
+
// Housekeeping belongs only on this write path, never the ~300ms render path. Every file is
|
|
328
|
+
// independently guarded so an unreadable/racing entry cannot prevent the live state write.
|
|
329
|
+
try {
|
|
330
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1_000;
|
|
331
|
+
for (const name of readdirSync(dir)) {
|
|
332
|
+
if (!name.endsWith('.json')) continue;
|
|
333
|
+
const stalePath = join(dir, name);
|
|
334
|
+
try {
|
|
335
|
+
if (statSync(stalePath).mtimeMs < cutoff) unlinkSync(stalePath);
|
|
336
|
+
} catch { /* best-effort per-file cleanup */ }
|
|
337
|
+
}
|
|
338
|
+
} catch { /* best-effort directory cleanup */ }
|
|
339
|
+
|
|
340
|
+
const path = featureAdrStatePath(root, input.slug);
|
|
254
341
|
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
255
342
|
} catch {
|
|
256
343
|
return undefined;
|