@dzhechkov/harness-core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.dz-manifest.json +339 -75
  2. package/README.md +47 -4
  3. package/dist/codex-hooks-assets.d.ts.map +1 -1
  4. package/dist/codex-hooks-assets.js +39 -2
  5. package/dist/codex-hooks-assets.js.map +1 -1
  6. package/dist/codex-hooks-verify.d.ts +23 -2
  7. package/dist/codex-hooks-verify.d.ts.map +1 -1
  8. package/dist/codex-hooks-verify.js +29 -0
  9. package/dist/codex-hooks-verify.js.map +1 -1
  10. package/dist/codex-hooks.d.ts +90 -7
  11. package/dist/codex-hooks.d.ts.map +1 -1
  12. package/dist/codex-hooks.js +171 -21
  13. package/dist/codex-hooks.js.map +1 -1
  14. package/dist/feature-adr-routing.d.ts +22 -0
  15. package/dist/feature-adr-routing.d.ts.map +1 -1
  16. package/dist/feature-adr-routing.js +45 -0
  17. package/dist/feature-adr-routing.js.map +1 -1
  18. package/dist/index.d.ts +11 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +17 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/loop-blobs.generated.d.ts +1 -1
  23. package/dist/loop-blobs.generated.d.ts.map +1 -1
  24. package/dist/loop-blobs.generated.js +12 -3
  25. package/dist/loop-blobs.generated.js.map +1 -1
  26. package/dist/loop-plan.d.ts +70 -0
  27. package/dist/loop-plan.d.ts.map +1 -1
  28. package/dist/loop-plan.js +103 -0
  29. package/dist/loop-plan.js.map +1 -1
  30. package/dist/loop-render.d.ts.map +1 -1
  31. package/dist/loop-render.js +38 -130
  32. package/dist/loop-render.js.map +1 -1
  33. package/dist/loop-run-semantics.d.ts +130 -0
  34. package/dist/loop-run-semantics.d.ts.map +1 -0
  35. package/dist/loop-run-semantics.js +257 -0
  36. package/dist/loop-run-semantics.js.map +1 -0
  37. package/dist/loop-trace.d.ts +106 -5
  38. package/dist/loop-trace.d.ts.map +1 -1
  39. package/dist/loop-trace.js +151 -18
  40. package/dist/loop-trace.js.map +1 -1
  41. package/dist/managed-hooks.d.ts +10 -0
  42. package/dist/managed-hooks.d.ts.map +1 -1
  43. package/dist/managed-hooks.js +17 -5
  44. package/dist/managed-hooks.js.map +1 -1
  45. package/dist/named-lock.d.ts +57 -0
  46. package/dist/named-lock.d.ts.map +1 -0
  47. package/dist/named-lock.js +247 -0
  48. package/dist/named-lock.js.map +1 -0
  49. package/dist/operations.d.ts +81 -5
  50. package/dist/operations.d.ts.map +1 -1
  51. package/dist/operations.js +356 -38
  52. package/dist/operations.js.map +1 -1
  53. package/dist/parity.d.ts +70 -2
  54. package/dist/parity.d.ts.map +1 -1
  55. package/dist/parity.js +133 -2
  56. package/dist/parity.js.map +1 -1
  57. package/dist/qe-bridge.d.ts +291 -0
  58. package/dist/qe-bridge.d.ts.map +1 -0
  59. package/dist/qe-bridge.js +538 -0
  60. package/dist/qe-bridge.js.map +1 -0
  61. package/dist/score.d.ts.map +1 -1
  62. package/dist/score.js +43 -9
  63. package/dist/score.js.map +1 -1
  64. package/dist/trace-corroborate.d.ts +48 -0
  65. package/dist/trace-corroborate.d.ts.map +1 -0
  66. package/dist/trace-corroborate.js +172 -0
  67. package/dist/trace-corroborate.js.map +1 -0
  68. package/dist/workflow-run-dispatch.d.ts +230 -0
  69. package/dist/workflow-run-dispatch.d.ts.map +1 -0
  70. package/dist/workflow-run-dispatch.js +363 -0
  71. package/dist/workflow-run-dispatch.js.map +1 -0
  72. package/dist/workflow-run.d.ts +513 -0
  73. package/dist/workflow-run.d.ts.map +1 -0
  74. package/dist/workflow-run.js +1377 -0
  75. package/dist/workflow-run.js.map +1 -0
  76. package/package.json +2 -2
  77. package/sbom.json +740 -80
  78. package/src/codex-hooks-assets.ts +39 -2
  79. package/src/codex-hooks-verify.ts +55 -2
  80. package/src/codex-hooks.ts +172 -20
  81. package/src/feature-adr-routing.ts +55 -0
  82. package/src/index.ts +46 -1
  83. package/src/loop-blobs.generated.ts +12 -3
  84. package/src/loop-plan.ts +185 -0
  85. package/src/loop-render.ts +38 -128
  86. package/src/loop-run-semantics.ts +278 -0
  87. package/src/loop-trace.ts +207 -16
  88. package/src/managed-hooks.ts +26 -5
  89. package/src/named-lock.ts +277 -0
  90. package/src/operations.ts +441 -40
  91. package/src/parity.ts +177 -2
  92. package/src/qe-bridge.ts +737 -0
  93. package/src/score.ts +50 -9
  94. package/src/trace-corroborate.ts +205 -0
  95. package/src/workflow-run-dispatch.ts +459 -0
  96. package/src/workflow-run.ts +1773 -0
package/src/parity.ts CHANGED
@@ -99,6 +99,18 @@ export interface CapabilityEvidence {
99
99
  readonly kind: 'transcript' | 'reproducer';
100
100
  /** ISO date the evidence was produced. Stale evidence is still evidence — silence is not. */
101
101
  readonly at: string;
102
+ /**
103
+ * The RUNTIME VERSION the evidence was measured on, verbatim from the runtime's own `--version`
104
+ * (e.g. `codex-cli 0.147.0`).
105
+ *
106
+ * ADR-006 pins capability grants to a runtime version, and the gate enforced only that a record
107
+ * existed and its file was on disk — so evidence recorded on codex 0.147 kept a grant alive on
108
+ * the installed 0.148 (independent review, finding 3). A hook registry is exactly the kind of
109
+ * surface a minor version moves. When this is set and the caller supplies a PROBED version that
110
+ * differs, the grant is reported `stale-runtime-version` — INCONCLUSIVE, not revoked: the fix is
111
+ * a fresh probe, and an absent probe flips nothing.
112
+ */
113
+ readonly runtimeVersion?: string;
102
114
  }
103
115
 
104
116
  export const CAPABILITY_EVIDENCE: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = {
@@ -119,15 +131,28 @@ export const CAPABILITY_EVIDENCE: Record<TargetName, Partial<Record<RuntimeCapab
119
131
  mcp: { evidence: 'codex mcp list', kind: 'reproducer', at: '2026-07-19' },
120
132
  // The two-sided live block: our marker in the transcript AND the sentinel side effect absent,
121
133
  // in a NON-bypassed run, with the entry reported `trusted` by codex's own `hooks/list`.
134
+ // RE-PROBED on the installed runtime at the independent-QE fix round (review finding 3): the
135
+ // 0.147.0 evidence was stale on a machine running 0.148.0, and re-running it was NOT a
136
+ // formality — the arming path had silently stopped working, because `hooks/list` spells its
137
+ // `eventName` field `preToolUse` on 0.148 while the trust KEY still says `pre_tool_use`.
138
+ // All four legs pass on 0.148.0 with the trust axis PARSED from `hooks/list` rather than
139
+ // hard-coded (the pre-fix grader asserted `trustStatus: 'trusted'` as a constant).
122
140
  'hooks-shell': {
123
- evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/veto-armed.txt',
141
+ evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/fixround/veto-armed.txt',
124
142
  kind: 'transcript',
125
143
  at: '2026-08-19',
144
+ runtimeVersion: 'codex-cli 0.148.0',
126
145
  },
146
+ // NOT re-probed on 0.148: the recall canary is a separate live run (forced hit + removed twin)
147
+ // and this round did not execute it. The version it was measured on is recorded HONESTLY, so
148
+ // `findUnbackedCapabilities` reports it `stale-runtime-version` — INCONCLUSIVE — the moment a
149
+ // caller supplies the probed 0.148.0. That is the correct state: the grant is not withdrawn,
150
+ // it is awaiting its re-probe.
127
151
  'hooks-prompt': {
128
152
  evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/recall-canary.md',
129
153
  kind: 'transcript',
130
154
  at: '2026-08-19',
155
+ runtimeVersion: 'codex-cli 0.147.0',
131
156
  },
132
157
  },
133
158
  opencode: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target opencode', kind: 'reproducer', at: '2026-07-19' } },
@@ -143,10 +168,23 @@ export const CAPABILITY_EVIDENCE: Record<TargetName, Partial<Record<RuntimeCapab
143
168
  export interface UnbackedCapability {
144
169
  readonly target: TargetName;
145
170
  readonly capability: RuntimeCapability;
146
- readonly reason: 'no-evidence-record' | 'dangling-transcript';
171
+ /**
172
+ * `stale-runtime-version` is INCONCLUSIVE, not a refutation: the grant was proven, on a runtime
173
+ * that is no longer the installed one. It clears when a current-version probe is recorded.
174
+ */
175
+ readonly reason: 'no-evidence-record' | 'dangling-transcript' | 'stale-runtime-version';
147
176
  readonly evidence?: string;
177
+ /** `recorded → probed`, for the stale case. */
178
+ readonly recordedVersion?: string;
179
+ readonly probedVersion?: string;
148
180
  }
149
181
 
182
+ /**
183
+ * Runtime versions observed NOW, keyed by target — injected, never probed here (this module is
184
+ * pure). An absent entry means "not probed", which flips nothing.
185
+ */
186
+ export type ProbedRuntimeVersions = Partial<Record<TargetName, string>>;
187
+
150
188
  /**
151
189
  * Every capability grant that is NOT backed by usable evidence.
152
190
  *
@@ -159,6 +197,7 @@ export function findUnbackedCapabilities(
159
197
  transcriptExists: (path: string) => boolean,
160
198
  capabilities: Record<TargetName, readonly RuntimeCapability[]> = TARGET_CAPABILITIES,
161
199
  evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
200
+ probedVersions: ProbedRuntimeVersions = {},
162
201
  ): UnbackedCapability[] {
163
202
  const out: UnbackedCapability[] = [];
164
203
  for (const target of Object.keys(capabilities) as TargetName[]) {
@@ -170,12 +209,112 @@ export function findUnbackedCapabilities(
170
209
  }
171
210
  if (record.kind === 'transcript' && !transcriptExists(record.evidence)) {
172
211
  out.push({ target, capability, reason: 'dangling-transcript', evidence: record.evidence });
212
+ continue;
213
+ }
214
+ // Version pin (ADR-006, finding 3). Only fires when BOTH sides are known: an unprobed target
215
+ // stays silent rather than reporting every grant stale on a machine without the runtime.
216
+ const probed = probedVersions[target];
217
+ if (record.runtimeVersion !== undefined && probed !== undefined && probed !== record.runtimeVersion) {
218
+ out.push({
219
+ target,
220
+ capability,
221
+ reason: 'stale-runtime-version',
222
+ evidence: record.evidence,
223
+ recordedVersion: record.runtimeVersion,
224
+ probedVersion: probed,
225
+ });
173
226
  }
174
227
  }
175
228
  }
176
229
  return out;
177
230
  }
178
231
 
232
+ /**
233
+ * Compare two recorded runtime versions the way a version string means it.
234
+ *
235
+ * `null` when the two are not comparable (different product, unparseable) — which the caller reads
236
+ * as "not the newest", the conservative direction.
237
+ */
238
+ function compareRuntimeVersions(a: string, b: string): number | null {
239
+ const parse = (v: string): { product: string; parts: number[] } | null => {
240
+ const m = /^(.*?)\s*(\d+(?:\.\d+)*)\s*$/.exec(v.trim());
241
+ if (m === null) return null;
242
+ return { product: (m[1] ?? '').trim(), parts: (m[2] ?? '').split('.').map((n) => Number(n)) };
243
+ };
244
+ const pa = parse(a);
245
+ const pb = parse(b);
246
+ if (pa === null || pb === null || pa.product !== pb.product) return null;
247
+ const len = Math.max(pa.parts.length, pb.parts.length);
248
+ for (let i = 0; i < len; i += 1) {
249
+ const x = pa.parts[i] ?? 0;
250
+ const y = pb.parts[i] ?? 0;
251
+ if (x !== y) return x < y ? -1 : 1;
252
+ }
253
+ return 0;
254
+ }
255
+
256
+ /**
257
+ * The newest runtime version recorded among a target's TRANSCRIPT evidence, or `null`.
258
+ *
259
+ * This is what makes staleness detectable WITHOUT running anything: the records date themselves
260
+ * against each other. `dz parity` computes a matrix and must keep doing so deterministically — a
261
+ * `codex --version` subprocess inside it would make a pure report depend on the machine it prints
262
+ * on. Re-probing one capability is what dates the others (fix round 2, R2-3).
263
+ */
264
+ export function newestRecordedRuntimeVersion(
265
+ target: TargetName,
266
+ evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
267
+ ): string | null {
268
+ let newest: string | null = null;
269
+ for (const record of Object.values(evidence[target] ?? {})) {
270
+ if (record === undefined || record.kind !== 'transcript' || record.runtimeVersion === undefined) continue;
271
+ if (newest === null) {
272
+ newest = record.runtimeVersion;
273
+ continue;
274
+ }
275
+ const cmp = compareRuntimeVersions(newest, record.runtimeVersion);
276
+ if (cmp !== null && cmp < 0) newest = record.runtimeVersion;
277
+ }
278
+ return newest;
279
+ }
280
+
281
+ /**
282
+ * Transcript evidence that is out of date with the newest record for its own target.
283
+ *
284
+ * SCOPE, stated because it is a judgement and not a derivation: the version rule applies to
285
+ * `kind: 'transcript'` records only. A transcript FREEZES one runtime moment, so it can go stale; a
286
+ * `reproducer` is a command anyone can re-run, and calling it stale would report `dz --version` as
287
+ * expired. A transcript with NO `runtimeVersion` is stale too — an undated observation cannot be
288
+ * shown to be current.
289
+ *
290
+ * INCONCLUSIVE, never a refutation: the grant is not withdrawn, it is awaiting its re-probe.
291
+ */
292
+ export function findStaleTranscriptEvidence(
293
+ capabilities: Record<TargetName, readonly RuntimeCapability[]> = TARGET_CAPABILITIES,
294
+ evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
295
+ ): UnbackedCapability[] {
296
+ const out: UnbackedCapability[] = [];
297
+ for (const target of Object.keys(capabilities) as TargetName[]) {
298
+ const newest = newestRecordedRuntimeVersion(target, evidence);
299
+ if (newest === null) continue; // nothing version-pinned on this target ⇒ nothing to be stale against
300
+ for (const capability of capabilities[target]) {
301
+ const record = evidence[target]?.[capability];
302
+ if (record === undefined || record.kind !== 'transcript') continue;
303
+ const recorded = record.runtimeVersion;
304
+ if (recorded !== undefined && compareRuntimeVersions(recorded, newest) === 0) continue;
305
+ out.push({
306
+ target,
307
+ capability,
308
+ reason: 'stale-runtime-version',
309
+ evidence: record.evidence,
310
+ ...(recorded !== undefined ? { recordedVersion: recorded } : {}),
311
+ probedVersion: newest,
312
+ });
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+
179
318
  /** How a feature manifests on a platform: a concrete FORM with its runtime requirements. */
180
319
  export interface FeatureForm {
181
320
  /** Human-readable name of the form, shown as the `via` of a parity cell (AM-2). */
@@ -329,6 +468,42 @@ export function computeParity(feature: ParityFeature, capabilities: readonly Run
329
468
  return { level: 'none' };
330
469
  }
331
470
 
471
+ /**
472
+ * A parity cell AS REPORTED — the computed level, plus the one thing the computation cannot know:
473
+ * whether the evidence behind the deciding capability is still current.
474
+ *
475
+ * `inconclusive` is a REPORTING level, not a model level: `computeParity` keeps answering the
476
+ * capability question, and this layer answers the evidence question. Keeping them apart is what
477
+ * lets the matrix stay a pure function of the capability model.
478
+ */
479
+ export interface ParityReportCell {
480
+ readonly level: 'full' | 'manual' | 'none' | 'inconclusive';
481
+ readonly via?: string | undefined;
482
+ /** The stale capabilities the deciding form depends on (present only when `inconclusive`). */
483
+ readonly staleEvidence?: readonly RuntimeCapability[];
484
+ }
485
+
486
+ /**
487
+ * Downgrade a cell whose DECIDING form rests on stale evidence to `inconclusive`.
488
+ *
489
+ * The round-1 gate could tell that `hooks-prompt` evidence was stale and nothing a user runs ever
490
+ * asked it (fix round 2, R2-3): `dz parity` printed `full` for the auto-recall leg on codex off a
491
+ * transcript recorded on a runtime that is no longer installed. A cell that says `full` on evidence
492
+ * nobody has re-confirmed is the same class of claim this whole feature exists to refuse.
493
+ */
494
+ export function downgradeForStaleEvidence(
495
+ feature: ParityFeature,
496
+ cell: ParityCell,
497
+ staleCapabilities: readonly RuntimeCapability[],
498
+ ): ParityReportCell {
499
+ if (cell.level === 'none' || cell.via === undefined || staleCapabilities.length === 0) return cell;
500
+ const form = feature.forms.find((f) => f.form === cell.via);
501
+ if (form === undefined) return cell;
502
+ const hit = form.requires.filter((r) => staleCapabilities.includes(r));
503
+ if (hit.length === 0) return cell;
504
+ return { level: 'inconclusive', via: cell.via, staleEvidence: hit };
505
+ }
506
+
332
507
  export interface ParityMatrixRow {
333
508
  readonly feature: ParityFeature;
334
509
  readonly cells: Readonly<Record<TargetName, ParityCell>>;