@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.
Files changed (137) hide show
  1. package/.dz-manifest.json +287 -87
  2. package/README.md +60 -1
  3. package/dist/amendment-trace.d.ts +12 -1
  4. package/dist/amendment-trace.d.ts.map +1 -1
  5. package/dist/amendment-trace.js +22 -4
  6. package/dist/amendment-trace.js.map +1 -1
  7. package/dist/book-kb.d.ts.map +1 -1
  8. package/dist/book-kb.js +17 -0
  9. package/dist/book-kb.js.map +1 -1
  10. package/dist/feature-adr-checkpoints.d.ts +61 -0
  11. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  12. package/dist/feature-adr-checkpoints.js +116 -2
  13. package/dist/feature-adr-checkpoints.js.map +1 -1
  14. package/dist/feature-adr-routing.d.ts +69 -12
  15. package/dist/feature-adr-routing.d.ts.map +1 -1
  16. package/dist/feature-adr-routing.js +117 -7
  17. package/dist/feature-adr-routing.js.map +1 -1
  18. package/dist/index.d.ts +23 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +23 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/learning-backend.d.ts +39 -0
  23. package/dist/learning-backend.d.ts.map +1 -1
  24. package/dist/learning-backend.js +31 -11
  25. package/dist/learning-backend.js.map +1 -1
  26. package/dist/lesson-bandit.d.ts +116 -0
  27. package/dist/lesson-bandit.d.ts.map +1 -0
  28. package/dist/lesson-bandit.js +235 -0
  29. package/dist/lesson-bandit.js.map +1 -0
  30. package/dist/lesson-payoff.d.ts +260 -0
  31. package/dist/lesson-payoff.d.ts.map +1 -0
  32. package/dist/lesson-payoff.js +597 -0
  33. package/dist/lesson-payoff.js.map +1 -0
  34. package/dist/loop-blobs.generated.js +2 -2
  35. package/dist/loop-blobs.generated.js.map +1 -1
  36. package/dist/mutation-gate.d.ts +31 -8
  37. package/dist/mutation-gate.d.ts.map +1 -1
  38. package/dist/mutation-gate.js +57 -16
  39. package/dist/mutation-gate.js.map +1 -1
  40. package/dist/named-lock.d.ts.map +1 -1
  41. package/dist/named-lock.js +11 -9
  42. package/dist/named-lock.js.map +1 -1
  43. package/dist/native-dep-probe.d.ts +11 -0
  44. package/dist/native-dep-probe.d.ts.map +1 -0
  45. package/dist/native-dep-probe.js +100 -0
  46. package/dist/native-dep-probe.js.map +1 -0
  47. package/dist/operations.d.ts.map +1 -1
  48. package/dist/operations.js +82 -1
  49. package/dist/operations.js.map +1 -1
  50. package/dist/parser-safe-region.d.ts +47 -0
  51. package/dist/parser-safe-region.d.ts.map +1 -0
  52. package/dist/parser-safe-region.js +47 -0
  53. package/dist/parser-safe-region.js.map +1 -0
  54. package/dist/patterns.d.ts +21 -0
  55. package/dist/patterns.d.ts.map +1 -1
  56. package/dist/patterns.js +52 -4
  57. package/dist/patterns.js.map +1 -1
  58. package/dist/profile.d.ts +202 -0
  59. package/dist/profile.d.ts.map +1 -0
  60. package/dist/profile.js +536 -0
  61. package/dist/profile.js.map +1 -0
  62. package/dist/project-skills-root.d.ts +44 -0
  63. package/dist/project-skills-root.d.ts.map +1 -0
  64. package/dist/project-skills-root.js +62 -0
  65. package/dist/project-skills-root.js.map +1 -0
  66. package/dist/publish.d.ts +5 -3
  67. package/dist/publish.d.ts.map +1 -1
  68. package/dist/publish.js +46 -8
  69. package/dist/publish.js.map +1 -1
  70. package/dist/qe-rounds.d.ts +75 -0
  71. package/dist/qe-rounds.d.ts.map +1 -0
  72. package/dist/qe-rounds.js +174 -0
  73. package/dist/qe-rounds.js.map +1 -0
  74. package/dist/registry.d.ts.map +1 -1
  75. package/dist/registry.js +5 -1
  76. package/dist/registry.js.map +1 -1
  77. package/dist/run-records.d.ts.map +1 -1
  78. package/dist/run-records.js +8 -1
  79. package/dist/run-records.js.map +1 -1
  80. package/dist/setup.d.ts +35 -4
  81. package/dist/setup.d.ts.map +1 -1
  82. package/dist/setup.js +86 -23
  83. package/dist/setup.js.map +1 -1
  84. package/dist/sign.d.ts +29 -24
  85. package/dist/sign.d.ts.map +1 -1
  86. package/dist/sign.js +183 -61
  87. package/dist/sign.js.map +1 -1
  88. package/dist/store-location.d.ts +56 -0
  89. package/dist/store-location.d.ts.map +1 -0
  90. package/dist/store-location.js +56 -0
  91. package/dist/store-location.js.map +1 -0
  92. package/dist/store-lock.d.ts +1 -0
  93. package/dist/store-lock.d.ts.map +1 -1
  94. package/dist/store-lock.js +7 -1
  95. package/dist/store-lock.js.map +1 -1
  96. package/dist/store-merge.d.ts +55 -0
  97. package/dist/store-merge.d.ts.map +1 -0
  98. package/dist/store-merge.js +77 -0
  99. package/dist/store-merge.js.map +1 -0
  100. package/dist/teach-target.d.ts +61 -0
  101. package/dist/teach-target.d.ts.map +1 -0
  102. package/dist/teach-target.js +105 -0
  103. package/dist/teach-target.js.map +1 -0
  104. package/dist/vector-tier.d.ts +30 -0
  105. package/dist/vector-tier.d.ts.map +1 -1
  106. package/dist/vector-tier.js +178 -20
  107. package/dist/vector-tier.js.map +1 -1
  108. package/package.json +13 -13
  109. package/sbom.json +591 -87
  110. package/src/amendment-trace.ts +34 -4
  111. package/src/book-kb.ts +17 -0
  112. package/src/feature-adr-checkpoints.ts +111 -2
  113. package/src/feature-adr-routing.ts +146 -7
  114. package/src/index.ts +55 -3
  115. package/src/learning-backend.ts +62 -11
  116. package/src/lesson-bandit.ts +279 -0
  117. package/src/lesson-payoff.ts +728 -0
  118. package/src/loop-blobs.generated.ts +2 -2
  119. package/src/mutation-gate.ts +74 -17
  120. package/src/named-lock.ts +10 -8
  121. package/src/native-dep-probe.ts +118 -0
  122. package/src/operations.ts +77 -1
  123. package/src/parser-safe-region.ts +65 -0
  124. package/src/patterns.ts +76 -6
  125. package/src/profile.ts +593 -0
  126. package/src/project-skills-root.ts +63 -0
  127. package/src/publish.ts +43 -7
  128. package/src/qe-rounds.ts +226 -0
  129. package/src/registry.ts +5 -1
  130. package/src/run-records.ts +9 -1
  131. package/src/setup.ts +87 -23
  132. package/src/sign.ts +180 -60
  133. package/src/store-location.ts +76 -0
  134. package/src/store-lock.ts +8 -1
  135. package/src/store-merge.ts +91 -0
  136. package/src/teach-target.ts +126 -0
  137. package/src/vector-tier.ts +236 -21
@@ -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
- const ROW_START = /^(?:[-*]\s+\*{0,2}AM-(\d+)|\|\s*\*{0,2}AM-(\d+))/gm;
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
- const re = /→\s*tests?\s+`([^`]+)`(?:\s*(?:and|и)\s*`([^`]+)`)?/g;
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 = /→\s*tests?\s+`[^`]+`(?:\s*(?:and|и)\s*`[^`]+`)?[\s\S]{0,40}?\bin\s+`([^`]+)`/.exec(raw);
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
- if (r.verdict !== 'resolved') reasons.push(`${r.id} ${r.verdict}: ${r.detail}`);
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
- let input = coerceText(opts.input);
530
- let output = coerceText(opts.output);
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
- export function codexProbeCommand(id: string): string | null {
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
- return "timeout 60 codex exec -m '" + id + "' 'Reply with exactly: OK'";
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
- let cmd = 'timeout ' + seconds + " codex review -c model='" + modelId + "' -c model_reasoning_effort='" + effort + "'";
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
- return 'codex not used ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'codex exec reported it could not run' : String(d.reason));
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 } from './setup.js';
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';