@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
package/src/backlog.ts
CHANGED
|
@@ -91,6 +91,9 @@ export type DedupAction = 'duplicate' | 'related' | 'new';
|
|
|
91
91
|
/** Pure output of the classifier (04) — consumed by capture. */
|
|
92
92
|
export interface DedupVerdict {
|
|
93
93
|
readonly action: DedupAction;
|
|
94
|
+
/** Ids excluded from VECTOR candidacy because their text was edited without a re-embed yet. A
|
|
95
|
+
* consumer must read this before treating a `new` verdict as "compared against everything". */
|
|
96
|
+
readonly staleExcluded?: string[];
|
|
94
97
|
/** Top-1 raw cosine (ADR-002 — never an RRF score). `-1` when there is nothing to compare against. */
|
|
95
98
|
readonly cosine: number;
|
|
96
99
|
readonly matchedId: string | undefined;
|
|
@@ -593,6 +596,160 @@ export function transitionIdeas(
|
|
|
593
596
|
return { ok: true, dryRun, changes, errors: [], written: true };
|
|
594
597
|
}
|
|
595
598
|
|
|
599
|
+
/* ── Edit a captured idea's TEXT (idea 1fde7bf6) ─────────────────────────────────────────────
|
|
600
|
+
*
|
|
601
|
+
* Why this verb exists at all: editing the store by hand does NOT re-embed the record, so its dedup
|
|
602
|
+
* vector keeps describing the OLD text and later duplicate checks run against something the record
|
|
603
|
+
* no longer says. The verb owns the text change; the CALLER owns the re-embed (it is async and needs
|
|
604
|
+
* the vector tier). Between the two, the record carries `embedStale` — see ADR-001: the guard against
|
|
605
|
+
* a stale vector lives where the HARM would be (the dedup verdict), not where the failure happened.
|
|
606
|
+
* ────────────────────────────────────────────────────────────────────────────────────────── */
|
|
607
|
+
|
|
608
|
+
/** Where an edit's PREVIOUS text is preserved. An edit destroys text and `reopen` cannot undo it the
|
|
609
|
+
* way it undoes `drop`, so the old text is appended here before the store is rewritten. */
|
|
610
|
+
export function editsLogPath(projectRoot: string): string {
|
|
611
|
+
return join(projectRoot, '.dz', 'backlog', 'edits.jsonl');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export interface EditReport {
|
|
615
|
+
readonly ok: boolean;
|
|
616
|
+
readonly dryRun: boolean;
|
|
617
|
+
readonly id?: string;
|
|
618
|
+
readonly previousText?: string;
|
|
619
|
+
readonly newText?: string;
|
|
620
|
+
readonly errors: string[];
|
|
621
|
+
readonly written: boolean;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Replace or extend ONE idea's text. Mirrors `transitionIdeas`' line discipline exactly: the file is
|
|
626
|
+
* split without discarding anything, every untouched line — including a line the parser cannot read —
|
|
627
|
+
* goes back BYTE-FOR-BYTE, and only the matched record's line is re-serialised. The store holds
|
|
628
|
+
* dozens of records; a whole-file JSON round-trip would reformat all of them to change one.
|
|
629
|
+
*/
|
|
630
|
+
export function editIdea(
|
|
631
|
+
projectRoot: string,
|
|
632
|
+
prefix: string,
|
|
633
|
+
opts: { text?: string; append?: string; dryRun?: boolean; nowIso?: string } = {},
|
|
634
|
+
): EditReport {
|
|
635
|
+
const dryRun = opts.dryRun === true;
|
|
636
|
+
const hasText = typeof opts.text === 'string' && opts.text !== '';
|
|
637
|
+
const hasAppend = typeof opts.append === 'string' && opts.append !== '';
|
|
638
|
+
if (hasText && hasAppend) {
|
|
639
|
+
return { ok: false, dryRun, errors: ['--text and --append are mutually exclusive — pick one'], written: false };
|
|
640
|
+
}
|
|
641
|
+
if (!hasText && !hasAppend) {
|
|
642
|
+
return { ok: false, dryRun, errors: ['nothing to do: give --text "<new text>" or --append "<more text>"'], written: false };
|
|
643
|
+
}
|
|
644
|
+
const path = ideasPath(projectRoot);
|
|
645
|
+
if (!existsSync(path)) {
|
|
646
|
+
return { ok: false, dryRun, errors: ['no backlog store — nothing captured yet (dz backlog add "<idea>")'], written: false };
|
|
647
|
+
}
|
|
648
|
+
let raw: string;
|
|
649
|
+
try {
|
|
650
|
+
raw = readFileSync(path, 'utf-8');
|
|
651
|
+
} catch (e) {
|
|
652
|
+
return { ok: false, dryRun, errors: [`cannot read ${path}: ${(e as Error).message}`], written: false };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Same line discipline as transitionIdeas: nothing is discarded, corrupt lines are left alone.
|
|
656
|
+
const lines = raw.split('\n');
|
|
657
|
+
const parsed: { index: number; obj: Record<string, unknown>; id: string }[] = [];
|
|
658
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
659
|
+
const trimmed = (lines[i] ?? '').trim();
|
|
660
|
+
if (trimmed === '') continue;
|
|
661
|
+
try {
|
|
662
|
+
const obj = JSON.parse(trimmed) as Record<string, unknown>;
|
|
663
|
+
if (typeof obj.id === 'string' && obj.id !== '') parsed.push({ index: i, obj, id: obj.id });
|
|
664
|
+
} catch {
|
|
665
|
+
/* corrupt line — left byte-for-byte as-is */
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!isSafeId(prefix)) {
|
|
670
|
+
return { ok: false, dryRun, errors: [`refusing an unsafe idea id: ${JSON.stringify(prefix)}`], written: false };
|
|
671
|
+
}
|
|
672
|
+
const res = resolveIdPrefix(parsed.map((p) => p.id), prefix);
|
|
673
|
+
if (res.kind === 'not-found') {
|
|
674
|
+
return { ok: false, dryRun, errors: [`no idea matches ${prefix} — run dz backlog list to see the ids`], written: false };
|
|
675
|
+
}
|
|
676
|
+
if (res.kind === 'ambiguous') {
|
|
677
|
+
return { ok: false, dryRun, errors: [`ambiguous prefix ${prefix} — matches ${res.matches.join(', ')}; give more characters`], written: false };
|
|
678
|
+
}
|
|
679
|
+
const entries = parsed.filter((p) => p.id === res.id);
|
|
680
|
+
if (entries.length > 1) {
|
|
681
|
+
// Deciding on the first line while rewriting one is how the sibling verb grew its twin bug.
|
|
682
|
+
return { ok: false, dryRun, errors: [`${res.id} appears ${entries.length}× in the store (duplicate lines; resolve the duplicate by hand)`], written: false };
|
|
683
|
+
}
|
|
684
|
+
const entry = entries[0]!;
|
|
685
|
+
const previousText = typeof entry.obj.text === 'string' ? (entry.obj.text as string) : '';
|
|
686
|
+
const newText = hasText ? (opts.text as string) : `${previousText}${previousText === '' ? '' : ' '}${opts.append as string}`;
|
|
687
|
+
if (newText === previousText) {
|
|
688
|
+
return { ok: true, dryRun, id: res.id, previousText, newText, errors: [], written: false };
|
|
689
|
+
}
|
|
690
|
+
if (dryRun) {
|
|
691
|
+
return { ok: true, dryRun, id: res.id, previousText, newText, errors: [], written: false };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Only `text` changes, plus the stale marker. Every other field is carried through untouched.
|
|
695
|
+
entry.obj.text = newText;
|
|
696
|
+
entry.obj.embedStale = true;
|
|
697
|
+
lines[entry.index] = JSON.stringify(entry.obj);
|
|
698
|
+
|
|
699
|
+
const nowIso = opts.nowIso ?? new Date().toISOString();
|
|
700
|
+
const logPath = editsLogPath(projectRoot);
|
|
701
|
+
try {
|
|
702
|
+
mkdirSync(join(projectRoot, '.dz', 'backlog'), { recursive: true });
|
|
703
|
+
appendFileSync(logPath, `${JSON.stringify({ id: res.id, previousText, newText, ts: nowIso })}\n`);
|
|
704
|
+
} catch (e) {
|
|
705
|
+
// The trail is the ONLY copy of the previous text. Refuse rather than destroy it untraceably.
|
|
706
|
+
return { ok: false, dryRun, id: res.id, previousText, newText, errors: [`edit log write failed, store left untouched: ${(e as Error).message}`], written: false };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
710
|
+
try {
|
|
711
|
+
writeFileSync(tmp, lines.join('\n'));
|
|
712
|
+
renameSync(tmp, path);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
try { unlinkSync(tmp); } catch { /* best-effort litter cleanup */ }
|
|
715
|
+
return { ok: false, dryRun, id: res.id, previousText, newText, errors: [`store write failed: ${(e as Error).message}`], written: false };
|
|
716
|
+
}
|
|
717
|
+
return { ok: true, dryRun, id: res.id, previousText, newText, errors: [], written: true };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Clear the stale marker after a successful re-embed. Separate from `editIdea` because the re-embed
|
|
721
|
+
* is async and belongs to the caller; a marker cleared without a re-embed would be a lie. */
|
|
722
|
+
export function clearEmbedStale(projectRoot: string, id: string): boolean {
|
|
723
|
+
const path = ideasPath(projectRoot);
|
|
724
|
+
if (!existsSync(path)) return false;
|
|
725
|
+
let raw: string;
|
|
726
|
+
try { raw = readFileSync(path, 'utf-8'); } catch { return false; }
|
|
727
|
+
const lines = raw.split('\n');
|
|
728
|
+
let touched = false;
|
|
729
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
730
|
+
const trimmed = (lines[i] ?? '').trim();
|
|
731
|
+
if (trimmed === '') continue;
|
|
732
|
+
try {
|
|
733
|
+
const obj = JSON.parse(trimmed) as Record<string, unknown>;
|
|
734
|
+
if (obj.id === id && obj.embedStale === true) {
|
|
735
|
+
delete obj.embedStale;
|
|
736
|
+
lines[i] = JSON.stringify(obj);
|
|
737
|
+
touched = true;
|
|
738
|
+
}
|
|
739
|
+
} catch { /* corrupt line — left alone */ }
|
|
740
|
+
}
|
|
741
|
+
if (!touched) return false;
|
|
742
|
+
const tmp = `${path}.tmp-clear-${process.pid}`;
|
|
743
|
+
try {
|
|
744
|
+
writeFileSync(tmp, lines.join('\n'));
|
|
745
|
+
renameSync(tmp, path);
|
|
746
|
+
return true;
|
|
747
|
+
} catch {
|
|
748
|
+
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
596
753
|
/* ── Store privacy (idea ec4cd60d): raw ideas are prompt-class PRIVATE content, like recall-usage.jsonl. ── */
|
|
597
754
|
|
|
598
755
|
export type GitignoreAction = 'created' | 'appended' | 'already-covered' | 'user-opted-out' | 'skipped';
|
|
@@ -741,6 +898,11 @@ export interface DedupCandidate {
|
|
|
741
898
|
readonly id: string;
|
|
742
899
|
readonly cosine: number;
|
|
743
900
|
readonly containment?: number;
|
|
901
|
+
/** Set when the record's text was edited but its vector has not been rewritten yet (ADR-001,
|
|
902
|
+
* idea 1fde7bf6). Such a candidate is excluded from VECTOR candidacy — its cosine describes text
|
|
903
|
+
* the record no longer has. The exact-text net still applies: the marker degrades SIMILARITY,
|
|
904
|
+
* never IDENTITY. */
|
|
905
|
+
readonly embedStale?: boolean;
|
|
744
906
|
}
|
|
745
907
|
|
|
746
908
|
/**
|
|
@@ -762,10 +924,18 @@ export function classifyDedup(
|
|
|
762
924
|
// HIGH-4: a non-finite cosine (NaN/±Infinity) sorts unpredictably and can shove a real 0.97 duplicate
|
|
763
925
|
// out of the top slot → misclassified NEW. Drop non-finite candidates BEFORE sorting/banding (the
|
|
764
926
|
// recurring repo `Number.isFinite` lesson).
|
|
765
|
-
|
|
927
|
+
// A record whose text was edited but whose vector has not been rewritten is EXCLUDED from vector
|
|
928
|
+
// candidacy — the same treatment a non-finite cosine gets, and for the same reason: the number does
|
|
929
|
+
// not describe the record. ADR-001: the guard sits where the HARM would be (this verdict, days after
|
|
930
|
+
// the edit) rather than where the failure happened (the edit's own output, which nobody re-reads).
|
|
931
|
+
const staleExcluded = candidates.filter((c) => c.embedStale === true).map((c) => c.id);
|
|
932
|
+
const sorted = candidates
|
|
933
|
+
.filter((c) => c.embedStale !== true)
|
|
934
|
+
.filter((c) => Number.isFinite(c.cosine))
|
|
935
|
+
.sort((a, b) => b.cosine - a.cosine);
|
|
766
936
|
const top = sorted[0];
|
|
767
937
|
const exactTextOnly = opts.exactTextOnly === true;
|
|
768
|
-
if (top === undefined) return { action: 'new', cosine: -1, matchedId: undefined, topMatchId: undefined, relatedIds: [], exactTextOnly };
|
|
938
|
+
if (top === undefined) return { action: 'new', cosine: -1, matchedId: undefined, topMatchId: undefined, relatedIds: [], exactTextOnly, staleExcluded };
|
|
769
939
|
const bands = new Map(sorted.map((c) => [c.id, dedupPairBand(c.cosine, c.containment, cfg)]));
|
|
770
940
|
if (bands.get(top.id) === 'duplicate') {
|
|
771
941
|
return {
|
|
@@ -775,6 +945,7 @@ export function classifyDedup(
|
|
|
775
945
|
topMatchId: top.id,
|
|
776
946
|
relatedIds: [],
|
|
777
947
|
exactTextOnly,
|
|
948
|
+
staleExcluded,
|
|
778
949
|
...(top.containment !== undefined ? { containment: top.containment } : {}),
|
|
779
950
|
};
|
|
780
951
|
}
|
|
@@ -794,6 +965,7 @@ export function classifyDedup(
|
|
|
794
965
|
topMatchId: top.id,
|
|
795
966
|
relatedIds: [],
|
|
796
967
|
exactTextOnly,
|
|
968
|
+
staleExcluded,
|
|
797
969
|
subsetMatch: true,
|
|
798
970
|
...(subset.containment !== undefined ? { containment: subset.containment } : {}),
|
|
799
971
|
...demoted,
|
|
@@ -810,11 +982,12 @@ export function classifyDedup(
|
|
|
810
982
|
topMatchId: top.id,
|
|
811
983
|
relatedIds: related.map((c) => c.id),
|
|
812
984
|
exactTextOnly,
|
|
985
|
+
staleExcluded,
|
|
813
986
|
...topContainment,
|
|
814
987
|
...demoted,
|
|
815
988
|
};
|
|
816
989
|
}
|
|
817
|
-
return { action: 'new', cosine: top.cosine, matchedId: undefined, topMatchId: top.id, relatedIds: [], exactTextOnly, ...topContainment };
|
|
990
|
+
return { action: 'new', cosine: top.cosine, matchedId: undefined, topMatchId: top.id, relatedIds: [], exactTextOnly, ...topContainment , staleExcluded };
|
|
818
991
|
}
|
|
819
992
|
|
|
820
993
|
/** Injectable deps so the production dedup path is testable without a live agentdb. */
|
|
@@ -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
|
@@ -34,6 +34,7 @@ export * from './operations.js';
|
|
|
34
34
|
export * from './workflows.js';
|
|
35
35
|
// loop-designer (feature loop-designer): loop-plan/1 schema + generator + lint + trace planes.
|
|
36
36
|
export * from './loop-plan.js';
|
|
37
|
+
export * from './loop-plan-graph.js';
|
|
37
38
|
export * from './loop-render.js';
|
|
38
39
|
// loop-lint: EXPLICIT export list (QE round-2 G14) — `dominators`, the deliberately-WEAKER
|
|
39
40
|
// analysis kept in src/loop-lint.ts solely as AM-1's mutation seam, is NOT part of the published
|
|
@@ -53,6 +54,7 @@ export {
|
|
|
53
54
|
type LintOptions,
|
|
54
55
|
} from './loop-lint.js';
|
|
55
56
|
export * from './loop-trace.js';
|
|
57
|
+
export * from './trace-bundle.js';
|
|
56
58
|
export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
|
|
57
59
|
export type { LoopBlob } from './loop-blobs.generated.js';
|
|
58
60
|
export * from './sign.js';
|
|
@@ -127,7 +129,7 @@ export type {
|
|
|
127
129
|
TeachGuardResult,
|
|
128
130
|
} from './vector-tier.js';
|
|
129
131
|
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
|
|
130
|
-
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStatePath } from './statusline.js';
|
|
132
|
+
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
|
|
131
133
|
export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
|
|
132
134
|
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES } from './agentdb-index.js';
|
|
133
135
|
export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
|
|
@@ -491,6 +493,7 @@ export * from './session-retro.js';
|
|
|
491
493
|
export * from './feature-adr-setup.js';
|
|
492
494
|
export * from './challenge-panel.js';
|
|
493
495
|
export * from './routing-outcomes.js';
|
|
496
|
+
export * from './model-recommender.js';
|
|
494
497
|
export * from './bto-optimize.js';
|
|
495
498
|
export * from './discrimination-gate.js';
|
|
496
499
|
export * from './guard.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",
|