@dzhechkov/harness-core 0.4.5 → 0.5.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 (141) hide show
  1. package/.dz-manifest.json +489 -105
  2. package/README.md +116 -5
  3. package/dist/agents-policy.d.ts +67 -0
  4. package/dist/agents-policy.d.ts.map +1 -0
  5. package/dist/agents-policy.js +258 -0
  6. package/dist/agents-policy.js.map +1 -0
  7. package/dist/codex-hooks-assets.d.ts +47 -0
  8. package/dist/codex-hooks-assets.d.ts.map +1 -0
  9. package/dist/codex-hooks-assets.js +324 -0
  10. package/dist/codex-hooks-assets.js.map +1 -0
  11. package/dist/codex-hooks-verify.d.ts +95 -0
  12. package/dist/codex-hooks-verify.d.ts.map +1 -0
  13. package/dist/codex-hooks-verify.js +169 -0
  14. package/dist/codex-hooks-verify.js.map +1 -0
  15. package/dist/codex-hooks.d.ts +341 -0
  16. package/dist/codex-hooks.d.ts.map +1 -0
  17. package/dist/codex-hooks.js +541 -0
  18. package/dist/codex-hooks.js.map +1 -0
  19. package/dist/discrimination-gate.d.ts +88 -15
  20. package/dist/discrimination-gate.d.ts.map +1 -1
  21. package/dist/discrimination-gate.js +343 -51
  22. package/dist/discrimination-gate.js.map +1 -1
  23. package/dist/feature-adr-checkpoints.d.ts +22 -0
  24. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  25. package/dist/feature-adr-checkpoints.js +42 -0
  26. package/dist/feature-adr-checkpoints.js.map +1 -1
  27. package/dist/feature-adr-routing.d.ts +218 -5
  28. package/dist/feature-adr-routing.d.ts.map +1 -1
  29. package/dist/feature-adr-routing.js +583 -54
  30. package/dist/feature-adr-routing.js.map +1 -1
  31. package/dist/guard.d.ts +13 -0
  32. package/dist/guard.d.ts.map +1 -1
  33. package/dist/guard.js +25 -1
  34. package/dist/guard.js.map +1 -1
  35. package/dist/index.d.ts +23 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +37 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/loop-blobs.generated.d.ts +1 -1
  40. package/dist/loop-blobs.generated.d.ts.map +1 -1
  41. package/dist/loop-blobs.generated.js +12 -3
  42. package/dist/loop-blobs.generated.js.map +1 -1
  43. package/dist/loop-plan.d.ts +70 -0
  44. package/dist/loop-plan.d.ts.map +1 -1
  45. package/dist/loop-plan.js +103 -0
  46. package/dist/loop-plan.js.map +1 -1
  47. package/dist/loop-render.d.ts.map +1 -1
  48. package/dist/loop-render.js +37 -129
  49. package/dist/loop-render.js.map +1 -1
  50. package/dist/loop-run-semantics.d.ts +130 -0
  51. package/dist/loop-run-semantics.d.ts.map +1 -0
  52. package/dist/loop-run-semantics.js +257 -0
  53. package/dist/loop-run-semantics.js.map +1 -0
  54. package/dist/loop-trace.d.ts +31 -2
  55. package/dist/loop-trace.d.ts.map +1 -1
  56. package/dist/loop-trace.js +89 -12
  57. package/dist/loop-trace.js.map +1 -1
  58. package/dist/managed-hooks.d.ts +86 -0
  59. package/dist/managed-hooks.d.ts.map +1 -0
  60. package/dist/managed-hooks.js +101 -0
  61. package/dist/managed-hooks.js.map +1 -0
  62. package/dist/mutation-gate.d.ts +14 -0
  63. package/dist/mutation-gate.d.ts.map +1 -1
  64. package/dist/mutation-gate.js +25 -2
  65. package/dist/mutation-gate.js.map +1 -1
  66. package/dist/named-lock.d.ts +57 -0
  67. package/dist/named-lock.d.ts.map +1 -0
  68. package/dist/named-lock.js +247 -0
  69. package/dist/named-lock.js.map +1 -0
  70. package/dist/operations.d.ts +229 -0
  71. package/dist/operations.d.ts.map +1 -1
  72. package/dist/operations.js +880 -26
  73. package/dist/operations.js.map +1 -1
  74. package/dist/parity.d.ts +106 -1
  75. package/dist/parity.d.ts.map +1 -1
  76. package/dist/parity.js +209 -5
  77. package/dist/parity.js.map +1 -1
  78. package/dist/qe-bridge.d.ts +291 -0
  79. package/dist/qe-bridge.d.ts.map +1 -0
  80. package/dist/qe-bridge.js +538 -0
  81. package/dist/qe-bridge.js.map +1 -0
  82. package/dist/recall-usage.d.ts +53 -0
  83. package/dist/recall-usage.d.ts.map +1 -1
  84. package/dist/recall-usage.js +125 -2
  85. package/dist/recall-usage.js.map +1 -1
  86. package/dist/score.d.ts.map +1 -1
  87. package/dist/score.js +43 -9
  88. package/dist/score.js.map +1 -1
  89. package/dist/setup.d.ts.map +1 -1
  90. package/dist/setup.js +14 -26
  91. package/dist/setup.js.map +1 -1
  92. package/dist/shell-veto-policy.d.ts +53 -0
  93. package/dist/shell-veto-policy.d.ts.map +1 -0
  94. package/dist/shell-veto-policy.js +103 -0
  95. package/dist/shell-veto-policy.js.map +1 -0
  96. package/dist/skills.d.ts +86 -1
  97. package/dist/skills.d.ts.map +1 -1
  98. package/dist/skills.js +116 -1
  99. package/dist/skills.js.map +1 -1
  100. package/dist/targets.d.ts +75 -0
  101. package/dist/targets.d.ts.map +1 -1
  102. package/dist/targets.js +160 -0
  103. package/dist/targets.js.map +1 -1
  104. package/dist/workflow-run-dispatch.d.ts +230 -0
  105. package/dist/workflow-run-dispatch.d.ts.map +1 -0
  106. package/dist/workflow-run-dispatch.js +363 -0
  107. package/dist/workflow-run-dispatch.js.map +1 -0
  108. package/dist/workflow-run.d.ts +498 -0
  109. package/dist/workflow-run.d.ts.map +1 -0
  110. package/dist/workflow-run.js +1359 -0
  111. package/dist/workflow-run.js.map +1 -0
  112. package/package.json +6 -5
  113. package/sbom.json +1099 -139
  114. package/src/agents-policy.ts +338 -0
  115. package/src/codex-hooks-assets.ts +328 -0
  116. package/src/codex-hooks-verify.ts +237 -0
  117. package/src/codex-hooks.ts +723 -0
  118. package/src/discrimination-gate.ts +456 -58
  119. package/src/feature-adr-checkpoints.ts +38 -0
  120. package/src/feature-adr-routing.ts +697 -75
  121. package/src/guard.ts +36 -1
  122. package/src/index.ts +162 -2
  123. package/src/loop-blobs.generated.ts +12 -3
  124. package/src/loop-plan.ts +185 -0
  125. package/src/loop-render.ts +37 -127
  126. package/src/loop-run-semantics.ts +278 -0
  127. package/src/loop-trace.ts +84 -10
  128. package/src/managed-hooks.ts +150 -0
  129. package/src/mutation-gate.ts +24 -2
  130. package/src/named-lock.ts +277 -0
  131. package/src/operations.ts +1121 -29
  132. package/src/parity.ts +295 -6
  133. package/src/qe-bridge.ts +737 -0
  134. package/src/recall-usage.ts +184 -1
  135. package/src/score.ts +50 -9
  136. package/src/setup.ts +26 -27
  137. package/src/shell-veto-policy.ts +119 -0
  138. package/src/skills.ts +174 -1
  139. package/src/targets.ts +189 -0
  140. package/src/workflow-run-dispatch.ts +459 -0
  141. package/src/workflow-run.ts +1743 -0
package/src/parity.ts CHANGED
@@ -23,7 +23,13 @@ import type { TargetName } from './targets.js';
23
23
  export type RuntimeCapability =
24
24
  | 'shell' // can run the dz CLI, i.e. a shell WITH Node.js (what `npm i -g` implies on a dev machine)
25
25
  | 'skills' // consumes compiled skills (all adapters emit them; agents-md as one merged file)
26
- | 'hooks' // pre/post tool-call hooks (auto claim-check, auto recall apply-leg)
26
+ // The blanket `hooks` capability is deliberately GONE (AM-21). The matrix is COMPUTED, so one
27
+ // word here moves several cells: granting a target `hooks` promoted BOTH `claim-check` (a
28
+ // GATE_FEATURE_IDS member) and `learning-apply` to `full` in one edit, and a caveat comment
29
+ // cannot move a computed cell back.
30
+ | 'hooks-write' // PreToolUse on FILE writes — what claim-check's automatic form needs
31
+ | 'hooks-shell' // PreToolUse on SHELL commands — what the codex veto leg ships
32
+ | 'hooks-prompt' // UserPromptSubmit — what the auto-recall apply leg ships
27
33
  | 'mcp' // Model Context Protocol client exists on the platform (servers still need configuring)
28
34
  | 'mcp-configured' // our MCP servers (AgentDB / agentic-qe) are wired up out of the box (.mcp.json)
29
35
  | 'workflows' // deterministic multi-agent Workflow runtime (ultracode feature-adr, delivery gate)
@@ -33,7 +39,9 @@ export type RuntimeCapability =
33
39
  export const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [
34
40
  'shell',
35
41
  'skills',
36
- 'hooks',
42
+ 'hooks-write',
43
+ 'hooks-shell',
44
+ 'hooks-prompt',
37
45
  'mcp',
38
46
  'mcp-configured',
39
47
  'workflows',
@@ -47,9 +55,18 @@ export const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [
47
55
  export const TARGET_CAPABILITIES: Record<TargetName, readonly RuntimeCapability[]> = {
48
56
  // Daily-driven harness: hooks (claim-check PreToolUse, recall UserPromptSubmit), MCP servers,
49
57
  // Workflow runtime and the statusline are all exercised in this repo every session.
50
- 'claude-code': ['shell', 'skills', 'hooks', 'mcp', 'mcp-configured', 'workflows', 'statusline'],
58
+ 'claude-code': ['shell', 'skills', 'hooks-write', 'hooks-shell', 'hooks-prompt', 'mcp', 'mcp-configured', 'workflows', 'statusline'],
51
59
  // MCP subsystem probed live: `codex mcp --help` + `codex mcp list` answer (2026-07-19).
52
- codex: ['shell', 'skills', 'mcp'],
60
+ // hooks-shell + hooks-prompt: user-global `$CODEX_HOME/hooks.json`, installed and ARMED
61
+ // unattended, proved by a live two-sided block probe (2026-08-19, codex-cli 0.147.0 — the
62
+ // transcripts named in CAPABILITY_EVIDENCE). `hooks-write` is NOT granted: this leg ships no
63
+ // PreToolUse guard on Write/Edit, so `claim-check` stays `manual` on codex.
64
+ //
65
+ // Fact CORRECTED 2026-08-19: a project-level `<repo>/.codex/hooks.json` DOES load on 0.147.0
66
+ // (`source: "project"`, MEASURED). The earlier note that project-level files are ignored was
67
+ // measured on 0.144.6 and is stale. dz still writes only the user-global registry, but that is
68
+ // now a DECISION (one carrier, one removable unit) rather than a description of the runtime.
69
+ codex: ['shell', 'skills', 'mcp', 'hooks-shell', 'hooks-prompt'],
53
70
  // Conservative v1: skills emission verified by the adapters; richer runtimes unproven.
54
71
  opencode: ['shell', 'skills'],
55
72
  hermes: ['shell', 'skills'],
@@ -62,6 +79,242 @@ export const TARGET_CAPABILITIES: Record<TargetName, readonly RuntimeCapability[
62
79
  windsurf: ['shell', 'skills'],
63
80
  };
64
81
 
82
+ /**
83
+ * WHY each capability grant is believed, as machine-readable DATA (AM-22).
84
+ *
85
+ * `parity.ts` has declared an honesty contract in prose since day one — *"a capability flag states
86
+ * only what has been VERIFIED, each with its source in a comment"*. A comment is documentation, not
87
+ * a gate. The pinned map in `parity.test.ts` catches an ACCIDENTAL capability, but updating a pin is
88
+ * a mechanical edit that demands no evidence, so the contract had no layer-1 half.
89
+ *
90
+ * This is that half. `parity_no_capability_grant_without_evidence` fails when a target declares a
91
+ * capability with no record here, when a `transcript` record points at a file that does not exist,
92
+ * or when **any cell computes `full` on a target whose deciding capability has no transcript**.
93
+ *
94
+ * `kind: 'transcript'` means a recorded live run is on disk at `evidence`; `kind: 'reproducer'`
95
+ * means `evidence` is a command anyone can re-run.
96
+ */
97
+ export interface CapabilityEvidence {
98
+ readonly evidence: string;
99
+ readonly kind: 'transcript' | 'reproducer';
100
+ /** ISO date the evidence was produced. Stale evidence is still evidence — silence is not. */
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;
114
+ }
115
+
116
+ export const CAPABILITY_EVIDENCE: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = {
117
+ 'claude-code': {
118
+ shell: { evidence: 'dz --version', kind: 'reproducer', at: '2026-07-19' },
119
+ skills: { evidence: 'dz compose --target claude-code', kind: 'reproducer', at: '2026-07-19' },
120
+ 'hooks-write': { evidence: 'grep -n claim-check-hook .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
121
+ 'hooks-shell': { evidence: 'grep -n PreToolUse .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
122
+ 'hooks-prompt': { evidence: 'grep -n recall-hook.cjs .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
123
+ mcp: { evidence: 'cat .mcp.json', kind: 'reproducer', at: '2026-07-19' },
124
+ 'mcp-configured': { evidence: 'cat .mcp.json', kind: 'reproducer', at: '2026-07-19' },
125
+ workflows: { evidence: 'ls .claude/workflows/feature-adr.js', kind: 'reproducer', at: '2026-07-19' },
126
+ statusline: { evidence: 'dz statusline', kind: 'reproducer', at: '2026-07-19' },
127
+ },
128
+ codex: {
129
+ shell: { evidence: 'codex exec -m <id> "Reply with exactly: OK"', kind: 'reproducer', at: '2026-07-19' },
130
+ skills: { evidence: 'dz compose --target codex', kind: 'reproducer', at: '2026-07-19' },
131
+ mcp: { evidence: 'codex mcp list', kind: 'reproducer', at: '2026-07-19' },
132
+ // The two-sided live block: our marker in the transcript AND the sentinel side effect absent,
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).
140
+ 'hooks-shell': {
141
+ evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/fixround/veto-armed.txt',
142
+ kind: 'transcript',
143
+ at: '2026-08-19',
144
+ runtimeVersion: 'codex-cli 0.148.0',
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.
151
+ 'hooks-prompt': {
152
+ evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/recall-canary.md',
153
+ kind: 'transcript',
154
+ at: '2026-08-19',
155
+ runtimeVersion: 'codex-cli 0.147.0',
156
+ },
157
+ },
158
+ opencode: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target opencode', kind: 'reproducer', at: '2026-07-19' } },
159
+ hermes: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target hermes', kind: 'reproducer', at: '2026-07-19' } },
160
+ openclaude: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target openclaude', kind: 'reproducer', at: '2026-07-19' } },
161
+ copilot: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target copilot', kind: 'reproducer', at: '2026-07-19' } },
162
+ 'agents-md': { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target agents-md', kind: 'reproducer', at: '2026-07-19' } },
163
+ cursor: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target cursor', kind: 'reproducer', at: '2026-07-19' } },
164
+ gemini: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target gemini', kind: 'reproducer', at: '2026-07-19' } },
165
+ windsurf: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target windsurf', kind: 'reproducer', at: '2026-07-19' } },
166
+ };
167
+
168
+ export interface UnbackedCapability {
169
+ readonly target: TargetName;
170
+ readonly capability: RuntimeCapability;
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';
176
+ readonly evidence?: string;
177
+ /** `recorded → probed`, for the stale case. */
178
+ readonly recordedVersion?: string;
179
+ readonly probedVersion?: string;
180
+ }
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
+
188
+ /**
189
+ * Every capability grant that is NOT backed by usable evidence.
190
+ *
191
+ * PURE, with the filesystem injected as `transcriptExists`. That is deliberate: the property this
192
+ * enforces — *a grant with a dangling transcript is not a grant* — has to be provable without a
193
+ * repository on disk, or the mutation gate (which copies the PACKAGE, not the repo) could never
194
+ * turn its mutant red, and an unkillable mutant is a false green wearing a gate's clothes.
195
+ */
196
+ export function findUnbackedCapabilities(
197
+ transcriptExists: (path: string) => boolean,
198
+ capabilities: Record<TargetName, readonly RuntimeCapability[]> = TARGET_CAPABILITIES,
199
+ evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
200
+ probedVersions: ProbedRuntimeVersions = {},
201
+ ): UnbackedCapability[] {
202
+ const out: UnbackedCapability[] = [];
203
+ for (const target of Object.keys(capabilities) as TargetName[]) {
204
+ for (const capability of capabilities[target]) {
205
+ const record = evidence[target]?.[capability];
206
+ if (record === undefined) {
207
+ out.push({ target, capability, reason: 'no-evidence-record' });
208
+ continue;
209
+ }
210
+ if (record.kind === 'transcript' && !transcriptExists(record.evidence)) {
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
+ });
226
+ }
227
+ }
228
+ }
229
+ return out;
230
+ }
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
+
65
318
  /** How a feature manifests on a platform: a concrete FORM with its runtime requirements. */
66
319
  export interface FeatureForm {
67
320
  /** Human-readable name of the form, shown as the `via` of a parity cell (AM-2). */
@@ -124,7 +377,7 @@ export const PARITY_FEATURES: readonly ParityFeature[] = [
124
377
  id: 'claim-check',
125
378
  title: 'Integrity claim-check',
126
379
  forms: [
127
- { form: 'PreToolUse hook (automatic on Write/Edit)', requires: ['hooks'], level: 'full' },
380
+ { form: 'PreToolUse hook (automatic on Write/Edit)', requires: ['hooks-write'], level: 'full' },
128
381
  { form: 'dz claim-check (CLI) + publish gate', requires: ['shell'], level: 'manual' },
129
382
  ],
130
383
  },
@@ -137,7 +390,7 @@ export const PARITY_FEATURES: readonly ParityFeature[] = [
137
390
  id: 'learning-apply',
138
391
  title: 'Self-learning: automatic apply-leg',
139
392
  forms: [
140
- { form: 'UserPromptSubmit hook (auto recall)', requires: ['hooks'], level: 'full' },
393
+ { form: 'UserPromptSubmit hook (auto recall)', requires: ['hooks-prompt'], level: 'full' },
141
394
  { form: 'manual dz recall before a task', requires: ['shell'], level: 'manual' },
142
395
  ],
143
396
  },
@@ -215,6 +468,42 @@ export function computeParity(feature: ParityFeature, capabilities: readonly Run
215
468
  return { level: 'none' };
216
469
  }
217
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
+
218
507
  export interface ParityMatrixRow {
219
508
  readonly feature: ParityFeature;
220
509
  readonly cells: Readonly<Record<TargetName, ParityCell>>;