@dzhechkov/harness-core 0.7.11 → 0.8.1
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 +287 -87
- package/README.md +60 -1
- package/dist/amendment-trace.d.ts +12 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +22 -4
- package/dist/amendment-trace.js.map +1 -1
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +17 -0
- package/dist/book-kb.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +61 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +116 -2
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +69 -12
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +117 -7
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/learning-backend.d.ts +39 -0
- package/dist/learning-backend.d.ts.map +1 -1
- package/dist/learning-backend.js +31 -11
- package/dist/learning-backend.js.map +1 -1
- package/dist/lesson-bandit.d.ts +116 -0
- package/dist/lesson-bandit.d.ts.map +1 -0
- package/dist/lesson-bandit.js +235 -0
- package/dist/lesson-bandit.js.map +1 -0
- package/dist/lesson-payoff.d.ts +260 -0
- package/dist/lesson-payoff.d.ts.map +1 -0
- package/dist/lesson-payoff.js +597 -0
- package/dist/lesson-payoff.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +31 -8
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +57 -16
- package/dist/mutation-gate.js.map +1 -1
- package/dist/named-lock.d.ts.map +1 -1
- package/dist/named-lock.js +11 -9
- package/dist/named-lock.js.map +1 -1
- package/dist/native-dep-probe.d.ts +11 -0
- package/dist/native-dep-probe.d.ts.map +1 -0
- package/dist/native-dep-probe.js +100 -0
- package/dist/native-dep-probe.js.map +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +82 -1
- package/dist/operations.js.map +1 -1
- package/dist/parser-safe-region.d.ts +47 -0
- package/dist/parser-safe-region.d.ts.map +1 -0
- package/dist/parser-safe-region.js +47 -0
- package/dist/parser-safe-region.js.map +1 -0
- package/dist/patterns.d.ts +21 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +52 -4
- package/dist/patterns.js.map +1 -1
- package/dist/profile.d.ts +202 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +536 -0
- package/dist/profile.js.map +1 -0
- package/dist/project-skills-root.d.ts +44 -0
- package/dist/project-skills-root.d.ts.map +1 -0
- package/dist/project-skills-root.js +62 -0
- package/dist/project-skills-root.js.map +1 -0
- package/dist/publish.d.ts +5 -3
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +46 -8
- package/dist/publish.js.map +1 -1
- package/dist/qe-rounds.d.ts +75 -0
- package/dist/qe-rounds.d.ts.map +1 -0
- package/dist/qe-rounds.js +174 -0
- package/dist/qe-rounds.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -1
- package/dist/registry.js.map +1 -1
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +8 -1
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +35 -4
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +86 -23
- package/dist/setup.js.map +1 -1
- package/dist/sign.d.ts +29 -24
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +183 -61
- package/dist/sign.js.map +1 -1
- package/dist/store-location.d.ts +56 -0
- package/dist/store-location.d.ts.map +1 -0
- package/dist/store-location.js +56 -0
- package/dist/store-location.js.map +1 -0
- package/dist/store-lock.d.ts +1 -0
- package/dist/store-lock.d.ts.map +1 -1
- package/dist/store-lock.js +7 -1
- package/dist/store-lock.js.map +1 -1
- package/dist/store-merge.d.ts +55 -0
- package/dist/store-merge.d.ts.map +1 -0
- package/dist/store-merge.js +77 -0
- package/dist/store-merge.js.map +1 -0
- package/dist/teach-target.d.ts +61 -0
- package/dist/teach-target.d.ts.map +1 -0
- package/dist/teach-target.js +105 -0
- package/dist/teach-target.js.map +1 -0
- package/dist/vector-tier.d.ts +30 -0
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +178 -20
- package/dist/vector-tier.js.map +1 -1
- package/package.json +13 -13
- package/sbom.json +591 -87
- package/src/amendment-trace.ts +34 -4
- package/src/book-kb.ts +17 -0
- package/src/feature-adr-checkpoints.ts +111 -2
- package/src/feature-adr-routing.ts +146 -7
- package/src/index.ts +55 -3
- package/src/learning-backend.ts +62 -11
- package/src/lesson-bandit.ts +279 -0
- package/src/lesson-payoff.ts +728 -0
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +74 -17
- package/src/named-lock.ts +10 -8
- package/src/native-dep-probe.ts +118 -0
- package/src/operations.ts +77 -1
- package/src/parser-safe-region.ts +65 -0
- package/src/patterns.ts +76 -6
- package/src/profile.ts +593 -0
- package/src/project-skills-root.ts +63 -0
- package/src/publish.ts +43 -7
- package/src/qe-rounds.ts +226 -0
- package/src/registry.ts +5 -1
- package/src/run-records.ts +9 -1
- package/src/setup.ts +87 -23
- package/src/sign.ts +180 -60
- package/src/store-location.ts +76 -0
- package/src/store-lock.ts +8 -1
- package/src/store-merge.ts +91 -0
- package/src/teach-target.ts +126 -0
- package/src/vector-tier.ts +236 -21
package/src/amendment-trace.ts
CHANGED
|
@@ -26,6 +26,17 @@ export interface AmendmentRow {
|
|
|
26
26
|
export type AmendmentVerdict =
|
|
27
27
|
| 'resolved'
|
|
28
28
|
| 'placeholder'
|
|
29
|
+
/**
|
|
30
|
+
* The plan RETRACTED this amendment and named its successor. A first-class outcome, never a
|
|
31
|
+
* failure: a plan is allowed to change its mind, and until 2026-08-25 this checker had no concept
|
|
32
|
+
* of it at all (`grep superseded` over this file returned nothing), so a legitimately retracted
|
|
33
|
+
* amendment read as `unnamed` and failed the run. C6 has accepted the form since it was written —
|
|
34
|
+
* this is the half of the contract that was missing here.
|
|
35
|
+
*
|
|
36
|
+
* Honest limit, stated in ADR-001: nothing verifies that the named successor EXISTS, or that the
|
|
37
|
+
* retraction was justified. Refusing retraction outright is what produced the false failures.
|
|
38
|
+
*/
|
|
39
|
+
| 'superseded'
|
|
29
40
|
| 'unnamed'
|
|
30
41
|
| 'no-file-named'
|
|
31
42
|
| 'file-missing'
|
|
@@ -94,7 +105,11 @@ export function normalizeTestId(s: string): string {
|
|
|
94
105
|
* `features/ha-consilium/03.5_ideation_report.md` — is deliberately NOT a row: counting a legend as
|
|
95
106
|
* an amendment would open this feature by falsely accusing a feature that did nothing wrong.
|
|
96
107
|
*/
|
|
97
|
-
|
|
108
|
+
// The bullet is OPTIONAL, and `AM-CP-N` is a row like any other. MEASURED 2026-08-25: requiring a
|
|
109
|
+
// bullet drops 104 of the corpus's 347 real amendment rows out of the check entirely, and a
|
|
110
|
+
// bullet-less row made this tool return `not-established` — which the pipeline's own gate text calls
|
|
111
|
+
// "NEVER a pass". C6 has always treated the bullet as optional; this is that half of the contract.
|
|
112
|
+
const ROW_START = /^(?:[-*|]\s*)?\*{0,2}AM-(?:CP-)?(\d+)/gm;
|
|
98
113
|
|
|
99
114
|
/** The `## Amendments` section body, or null when the document has none (acid case A5). */
|
|
100
115
|
export function amendmentSection(md: string): string | null {
|
|
@@ -131,10 +146,15 @@ export function parseAmendments(md: string): AmendmentRow[] {
|
|
|
131
146
|
return rows;
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
/** `superseded by AM-N` / `AM-CP-N` — the retraction form C6 has always accepted. */
|
|
150
|
+
const SUPERSEDED = /superseded by AM-(?:CP-)?\d+/i;
|
|
151
|
+
|
|
134
152
|
/** `→ test \`a\`` and the two-id shape `→ tests \`a\` and \`b\`` — both are in the corpus. */
|
|
135
153
|
function extractTestIds(raw: string): string[] {
|
|
136
154
|
const out: string[] = [];
|
|
137
|
-
|
|
155
|
+
// Both arrows. C6 accepted `->` from the start and this file only accepted `→`, so an ASCII row
|
|
156
|
+
// read as `unnamed` here while passing there — an accident of two authors, not a decision.
|
|
157
|
+
const re = /(?:\u2192|->)\s*tests?\s+`([^`]+)`(?:\s*(?:and|и)\s*`([^`]+)`)?/g;
|
|
138
158
|
for (let m = re.exec(raw); m !== null; m = re.exec(raw)) {
|
|
139
159
|
if (m[1]) out.push(m[1].trim());
|
|
140
160
|
if (m[2]) out.push(m[2].trim());
|
|
@@ -148,7 +168,7 @@ function extractTestIds(raw: string): string[] {
|
|
|
148
168
|
* successive shell-written extractors returned 108, 111 and 13 rows over the identical corpus.
|
|
149
169
|
*/
|
|
150
170
|
function extractFile(raw: string): string | null {
|
|
151
|
-
const m =
|
|
171
|
+
const m = /(?:\u2192|->)\s*tests?\s+`[^`]+`(?:\s*(?:and|и)\s*`[^`]+`)?[\s\S]{0,40}?\bin\s+`([^`]+)`/.exec(raw);
|
|
152
172
|
return m && m[1] ? m[1].trim() : null;
|
|
153
173
|
}
|
|
154
174
|
|
|
@@ -158,6 +178,12 @@ export function resolveAmendments(
|
|
|
158
178
|
): AmendmentResolution[] {
|
|
159
179
|
const out: AmendmentResolution[] = [];
|
|
160
180
|
for (const row of rows) {
|
|
181
|
+
// A retraction is checked BEFORE the missing-pointer branch: a row that says it was superseded
|
|
182
|
+
// is not a row that forgot its test.
|
|
183
|
+
if (SUPERSEDED.test(row.raw)) {
|
|
184
|
+
out.push({ id: row.id, testId: null, file: row.file, verdict: 'superseded', detail: 'the plan retracted this amendment and named its successor' });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
161
187
|
if (row.testIds.length === 0) {
|
|
162
188
|
out.push({
|
|
163
189
|
id: row.id,
|
|
@@ -219,6 +245,7 @@ function resolveOne(
|
|
|
219
245
|
const ZERO_COUNTS: Record<AmendmentVerdict, number> = {
|
|
220
246
|
resolved: 0,
|
|
221
247
|
placeholder: 0,
|
|
248
|
+
superseded: 0,
|
|
222
249
|
unnamed: 0,
|
|
223
250
|
'no-file-named': 0,
|
|
224
251
|
'file-missing': 0,
|
|
@@ -282,7 +309,10 @@ export function decideAmendmentOutcome(input: {
|
|
|
282
309
|
);
|
|
283
310
|
}
|
|
284
311
|
for (const r of input.resolutions) {
|
|
285
|
-
|
|
312
|
+
// `superseded` is an OUTCOME, not a defect: the plan retracted the amendment and said so. It is
|
|
313
|
+
// reported in the counts and never becomes a reason, which is the whole point — refusing
|
|
314
|
+
// retraction is what produced the false failures this change removes.
|
|
315
|
+
if (r.verdict !== 'resolved' && r.verdict !== 'superseded') reasons.push(`${r.id} → ${r.verdict}: ${r.detail}`);
|
|
286
316
|
}
|
|
287
317
|
return { outcome: reasons.length > 0 ? 'fail' : 'pass', exit: reasons.length > 0 ? 1 : 0, reasons, counts };
|
|
288
318
|
}
|
package/src/book-kb.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
|
+
import { describeNativeDep, exerciseSqliteOpen, probeNativeDep } from './native-dep-probe.js';
|
|
20
|
+
|
|
19
21
|
/** A digitized Knowledge Unit as stored in the book KB. */
|
|
20
22
|
export interface BookKU {
|
|
21
23
|
readonly book: string; // ISBN or book slug (the immutable upstream key)
|
|
@@ -64,6 +66,11 @@ const SCHEMA = `CREATE VIRTUAL TABLE IF NOT EXISTS book_knowledge USING fts5(
|
|
|
64
66
|
);`;
|
|
65
67
|
|
|
66
68
|
async function openDb(projectRoot: string, dbPath?: string): Promise<NativeDb | { error: string }> {
|
|
69
|
+
const verdict = probeNativeDep(projectRoot, 'better-sqlite3', exerciseSqliteOpen, 'sqlite-open');
|
|
70
|
+
if (verdict.state === 'absent') {
|
|
71
|
+
return { error: 'better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
|
|
72
|
+
}
|
|
73
|
+
if (verdict.state === 'unusable') return { error: describeNativeDep(verdict) };
|
|
67
74
|
let sqliteUrl: string;
|
|
68
75
|
try {
|
|
69
76
|
const req = createRequire(join(projectRoot, 'package.json'));
|
|
@@ -71,6 +78,16 @@ async function openDb(projectRoot: string, dbPath?: string): Promise<NativeDb |
|
|
|
71
78
|
} catch {
|
|
72
79
|
return { error: 'better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
|
|
73
80
|
}
|
|
81
|
+
// The probe above absorbs the DEPENDENCY failures — absent, or present-but-unloadable — which is
|
|
82
|
+
// what cross-family review (F2) required: those are instrument failures and must be RETURNED.
|
|
83
|
+
//
|
|
84
|
+
// What follows must still THROW, and that is deliberate. `dz recall --books` distinguishes two
|
|
85
|
+
// states by throw-vs-return, and collapsing them loses a shipped honesty contract (measured
|
|
86
|
+
// 2026-08-27, `books-recall-honesty.test.ts`):
|
|
87
|
+
// throws -> the STORE ITSELF is unreadable -> exit 1, naming the path and a cure
|
|
88
|
+
// {error} -> the INSTRUMENT did not run -> exit 3 under --semantic, never a clean 0-hit
|
|
89
|
+
// A corrupted `books.sqlite` is the first state. Wrapping it into `{error}` made a corrupt store
|
|
90
|
+
// read as a missing instrument and exit 0 — a false clean pass over unreadable data.
|
|
74
91
|
const path = dbPath ?? bookKbPath(projectRoot);
|
|
75
92
|
mkdirSync(dirname(path), { recursive: true });
|
|
76
93
|
const { default: Database } = (await import(sqliteUrl)) as { default: new (p: string) => NativeDb };
|
|
@@ -497,6 +497,113 @@ export const TRAINPAIR_PRIVACY_NOTE =
|
|
|
497
497
|
/** Coerce a stage input/output to text: strings pass through; objects serialize to JSON;
|
|
498
498
|
* an unserializable value degrades to String(v) — buildTrainingPair NEVER throws (capture
|
|
499
499
|
* is non-blocking by contract). */
|
|
500
|
+
/** Marker pair of the operator-profile block (feature operator-profile, ADR-001 Decision 5).
|
|
501
|
+
* DELIBERATE LOCAL COPIES of profile.ts's PROFILE_MARKER_START/END: this module stays import-free
|
|
502
|
+
* so the workflow can mirror it inline. `test/profile-redaction.test.ts` pins the pairs equal. */
|
|
503
|
+
export const TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';
|
|
504
|
+
export const TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';
|
|
505
|
+
|
|
506
|
+
/** What a redacted block is replaced with — visible in the dataset, so a missing profile is
|
|
507
|
+
* distinguishable from a never-present one. */
|
|
508
|
+
export const TP_PROFILE_REDACTED = '[dz:profile REDACTED]';
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Strip every operator-profile block from `text` BEFORE a training pair is persisted.
|
|
512
|
+
*
|
|
513
|
+
* Why here and not "the guard already says never write the profile into a project": training-pair
|
|
514
|
+
* capture records the FULL prompt as the model received it into `.dz/fa-training/`, which is
|
|
515
|
+
* deliberately NOT gitignored — so a profile injected into context would reach a committable
|
|
516
|
+
* directory, and the never-in-a-project guard would be defeated through this path (ADR-001
|
|
517
|
+
* Decision 5, exit 3). Redaction at the single assembly seam closes it for capture AND backfill.
|
|
518
|
+
*
|
|
519
|
+
* Semantics: every complete `start…end` span is replaced (markers included) with
|
|
520
|
+
* {@link TP_PROFILE_REDACTED}. A START marker with no matching END fails CLOSED — everything from
|
|
521
|
+
* the marker to the end of the text is dropped (over-redaction is a lost training sample;
|
|
522
|
+
* under-redaction is a personal-data leak). Text without markers passes through byte-identical.
|
|
523
|
+
* Works on JSON-stringified payloads too: the marker literals contain no characters JSON escapes.
|
|
524
|
+
*/
|
|
525
|
+
export function redactProfileBlock(text: string): string {
|
|
526
|
+
if (typeof text !== 'string' || text === '') return typeof text === 'string' ? text : '';
|
|
527
|
+
let out = '';
|
|
528
|
+
let rest = text;
|
|
529
|
+
for (;;) {
|
|
530
|
+
const start = rest.indexOf(TP_PROFILE_MARKER_START);
|
|
531
|
+
if (start === -1) return out + rest;
|
|
532
|
+
out += rest.slice(0, start) + TP_PROFILE_REDACTED;
|
|
533
|
+
const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);
|
|
534
|
+
if (end === -1) return out; // unterminated block: fail closed, drop the tail
|
|
535
|
+
rest = rest.slice(end + TP_PROFILE_MARKER_END.length);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Deep redaction over an already-PARSED training-pair payload — the PERSIST-SIDE half of CF-6.
|
|
541
|
+
*
|
|
542
|
+
* Why a second entry point next to {@link redactProfileBlock}: the DEFAULT-ON capture in the
|
|
543
|
+
* canonical workflow builds its pair with an INLINE mirror of buildTrainingPair and hands the
|
|
544
|
+
* serialised JSON to `dz feature-adr-record` — a path that never passes through the core builder.
|
|
545
|
+
* Redacting at the builder alone therefore guarded the path that does NOT run (Codex cross-family
|
|
546
|
+
* finding, 2026-08-28: guard 1 defeated through guard 3, one seam further down — the exact shape
|
|
547
|
+
* ADR-001 Decision 5 names). This function runs inside `decideRecordWrite` (run-records.ts), the
|
|
548
|
+
* one decision every witnessed training-pair write funnels through, so a FUTURE pair builder is
|
|
549
|
+
* covered without patching its caller.
|
|
550
|
+
*
|
|
551
|
+
* Semantics: every string leaf (keys included) goes through {@link redactProfileBlock} — same
|
|
552
|
+
* fail-closed rule on an unterminated block; arrays and plain objects are walked; numbers,
|
|
553
|
+
* booleans and null pass through untouched. A payload with no markers anywhere round-trips to a
|
|
554
|
+
* deep-equal value.
|
|
555
|
+
*
|
|
556
|
+
* Two mechanics, both cross-family findings (2026-08-28), both load-bearing:
|
|
557
|
+
*
|
|
558
|
+
* - Rebuilt objects have a NULL prototype, so every JSON key — `__proto__` included — lands as an
|
|
559
|
+
* OWN property. The previous `{}` + assignment invoked the inherited `__proto__` SETTER for a
|
|
560
|
+
* payload like `{"slug":"s","stage":"code","__proto__":{"input":"i","output":"o"}}`: the result
|
|
561
|
+
* then INHERITED input/output (the shape check passed) while serialization dropped them — an
|
|
562
|
+
* invalid pair reported `written`. With own-key reconstruction the JSON keys round-trip exactly
|
|
563
|
+
* and that payload fails the shape check honestly.
|
|
564
|
+
* - The walk is ITERATIVE (explicit stack), not recursive: ~5000 nested arrays is a ~10 KB payload
|
|
565
|
+
* that passes JSON.parse and sits under the line cap, but a recursive map hit RangeError before
|
|
566
|
+
* any size guard — a throw escaping a seam whose callers promise non-blocking verdicts. The
|
|
567
|
+
* iterative walk chose over a caught-RangeError→`refused` wrapper because it keeps the honest
|
|
568
|
+
* outcome for deep-but-valid payloads (they get redacted and judged on their merits) instead of
|
|
569
|
+
* refusing them at an arbitrary engine-dependent depth. A repeated container is walked once and
|
|
570
|
+
* reused (WeakMap), so a shared or cyclic reference can never loop the walk either.
|
|
571
|
+
*/
|
|
572
|
+
export function redactTrainingPayload(v: unknown): unknown {
|
|
573
|
+
if (typeof v === 'string') return redactProfileBlock(v);
|
|
574
|
+
if (v === null || typeof v !== 'object') return v;
|
|
575
|
+
const makeDst = (src: object): Record<string, unknown> | unknown[] =>
|
|
576
|
+
Array.isArray(src) ? new Array(src.length) : (Object.create(null) as Record<string, unknown>);
|
|
577
|
+
const dstOf = new WeakMap<object, Record<string, unknown> | unknown[]>();
|
|
578
|
+
const root = makeDst(v);
|
|
579
|
+
dstOf.set(v, root);
|
|
580
|
+
const stack: object[] = [v];
|
|
581
|
+
const walkChild = (child: unknown): unknown => {
|
|
582
|
+
if (typeof child === 'string') return redactProfileBlock(child);
|
|
583
|
+
if (child === null || typeof child !== 'object') return child;
|
|
584
|
+
let dst = dstOf.get(child);
|
|
585
|
+
if (dst === undefined) {
|
|
586
|
+
dst = makeDst(child);
|
|
587
|
+
dstOf.set(child, dst);
|
|
588
|
+
stack.push(child);
|
|
589
|
+
}
|
|
590
|
+
return dst;
|
|
591
|
+
};
|
|
592
|
+
while (stack.length > 0) {
|
|
593
|
+
const src = stack.pop()!;
|
|
594
|
+
const dst = dstOf.get(src)!;
|
|
595
|
+
if (Array.isArray(src)) {
|
|
596
|
+
const arr = dst as unknown[];
|
|
597
|
+
for (let i = 0; i < src.length; i++) arr[i] = walkChild(src[i]);
|
|
598
|
+
} else {
|
|
599
|
+
const obj = dst as Record<string, unknown>;
|
|
600
|
+
// null prototype ⇒ this assignment defines an OWN property even for the key '__proto__'
|
|
601
|
+
for (const [key, value] of Object.entries(src)) obj[redactProfileBlock(key)] = walkChild(value);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return root;
|
|
605
|
+
}
|
|
606
|
+
|
|
500
607
|
function coerceText(v: unknown): string {
|
|
501
608
|
if (typeof v === 'string') return v;
|
|
502
609
|
if (v === null || v === undefined) return '';
|
|
@@ -526,8 +633,10 @@ export function buildTrainingPair(opts: {
|
|
|
526
633
|
captureMode?: unknown;
|
|
527
634
|
resumed?: unknown;
|
|
528
635
|
}): TrainingPair {
|
|
529
|
-
|
|
530
|
-
|
|
636
|
+
// Operator-profile redaction FIRST — before the oversize guard, so the truncation hashes are
|
|
637
|
+
// hashes of the redacted text and the full-text fnv1a64 never fingerprints personal data.
|
|
638
|
+
let input = redactProfileBlock(coerceText(opts.input));
|
|
639
|
+
let output = redactProfileBlock(coerceText(opts.output));
|
|
531
640
|
let truncated: TrainingPairTruncation | null = null;
|
|
532
641
|
if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {
|
|
533
642
|
truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };
|
|
@@ -1321,9 +1321,36 @@ export function isSafeCodexId(id: string): boolean {
|
|
|
1321
1321
|
* unquoted, so a malformed `args.codexModel` could corrupt or extend the command the agent runs.
|
|
1322
1322
|
* Reject anything that is not a plain id, and single-quote it anyway.
|
|
1323
1323
|
*/
|
|
1324
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Which binary bounds a dispatched run. `timeout(1)` is GNU coreutils and is NOT on macOS; brew's
|
|
1326
|
+
* coreutils installs it as `gtimeout`.
|
|
1327
|
+
*
|
|
1328
|
+
* An ALLOWLIST rather than a free string, because this value is interpolated into a shell command.
|
|
1329
|
+
*
|
|
1330
|
+
* MEASURED 2026-08-25 (field report, and reproduced here): with neither binary present the dispatch
|
|
1331
|
+
* exits 127 and cross-family QE — a NAMED safety property, that the model which wrote the code must
|
|
1332
|
+
* not review it — silently did not happen for a whole run.
|
|
1333
|
+
*
|
|
1334
|
+
* Deliberately NOT the portable `perl -e 'alarm N; exec @ARGV'` the report proposed: MEASURED on
|
|
1335
|
+
* this machine, that form exits **142** (SIGALRM kills the exec'd process) while GNU timeout exits
|
|
1336
|
+
* **124**, and `classifyCodexQeOutcome` keys `timeout` on `exit === 124` — a rule its own comment
|
|
1337
|
+
* says wins over every content rule. The suggested remedy would have silently reclassified every
|
|
1338
|
+
* timeout as a tool error. A remedy that breaks the classifier is worse than the defect.
|
|
1339
|
+
*/
|
|
1340
|
+
export const TIMEOUT_BINS: Readonly<Record<string, true>> = { timeout: true, gtimeout: true };
|
|
1341
|
+
|
|
1342
|
+
/** The requested timeout binary if it is one we allow, else the default. Never a free string. */
|
|
1343
|
+
export function timeoutBinOrDefault(bin: unknown): string {
|
|
1344
|
+
const b = typeof bin === 'string' ? bin : '';
|
|
1345
|
+
return TIMEOUT_BINS[b] === true ? b : 'timeout';
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
export function codexProbeCommand(id: string, timeoutBin?: string): string | null {
|
|
1325
1349
|
if (!isSafeCodexId(id)) return null;
|
|
1326
|
-
|
|
1350
|
+
// `< /dev/null` is not cosmetic: without it `codex exec` waits on stdin forever (measured
|
|
1351
|
+
// 2026-08-19, 45 minutes lost). It was present in the workflow mirror and MISSING here — a drift
|
|
1352
|
+
// invisible to the suite because this function is not in the lift test's function list.
|
|
1353
|
+
return timeoutBinOrDefault(timeoutBin) + " 60 codex exec -m '" + id + "' 'Reply with exactly: OK' < /dev/null";
|
|
1327
1354
|
}
|
|
1328
1355
|
|
|
1329
1356
|
export interface CodexProbeOutput {
|
|
@@ -1396,6 +1423,9 @@ export function parseCodexExecResult(text: string | null | undefined): CodexExec
|
|
|
1396
1423
|
|
|
1397
1424
|
/** Mode-A wall-clock bound. Run 3 measured 146 s; 600 s is ~4× headroom and still bounded. */
|
|
1398
1425
|
export const CODEX_REVIEW_TIMEOUT_SECONDS = 600;
|
|
1426
|
+
|
|
1427
|
+
/** The `codex exec` wall-clock bound. Mirrors the workflow's own constant (field report 27). */
|
|
1428
|
+
export const CODEX_EXEC_TIMEOUT_SECONDS = 280;
|
|
1399
1429
|
/** MEASURED (probe 0.3): `codex review` accepts and echoes `reasoning effort: high`. */
|
|
1400
1430
|
export const CODEX_REVIEW_DEFAULT_EFFORT = 'high';
|
|
1401
1431
|
/** The sentinel a wrapper returns when the command hit its `timeout` — distinct from CODEX_UNAVAILABLE. */
|
|
@@ -1427,6 +1457,22 @@ export interface CodexReviewCommandInput {
|
|
|
1427
1457
|
readonly ref?: string | null;
|
|
1428
1458
|
readonly modelId?: string | null;
|
|
1429
1459
|
readonly reasoning?: string | null;
|
|
1460
|
+
/**
|
|
1461
|
+
* Which binary bounds the run — `timeout` (default) or `gtimeout` on a mac with brew coreutils.
|
|
1462
|
+
* An INPUT rather than a platform sniff inside the builder, so the function stays pure and every
|
|
1463
|
+
* pinned command string in the tests stays byte-identical when it is omitted.
|
|
1464
|
+
*/
|
|
1465
|
+
readonly timeoutBin?: string | null;
|
|
1466
|
+
/**
|
|
1467
|
+
* The repo the review must run IN. Field report 27: `codex review` was dispatched with no working
|
|
1468
|
+
* directory at all, so it ran in the SESSION cwd — on a run against an external checkout it read a
|
|
1469
|
+
* different tree, resolved `--uncommitted` / `--base` against the wrong git repo, and still exited 0
|
|
1470
|
+
* with a `Grade:` line that the pipeline recorded as a verdict. `codex review` has NO `-C` flag
|
|
1471
|
+
* (MEASURED on codex-cli 0.149.1: `codex exec --help` carries `-C, --cd <DIR>`, `codex review --help`
|
|
1472
|
+
* does not), so the working directory can only be set by a `cd` prefix. Omitted ⇒ no prefix, which
|
|
1473
|
+
* keeps every previously pinned command string byte-identical.
|
|
1474
|
+
*/
|
|
1475
|
+
readonly repo?: string | null;
|
|
1430
1476
|
/**
|
|
1431
1477
|
* Accepted and DELIBERATELY IGNORED. MEASURED 2026-08-21: every scope flag refuses a positional
|
|
1432
1478
|
* prompt — `--commit`, `--base <BRANCH>` and `--uncommitted` each exit 2 with
|
|
@@ -1457,6 +1503,44 @@ export interface CodexReviewCommandResult {
|
|
|
1457
1503
|
* Default scope is `uncommitted` (probe 0.4b: it and `--base HEAD` reviewed the identical uncommitted
|
|
1458
1504
|
* diff, and `uncommitted` needs no ref, so it has no ref-injection surface at all).
|
|
1459
1505
|
*/
|
|
1506
|
+
/** Shell-quote one argument. A repo path may contain a space; it must never contain a command. */
|
|
1507
|
+
function codexSq(s: string): string {
|
|
1508
|
+
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* The `cd <repo> && ` prefix, or `''` when no repo was named. Empty is not a silent default — it is
|
|
1513
|
+
* the pre-2026-08-25 behaviour, preserved so an omitted `repo` keeps every pinned string identical.
|
|
1514
|
+
*/
|
|
1515
|
+
function codexCd(repo: string): string {
|
|
1516
|
+
return repo === '' ? '' : 'cd ' + codexSq(repo) + ' && ';
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* The `codex exec` dispatch, built as a pure string so the working directory is pinned by a test
|
|
1521
|
+
* rather than by whichever directory the dispatching agent happened to stand in (field report 27).
|
|
1522
|
+
* `-C` is real on codex-cli 0.149.1 and MEASURED: `-C <this repo>` answered in 4.6 s exit 0, while
|
|
1523
|
+
* `-C /tmp` exited 1 with "Not inside a trusted directory" — so the flag genuinely changes the tree,
|
|
1524
|
+
* and a non-git target is a LOUD failure rather than a quiet read of the wrong one.
|
|
1525
|
+
*/
|
|
1526
|
+
export function codexExecCommand(input: {
|
|
1527
|
+
readonly modelId?: string | null;
|
|
1528
|
+
readonly prompt?: string | null;
|
|
1529
|
+
readonly timeoutBin?: string | null;
|
|
1530
|
+
readonly timeoutSeconds?: number | null;
|
|
1531
|
+
readonly repo?: string | null;
|
|
1532
|
+
}): string | null {
|
|
1533
|
+
const o = input || {};
|
|
1534
|
+
const modelId = String(o.modelId === undefined || o.modelId === null ? '' : o.modelId);
|
|
1535
|
+
if (!isSafeCodexId(modelId)) return null;
|
|
1536
|
+
const raw = Number(o.timeoutSeconds);
|
|
1537
|
+
const seconds = raw === raw && raw !== Infinity && raw > 0 ? Math.floor(raw) : CODEX_EXEC_TIMEOUT_SECONDS;
|
|
1538
|
+
const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo);
|
|
1539
|
+
const cd = repo === '' ? '' : ' -C ' + codexSq(repo);
|
|
1540
|
+
return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd
|
|
1541
|
+
+ ' -m ' + codexSq(modelId) + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null';
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1460
1544
|
export function codexReviewCommand(input: CodexReviewCommandInput): CodexReviewCommandResult {
|
|
1461
1545
|
const o = input || ({} as CodexReviewCommandInput);
|
|
1462
1546
|
const scope = o.scope === undefined || o.scope === null || o.scope === '' ? 'uncommitted' : String(o.scope);
|
|
@@ -1477,7 +1561,8 @@ export function codexReviewCommand(input: CodexReviewCommandInput): CodexReviewC
|
|
|
1477
1561
|
}
|
|
1478
1562
|
const raw = Number(o.timeoutSeconds);
|
|
1479
1563
|
const seconds = raw === raw && raw !== Infinity && raw > 0 ? Math.floor(raw) : CODEX_REVIEW_TIMEOUT_SECONDS;
|
|
1480
|
-
|
|
1564
|
+
const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo);
|
|
1565
|
+
let cmd = codexCd(repo) + timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + " codex review -c model='" + modelId + "' -c model_reasoning_effort='" + effort + "'";
|
|
1481
1566
|
if (scope === 'commit') cmd += " --commit '" + ref + "'";
|
|
1482
1567
|
else if (scope === 'base') cmd += " --base '" + ref + "'";
|
|
1483
1568
|
else cmd += ' --uncommitted';
|
|
@@ -1662,7 +1747,7 @@ export function gradeFromReviewFindings(findings: readonly CodexQeFinding[] | nu
|
|
|
1662
1747
|
* The LOCKED decline taxonomy. A `kind` outside this set is a bug, not a new case — which is why
|
|
1663
1748
|
* {@link codexQeDeclineReason} throws on one rather than rendering something plausible.
|
|
1664
1749
|
*/
|
|
1665
|
-
export const CODEX_QE_DECLINE_KINDS: readonly string[] = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling'];
|
|
1750
|
+
export const CODEX_QE_DECLINE_KINDS: readonly string[] = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling', 'wrong-tree'];
|
|
1666
1751
|
|
|
1667
1752
|
export interface ClassifyCodexQeInput {
|
|
1668
1753
|
readonly exit?: number | null;
|
|
@@ -1675,6 +1760,49 @@ export interface ClassifyCodexQeInput {
|
|
|
1675
1760
|
* that was never wrapped (a saved fixture, a file on disk), where content is all there is.
|
|
1676
1761
|
*/
|
|
1677
1762
|
readonly signalExpected?: boolean;
|
|
1763
|
+
/**
|
|
1764
|
+
* The files the dispatch DECLARED it would review. Used only by the wrong-tree rule below; absent
|
|
1765
|
+
* ⇒ that rule cannot fire and the classifier behaves exactly as it did before field report 27.
|
|
1766
|
+
*/
|
|
1767
|
+
readonly declaredFiles?: readonly string[] | null;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* Did the reviewer fail to FIND the very files it was told to read?
|
|
1772
|
+
*
|
|
1773
|
+
* A review dispatched into the wrong working directory does not error: the paths simply are not
|
|
1774
|
+
* there, the model says so in prose, and the command still exits 0 — often with a `Grade:` line,
|
|
1775
|
+
* which the pipeline then records as a verdict about code nobody read.
|
|
1776
|
+
*
|
|
1777
|
+
* The rule is deliberately narrow, because a review of a file-handling module may legitimately
|
|
1778
|
+
* DISCUSS "No such file or directory" — a mention is not a claim. So it fires only when one LINE
|
|
1779
|
+
* carries both the not-found phrase and one of the declared paths. With no declared paths there is
|
|
1780
|
+
* nothing to discriminate against and the rule stays silent rather than guessing.
|
|
1781
|
+
*/
|
|
1782
|
+
export function codexReviewMissedItsFiles(body: string | null | undefined, declaredFiles?: readonly string[] | null): boolean {
|
|
1783
|
+
const text = String(body === undefined || body === null ? '' : body);
|
|
1784
|
+
const files = Array.isArray(declaredFiles) ? declaredFiles.filter((f) => typeof f === 'string' && f !== '') : [];
|
|
1785
|
+
if (text === '' || files.length === 0) return false;
|
|
1786
|
+
// Only quotes, whitespace and a colon may sit between the path and the failure. Prose may not —
|
|
1787
|
+
// and that single restriction is what separates "the tool could not open this path" from "this
|
|
1788
|
+
// finding is ABOUT this path": a review finding always names its file, so anything looser marks
|
|
1789
|
+
// every file-handling review as wrong-tree. (Codex, gpt-5.6-sol, on the first version of this
|
|
1790
|
+
// function: the finding line "- [P2] Do not swallow file not found - src/io.ts:42" plus a stated
|
|
1791
|
+
// grade C was classified wrong-tree, discarding a valid cross-family verdict and falling back to
|
|
1792
|
+
// same-family QE — the guard against a false-clean review destroying a true one.)
|
|
1793
|
+
const GAP = '["\'\u2018\u2019\u201c\u201d\u0060(\\[\\s:,]{0,4}';
|
|
1794
|
+
const NOT_FOUND = 'no such file or directory|file not found|not found|does not exist|is not present|cannot be found';
|
|
1795
|
+
const VERB = '(?:cannot|can\'t|could not|couldn\'t|unable to|failed to|error(?: while)?)\\s+(?:open|read|find|access|stat|locate|load)';
|
|
1796
|
+
for (const f of files) {
|
|
1797
|
+
const q = f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1798
|
+
const shapes = [
|
|
1799
|
+
q + GAP + '(?:' + NOT_FOUND + ')', // src/io.ts: No such file or directory
|
|
1800
|
+
'(?:' + NOT_FOUND + ')' + GAP + q, // No such file or directory: src/io.ts
|
|
1801
|
+
VERB + GAP + q, // cannot open 'src/io.ts'
|
|
1802
|
+
];
|
|
1803
|
+
for (const shape of shapes) if (new RegExp(shape, 'i').test(text)) return true;
|
|
1804
|
+
}
|
|
1805
|
+
return false;
|
|
1678
1806
|
}
|
|
1679
1807
|
|
|
1680
1808
|
/**
|
|
@@ -1711,6 +1839,10 @@ export function classifyCodexQeOutcome(input: ClassifyCodexQeInput): { kind: str
|
|
|
1711
1839
|
} else if (exit !== 0) {
|
|
1712
1840
|
return { kind: 'tool-error' };
|
|
1713
1841
|
}
|
|
1842
|
+
// BEFORE the grade rule, and deliberately so: a wrong-tree review usually DOES state a grade, and
|
|
1843
|
+
// that grade is the most dangerous output this pipeline can produce — a clean letter about code
|
|
1844
|
+
// nobody read, recorded while crossFamilyQe.happened stays true.
|
|
1845
|
+
if (codexReviewMissedItsFiles(body, input ? input.declaredFiles : null)) return { kind: 'wrong-tree' };
|
|
1714
1846
|
if (grade !== null) return { kind: 'verdict' };
|
|
1715
1847
|
return { kind: 'no-verdict' };
|
|
1716
1848
|
}
|
|
@@ -1750,6 +1882,9 @@ export function codexQeDeclineReason(kind: string | null | undefined, detail?: C
|
|
|
1750
1882
|
const exit = d.exit === undefined || d.exit === null ? '?' : String(d.exit);
|
|
1751
1883
|
const chars = d.chars === undefined || d.chars === null ? '?' : String(d.chars);
|
|
1752
1884
|
const extra = d.detail === undefined || d.detail === null || String(d.detail) === '' ? 'no detail' : String(d.detail);
|
|
1885
|
+
if (canonical === 'wrong-tree') {
|
|
1886
|
+
return 'codex reported that the declared file(s) do not exist — the review ran in the WRONG working directory and its verdict is about a tree nobody asked for; ' + files + ' file(s) declared, exit ' + exit + ' (' + extra + ')';
|
|
1887
|
+
}
|
|
1753
1888
|
if (canonical === 'timeout') {
|
|
1754
1889
|
return 'codex review timed out after ' + elapsed + 's on scope ' + ref + ' (' + files + ' files) — NARROW the scope (this is reconnaissance cost, not thinking time)';
|
|
1755
1890
|
}
|
|
@@ -1763,7 +1898,11 @@ export function codexQeDeclineReason(kind: string | null | undefined, detail?: C
|
|
|
1763
1898
|
return 'codex exec unusable — ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'codex exec returned no text' : String(d.reason));
|
|
1764
1899
|
}
|
|
1765
1900
|
if (canonical === 'unavailable') {
|
|
1766
|
-
|
|
1901
|
+
// The detail is the ONE field that carries the shell error, and this branch used to drop it
|
|
1902
|
+
// while `tool-error` right above rendered it — an asymmetry that made the field report's
|
|
1903
|
+
// "codex exec reported it could not run" unfixable blind. Same `extra`, same shape.
|
|
1904
|
+
const why = d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'codex exec reported it could not run' : String(d.reason);
|
|
1905
|
+
return 'codex not used — ' + why + (extra === 'no detail' ? '' : ' (' + extra + ')');
|
|
1767
1906
|
}
|
|
1768
1907
|
if (canonical === 'over-ceiling') {
|
|
1769
1908
|
return 'prompt is ' + chars + ' chars / unscoped — refused before dispatch';
|
|
@@ -1804,12 +1943,12 @@ export interface CodexReviewResult {
|
|
|
1804
1943
|
* wrapper and calls {@link classifyCodexQeOutcome} directly with `signalExpected: true`, so a
|
|
1805
1944
|
* swallowed sentinel there is a `tool-error` and never a pass. A wiring test pins that.
|
|
1806
1945
|
*/
|
|
1807
|
-
export function parseCodexReviewResult(text: string | null | undefined): CodexReviewResult {
|
|
1946
|
+
export function parseCodexReviewResult(text: string | null | undefined, declaredFiles?: readonly string[] | null): CodexReviewResult {
|
|
1808
1947
|
const sig = parseCodexReviewSignal(text);
|
|
1809
1948
|
const findings = parseCodexReviewFindings(sig.body);
|
|
1810
1949
|
const stated = parseCodexGrade(sig.body);
|
|
1811
1950
|
const grade = stated !== null ? stated : gradeFromReviewFindings(findings);
|
|
1812
|
-
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: sig.signalPresent });
|
|
1951
|
+
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: sig.signalPresent, declaredFiles: declaredFiles === undefined ? null : declaredFiles });
|
|
1813
1952
|
const kind = outcome.kind === 'unusable-output' ? 'empty' : outcome.kind;
|
|
1814
1953
|
const ok = kind === 'verdict';
|
|
1815
1954
|
const reason = ok ? null : codexQeDeclineReason(kind, { elapsedSeconds: sig.elapsedSeconds, exit: sig.exit, chars: sig.body.length });
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,9 @@ export { stampCheckpointLine } from './checkpoint-stamp.js';
|
|
|
81
81
|
export { TELEMETRY_VOCAB_VERSION, TELEMETRY_FIELDS, PROVISIONAL_TELEMETRY_FIELDS, LOCAL_FIELD_ALIASES, telemetryFieldFor } from './telemetry-vocabulary.js';
|
|
82
82
|
export type { TelemetryField, FieldSource } from './telemetry-vocabulary.js';
|
|
83
83
|
export { planLedgerBackfill, LEDGER_FILL_SOURCE, AMBIGUOUS, resolveLedgerRunId } from './ledger-backfill.js';
|
|
84
|
+
// project-skills root resolution (field report doc-25b): the ONE builder behind both the Step-0
|
|
85
|
+
// probe and the PS_GUIDANCE paragraph, so the two can never look at different roots again.
|
|
86
|
+
export { projectSkillsOneRoot, projectSkillsProbeCommand } from './project-skills-root.js';
|
|
84
87
|
export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
|
|
85
88
|
export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
|
|
86
89
|
export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
@@ -119,12 +122,18 @@ export {
|
|
|
119
122
|
} from './qe-bridge.js';
|
|
120
123
|
export type { BridgeFamily, BridgeFailureReason, BridgeFinding, BridgeSignoff, BridgeParse, BridgeParseOk, BridgeParseFail, BridgeChannels, BridgeAudit, ClaudeResultExtraction, NamedExtract, BridgePromptInput } from './qe-bridge.js';
|
|
121
124
|
export type { PatternRecord, SessionRecord, LearningConfig, MemoryLearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult, ReinforcementState, ReinforcePatternResult, StoreStats, LessonDeltaReport, LessonDeltaRow, QuarantineState, PromoteResult, QuarantineExpiryCandidate } from './patterns.js';
|
|
122
|
-
export { DEFAULT_REINFORCE_THRESHOLD, NoopLearningBackend, NativeReinforcementBackend, resolveLearningBackend, isLearningSignalBackend } from './learning-backend.js';
|
|
123
|
-
export type { LearningSignalBackend, LearningSignalStats, LearningSample, SignalCandidate, EnhanceContext, TrainingResult, LearningBackendMode } from './learning-backend.js';
|
|
125
|
+
export { DEFAULT_REINFORCE_THRESHOLD, NoopLearningBackend, NativeReinforcementBackend, resolveLearningBackend, isLearningSignalBackend, applyLearningSignals, applyLearningSignalsWithDelta, applyLearningSignalsWithTerms } from './learning-backend.js';
|
|
126
|
+
export type { LearningSignalBackend, LearningSignalStats, LearningSample, SignalCandidate, EnhanceContext, TrainingResult, LearningBackendMode, RerankTerm } from './learning-backend.js';
|
|
127
|
+
// lesson-bandit-rerank (I-8): the ACL's public surface only. The vendored engine class is
|
|
128
|
+
// deliberately NOT exported — `selectArm`'s "pick one and commit" is authority this domain denies
|
|
129
|
+
// the ranker, and a foreign, invariant-blind API has no business on our safety-critical seam.
|
|
130
|
+
export { resolveBanditConfig, payoffTermsFor, recordReward, recordExposures, contextKeyFor, banditStats, renderBanditHealth, narrowBanditReport, loadBanditState, banditStatePath, banditStateDir, freshBanditEnvelope, makeRewardEvent, classifySignal, BANDIT_LOCK_NAME, BANDIT_STATE_SCHEMA } from './lesson-payoff.js';
|
|
131
|
+
export type { ResolvedBanditConfig, RewardEvent, ExposureEvent, PayoffTerm, PayoffTerms, BanditStateEnvelope, BanditLoadReason, BanditRecallReport, BanditHealth, BanditWriteOutcome } from './lesson-payoff.js';
|
|
124
132
|
export {
|
|
125
133
|
DEFAULT_VECTOR_TIMEOUT_MS,
|
|
126
134
|
DEFAULT_HARMONIZE_THRESHOLD,
|
|
127
135
|
REINFORCE_RRF_CAP,
|
|
136
|
+
BANDIT_RRF_CAP,
|
|
128
137
|
withVectorTimeout,
|
|
129
138
|
isVectorNoise,
|
|
130
139
|
patternVectorEntry,
|
|
@@ -173,7 +182,8 @@ export type {
|
|
|
173
182
|
ReindexVectorReport,
|
|
174
183
|
TeachGuardResult,
|
|
175
184
|
} from './vector-tier.js';
|
|
176
|
-
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION
|
|
185
|
+
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION,
|
|
186
|
+
agentdbStorePath, agentdbMcpStorePath, agentdbStoreSeparationProblem } from './setup.js';
|
|
177
187
|
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
|
|
178
188
|
export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
|
|
179
189
|
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES } from './agentdb-index.js';
|
|
@@ -242,6 +252,11 @@ export {
|
|
|
242
252
|
serializeTrainingPair,
|
|
243
253
|
trainingPairPath,
|
|
244
254
|
trainingPairAppendCmd,
|
|
255
|
+
// operator-profile (ADR-001 Decision 5): the profile block never reaches .dz/fa-training/.
|
|
256
|
+
redactProfileBlock,
|
|
257
|
+
TP_PROFILE_MARKER_START,
|
|
258
|
+
TP_PROFILE_MARKER_END,
|
|
259
|
+
TP_PROFILE_REDACTED,
|
|
245
260
|
// wave1-instrument-repair (ADR-003 Condition 3): the persist allowlist + composite shape check.
|
|
246
261
|
codeCheckpointPersistAllowed,
|
|
247
262
|
codeStageResultShapeValid,
|
|
@@ -634,6 +649,11 @@ export {
|
|
|
634
649
|
SCOPED_QE_PROMPT_MAX_CHARS,
|
|
635
650
|
isSafeCodexRef,
|
|
636
651
|
codexReviewCommand,
|
|
652
|
+
codexExecCommand,
|
|
653
|
+
codexReviewMissedItsFiles,
|
|
654
|
+
CODEX_EXEC_TIMEOUT_SECONDS,
|
|
655
|
+
timeoutBinOrDefault,
|
|
656
|
+
TIMEOUT_BINS,
|
|
637
657
|
codexQeSignalCommand,
|
|
638
658
|
scopedQePrompt,
|
|
639
659
|
parseCodexReviewSignal,
|
|
@@ -677,6 +697,12 @@ export type {
|
|
|
677
697
|
CodexQeDeclineDetail,
|
|
678
698
|
CodexReviewResult,
|
|
679
699
|
} from './feature-adr-routing.js';
|
|
700
|
+
export {
|
|
701
|
+
PARSER_SAFE_REGION_START,
|
|
702
|
+
PARSER_SAFE_REGION_END,
|
|
703
|
+
checkParserSafeRegion,
|
|
704
|
+
} from './parser-safe-region.js';
|
|
705
|
+
export type { ParserSafeRegionVerdict, ParserSafeRegionRefusal } from './parser-safe-region.js';
|
|
680
706
|
export {
|
|
681
707
|
CLAUDE_USAGE_MODELS,
|
|
682
708
|
computeUsage,
|
|
@@ -844,3 +870,29 @@ export { quiescenceProbeScript, decideWriterQuiescence, WQ_WINDOW_SECONDS, WQ_MA
|
|
|
844
870
|
export type { WriterQuiescenceDecision } from './writer-quiescence.js';
|
|
845
871
|
export { decideCadenceWindow, isoWeekOf, weeklyBuckets, guardRepeatDecay, buildCadenceReport, CADENCE_WINDOW_DAYS } from './cadence.js';
|
|
846
872
|
export type { CadenceWindow, CadenceReport, CadenceWindowDecision } from './cadence.js';
|
|
873
|
+
export { readQeRounds, countQeRounds, QE_ROUNDS_DEFAULT_CEILING } from './qe-rounds.js';
|
|
874
|
+
export type { QeRound, QeFailedAttempt, QeRoundsReport, QeRoundsStatus } from './qe-rounds.js';
|
|
875
|
+
export { describeStoreLocation, storeLocationLine } from './store-location.js';
|
|
876
|
+
export type { StoreLocation, StoreOrigin } from './store-location.js';
|
|
877
|
+
export { mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel } from './store-merge.js';
|
|
878
|
+
export type { HitOrigin, OriginTagged } from './store-merge.js';
|
|
879
|
+
export { resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, TEACH_STORES } from './teach-target.js';
|
|
880
|
+
export type { TeachStore, TeachReason, TeachTarget } from './teach-target.js';
|
|
881
|
+
export { probeNativeDep, describeNativeDep, exerciseSqliteOpen } from './native-dep-probe.js';
|
|
882
|
+
export type { NativeDepState, NativeDepVerdict } from './native-dep-probe.js';
|
|
883
|
+
|
|
884
|
+
// Operator profile (feature operator-profile, ADR-001) — WHO the assistant is talking to: a
|
|
885
|
+
// per-user 0600 store under homedir(), rendered into a marked block in ~/.claude/CLAUDE.md
|
|
886
|
+
// (layer 2 — loaded in every project, no dz required). The register changes FORM, never FACTS.
|
|
887
|
+
export {
|
|
888
|
+
renderProfileBlock, readProfile, writeProfile, validateProfile,
|
|
889
|
+
profileStorePath, claudeMdPath, syncProfileBlock, checkProfileDrift,
|
|
890
|
+
mergeProfileBlock, extractProfileBlock, wrapProfileBlock,
|
|
891
|
+
parseRegister, registerOwnerWord, profileAgeDays,
|
|
892
|
+
parseDomainList, domainListText, parseYesNo,
|
|
893
|
+
PROFILE_MARKER_START, PROFILE_MARKER_END, REGISTERS,
|
|
894
|
+
} from './profile.js';
|
|
895
|
+
export type {
|
|
896
|
+
Register, Domain, OperatorProfile, ProfileReadResult, ProfileSyncResult,
|
|
897
|
+
ProfileDriftVerdict, ProfileDriftResult,
|
|
898
|
+
} from './profile.js';
|