@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
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
/** Blob version stamp read by scripts/gen-loop-blobs.mjs (feature loop-designer, ADR-004) — the
|
|
28
28
|
* ONLY loop-designer change to this canonical file; bump when any blob-exported semantic changes. */
|
|
29
|
-
export const BLOB_VERSION = '1.
|
|
29
|
+
export const BLOB_VERSION = '1.1.0';
|
|
30
30
|
|
|
31
31
|
/** Stages the workflow checkpoints, in pipeline order. Cheap side-channel agents (usage probes,
|
|
32
32
|
* fa-record, auto-cost selects) are never checkpointed; the opt-in Delivery gate re-runs by design
|
|
@@ -241,13 +241,59 @@ export function checkpointAppendCmd(fdirAbs: string, line: string): string {
|
|
|
241
241
|
// dataset must honour the cross-model rule — QE pairs must come from a DIFFERENT
|
|
242
242
|
// family than the coder's pairs; router/plan distill easily, code is hardest.
|
|
243
243
|
//
|
|
244
|
-
// Pure and deterministic like the checkpoint half: ts is PASSED IN (never
|
|
244
|
+
// Pure and deterministic like the checkpoint half: ts is PASSED IN (never read from a clock
|
|
245
245
|
// — the workflow sandbox forbids Date, and tests must stay deterministic); the
|
|
246
246
|
// workflow mirrors these functions inline and fills ts shell-side via sed.
|
|
247
247
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
248
|
|
|
249
|
+
export type CaptureMode = 'capture' | 'backfill' | 'skip-disabled' | 'skip-empty';
|
|
250
|
+
|
|
251
|
+
/** Decide whether this completion is captured. A resumed stage is backfilled rather than
|
|
252
|
+
* skipped: its input (stage template + args) and checkpointed output are both in scope at the
|
|
253
|
+
* capture site, so the pair is deterministically reconstructible. trainingPairBackfillCmd's
|
|
254
|
+
* persistent atomic mark makes that write at-most-once, so concurrent invocations and later
|
|
255
|
+
* runIds cannot double-append the same pair. */
|
|
256
|
+
export function decideCaptureMode(opts: { enabled: boolean; resumed: boolean; recordCount: number }): CaptureMode {
|
|
257
|
+
if (!opts.enabled) return 'skip-disabled';
|
|
258
|
+
if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0) return 'skip-empty';
|
|
259
|
+
return opts.resumed ? 'backfill' : 'capture';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export type CaptureFailureReason = 'threw' | 'unserializable' | 'unverified' | 'backfill-unverified' | 'empty-output';
|
|
263
|
+
|
|
264
|
+
export interface CaptureFailureRecord {
|
|
265
|
+
stage: string;
|
|
266
|
+
mode: CaptureMode | null;
|
|
267
|
+
reason: CaptureFailureReason;
|
|
268
|
+
detail: string | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Normalize capture failures for collection by the caller. This recorder must never throw:
|
|
272
|
+
* replacing the original capture failure with a reporting failure would hide the real cause. */
|
|
273
|
+
export function captureFailureRecord(stage: unknown, mode: unknown, reason: unknown, detail: unknown): CaptureFailureRecord {
|
|
274
|
+
const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';
|
|
275
|
+
const normalizedMode: CaptureMode | null =
|
|
276
|
+
mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'
|
|
277
|
+
? mode
|
|
278
|
+
: null;
|
|
279
|
+
const normalizedReason: CaptureFailureReason =
|
|
280
|
+
reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'
|
|
281
|
+
? reason
|
|
282
|
+
: 'threw';
|
|
283
|
+
let normalizedDetail: string | null = null;
|
|
284
|
+
if (detail !== null && detail !== undefined) {
|
|
285
|
+
try {
|
|
286
|
+
const text = String(detail);
|
|
287
|
+
if (text !== '') normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;
|
|
288
|
+
} catch {
|
|
289
|
+
normalizedDetail = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };
|
|
293
|
+
}
|
|
294
|
+
|
|
249
295
|
/** Training-pair record format version. Bump on any field-shape change. */
|
|
250
|
-
export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-
|
|
296
|
+
export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-2';
|
|
251
297
|
|
|
252
298
|
/** Oversize guard cap over input+output combined (same posture as
|
|
253
299
|
* CHECKPOINT_MAX_RESULT_CHARS, sized for full stage prompts): an over-cap pair is
|
|
@@ -299,12 +345,15 @@ export interface TrainingPair {
|
|
|
299
345
|
schema: string;
|
|
300
346
|
slug: string;
|
|
301
347
|
stage: string;
|
|
348
|
+
/** ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint. */
|
|
302
349
|
ts: number | string | null;
|
|
303
350
|
input: string;
|
|
304
351
|
output: string;
|
|
305
352
|
evaluation: TrainingPairEvaluation;
|
|
306
353
|
provenance: TrainingPairProvenance;
|
|
307
354
|
truncated: TrainingPairTruncation | null;
|
|
355
|
+
captureMode: 'capture' | 'backfill';
|
|
356
|
+
resumed: boolean;
|
|
308
357
|
}
|
|
309
358
|
|
|
310
359
|
/** Per-stage JSONL path, relative to the repo root. ONE file per stage. */
|
|
@@ -315,7 +364,7 @@ export function trainingPairPath(slug: string, stage: string): string {
|
|
|
315
364
|
/** README dropped once into the capture dir. The caveat is documented ON DISK because the
|
|
316
365
|
* directory is deliberately not gitignored (explicit owner decision, 2026-08). */
|
|
317
366
|
export const TRAINPAIR_PRIVACY_NOTE =
|
|
318
|
-
|
|
367
|
+
"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.";
|
|
319
368
|
|
|
320
369
|
/** Coerce a stage input/output to text: strings pass through; objects serialize to JSON;
|
|
321
370
|
* an unserializable value degrades to String(v) — buildTrainingPair NEVER throws (capture
|
|
@@ -346,6 +395,8 @@ export function buildTrainingPair(opts: {
|
|
|
346
395
|
output: unknown;
|
|
347
396
|
evaluation?: Partial<TrainingPairEvaluation> | null;
|
|
348
397
|
provenance?: Partial<TrainingPairProvenance> | null;
|
|
398
|
+
captureMode?: unknown;
|
|
399
|
+
resumed?: unknown;
|
|
349
400
|
}): TrainingPair {
|
|
350
401
|
let input = coerceText(opts.input);
|
|
351
402
|
let output = coerceText(opts.output);
|
|
@@ -383,6 +434,8 @@ export function buildTrainingPair(opts: {
|
|
|
383
434
|
minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,
|
|
384
435
|
},
|
|
385
436
|
truncated,
|
|
437
|
+
captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',
|
|
438
|
+
resumed: opts.resumed === true,
|
|
386
439
|
};
|
|
387
440
|
}
|
|
388
441
|
|
|
@@ -410,3 +463,49 @@ export function trainingPairAppendCmd(repoAbs: string, slug: string, stage: stri
|
|
|
410
463
|
" && printf '%s\\n' " + shellQuote(line) + ' >> ' + shellQuote(fileAbs)
|
|
411
464
|
);
|
|
412
465
|
}
|
|
466
|
+
|
|
467
|
+
/** Readback sentinels for the caller to distinguish an at-most-once write from an existing pair. */
|
|
468
|
+
export const TP_BACKFILL_OK = 'TP-BACKFILL-OK';
|
|
469
|
+
export const TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';
|
|
470
|
+
export const TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';
|
|
471
|
+
|
|
472
|
+
/** Build the deterministic resume-backfill command. The persistent mkdir mark is the atomic
|
|
473
|
+
* at-most-once primitive; the inner file-absence guard also protects pair files created before
|
|
474
|
+
* marks existed. The mark is RELEASED when — and only when — the append fails, so a failed backfill
|
|
475
|
+
* stays retryable. The `[ -f ]` path keeps the mark because the pair genuinely exists.
|
|
476
|
+
* KNOWN RESIDUAL: a process killed (SIGKILL, sandbox timeout) between the `mkdir` claim and the end
|
|
477
|
+
* of the append still leaves a poisoned mark. That window is strictly narrower than "any append
|
|
478
|
+
* failure" and is the same externally-killed class this feature already names for the ledger row.
|
|
479
|
+
* `TP_BACKFILL_SKIP` means "the per-stage pair file already existed"; `TP_BACKFILL_DUP` means
|
|
480
|
+
* "another run already owns this content". The two strings are deliberately NON-PREFIXING because
|
|
481
|
+
* the two producers parse the readback differently — the generated loop compares `===` after
|
|
482
|
+
* `trim`, while the `feature-adr.js` twin tests an UNANCHORED regex; a prefixed name would be `DUP`
|
|
483
|
+
* to one parser and `SKIP` to the other from the same bytes. The default `markKey` is per-CONTENT
|
|
484
|
+
* only; a caller whose line embeds a per-run identifier must pass a run-independent `markKey`.
|
|
485
|
+
* Marks are deliberately never pruned. */
|
|
486
|
+
export function trainingPairBackfillCmd(repoAbs: string, slug: string, stage: string, lines: readonly string[], markKey?: string): string | null {
|
|
487
|
+
if (typeof repoAbs !== 'string' || repoAbs === '') return null;
|
|
488
|
+
if (typeof slug !== 'string' || slug === '') return null;
|
|
489
|
+
if (typeof stage !== 'string' || stage === '') return null;
|
|
490
|
+
if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== '')) return null;
|
|
491
|
+
|
|
492
|
+
const dirAbs = repoAbs + '/.dz/fa-training/' + slug;
|
|
493
|
+
const readmeAbs = repoAbs + '/.dz/fa-training/README.md';
|
|
494
|
+
const fileAbs = dirAbs + '/' + stage + '.jsonl';
|
|
495
|
+
const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';
|
|
496
|
+
const markStage = stage.replace(/\.\./g, '_').replace(/\//g, '_');
|
|
497
|
+
const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\0' + lines.join('\n')) : markKey;
|
|
498
|
+
const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;
|
|
499
|
+
const appends = lines
|
|
500
|
+
.map(line => "printf '%s\\n' " + shellQuote(line) + ' >> ' + shellQuote(fileAbs))
|
|
501
|
+
.join(' && ');
|
|
502
|
+
return (
|
|
503
|
+
'mkdir -p ' + shellQuote(dirAbs) +
|
|
504
|
+
' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \'%s\\n\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +
|
|
505
|
+
' && mkdir -p ' + shellQuote(markDir) +
|
|
506
|
+
' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +
|
|
507
|
+
'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +
|
|
508
|
+
'; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +
|
|
509
|
+
'; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi'
|
|
510
|
+
);
|
|
511
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -127,7 +127,7 @@ export type {
|
|
|
127
127
|
TeachGuardResult,
|
|
128
128
|
} from './vector-tier.js';
|
|
129
129
|
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
|
|
130
|
-
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStatePath } from './statusline.js';
|
|
130
|
+
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
|
|
131
131
|
export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
|
|
132
132
|
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES } from './agentdb-index.js';
|
|
133
133
|
export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
|
|
@@ -41,7 +41,7 @@ export const BLOB_COVERAGE_MANIFEST: { coveredWorkflows: string[] } = {
|
|
|
41
41
|
export const BLOBS: Record<string, LoopBlob> = {
|
|
42
42
|
"checkpoints": {
|
|
43
43
|
name: "checkpoints",
|
|
44
|
-
version: "1.
|
|
44
|
+
version: "1.1.0",
|
|
45
45
|
contentHash: "aa730483f52a9f6263751138d4514fe9a6a3f4f191897c86d1e035f3da890574",
|
|
46
46
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
47
47
|
requires: [],
|
|
@@ -50,12 +50,12 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
50
50
|
},
|
|
51
51
|
"training-pairs": {
|
|
52
52
|
name: "training-pairs",
|
|
53
|
-
version: "1.
|
|
54
|
-
contentHash: "
|
|
53
|
+
version: "1.1.0",
|
|
54
|
+
contentHash: "740b733d3d995e7031590f1707b442d0f0f9d7d585ada6f37bbf08c5db3351e9",
|
|
55
55
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
56
56
|
requires: ["checkpoints"],
|
|
57
|
-
exports: ["TRAINPAIR_SCHEMA_VERSION","TRAINPAIR_MAX_IO_CHARS","trainingPairFamily","trainingPairPath","TRAINPAIR_PRIVACY_NOTE","buildTrainingPair","serializeTrainingPair","trainingPairAppendCmd"],
|
|
58
|
-
code: "const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-
|
|
57
|
+
exports: ["TRAINPAIR_SCHEMA_VERSION","TRAINPAIR_MAX_IO_CHARS","trainingPairFamily","trainingPairPath","TRAINPAIR_PRIVACY_NOTE","buildTrainingPair","serializeTrainingPair","trainingPairAppendCmd","decideCaptureMode","captureFailureRecord","trainingPairBackfillCmd","TP_BACKFILL_OK","TP_BACKFILL_SKIP"],
|
|
58
|
+
code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-2';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction buildTrainingPair(opts) {\n let input = coerceText(opts.input);\n let output = coerceText(opts.output);\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
|
|
59
59
|
},
|
|
60
60
|
"model-resolver": {
|
|
61
61
|
name: "model-resolver",
|
|
@@ -95,12 +95,12 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
95
95
|
},
|
|
96
96
|
"trace": {
|
|
97
97
|
name: "trace",
|
|
98
|
-
version: "1.
|
|
99
|
-
contentHash: "
|
|
98
|
+
version: "1.1.0",
|
|
99
|
+
contentHash: "fc59098bb0deb747c4c1c9a6ba843df70edfe0dc5ef8370c0f7192afc7657890",
|
|
100
100
|
sourcePath: "packages/@dzhechkov/harness-core/src/loop-trace.ts",
|
|
101
101
|
requires: [],
|
|
102
|
-
exports: ["LOOP_TRACE_SCHEMA_VERSION","TRACE_RUNID_RE","TRACE_KEY_RE","traceShellQuote","traceValidateEvent","traceInit","traceOnDispatch","traceOnSettle","traceClose","traceFlushCmd"],
|
|
103
|
-
code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}",
|
|
102
|
+
exports: ["LOOP_TRACE_SCHEMA_VERSION","TRACE_RUNID_RE","TRACE_KEY_RE","traceShellQuote","traceValidateEvent","traceInit","traceOnDispatch","traceOnSettle","traceClose","traceFlushCmd","traceFaRecordCmd","traceLedgerLine","traceLedgerAppendCmd"],
|
|
103
|
+
code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
|
|
104
104
|
},
|
|
105
105
|
"ha-consult-router": {
|
|
106
106
|
name: "ha-consult-router",
|
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
|
|