harnery 0.37.0 → 0.38.0

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 (184) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -2
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +61 -1
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +48 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +349 -21
  17. package/dist/commands/fetch.js +1 -0
  18. package/dist/commands/qa-record.d.ts +144 -0
  19. package/dist/commands/qa-record.d.ts.map +1 -0
  20. package/dist/commands/qa-record.js +0 -0
  21. package/dist/commands/qa-run.d.ts +7 -3
  22. package/dist/commands/qa-run.d.ts.map +1 -1
  23. package/dist/commands/qa-run.js +268 -13
  24. package/dist/commands/qa-status.d.ts +71 -0
  25. package/dist/commands/qa-status.d.ts.map +1 -0
  26. package/dist/commands/qa-status.js +490 -0
  27. package/dist/commands/qa-verify.d.ts +40 -0
  28. package/dist/commands/qa-verify.d.ts.map +1 -0
  29. package/dist/commands/qa-verify.js +180 -0
  30. package/dist/commands/review-pack.d.ts +4 -0
  31. package/dist/commands/review-pack.d.ts.map +1 -0
  32. package/dist/commands/review-pack.js +1001 -0
  33. package/dist/core/agents/qa-signal.d.ts +111 -0
  34. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  35. package/dist/core/agents/qa-signal.js +231 -0
  36. package/dist/core/agents/session-name-display.d.ts +20 -5
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +67 -7
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  41. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  43. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  44. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  45. package/dist/core/agents/state/live-coordination-view.js +1 -0
  46. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  47. package/dist/core/artifacts/constants.d.ts +1 -1
  48. package/dist/core/artifacts/constants.js +1 -1
  49. package/dist/core/artifacts/index.d.ts +57 -6
  50. package/dist/core/artifacts/index.d.ts.map +1 -1
  51. package/dist/core/artifacts/index.js +265 -10
  52. package/dist/core/config.d.ts +8 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +16 -0
  55. package/dist/core/diagnostics/bundle.d.ts +16 -0
  56. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  57. package/dist/core/diagnostics/bundle.js +101 -7
  58. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  59. package/dist/core/events/v3/bootstrap.js +10 -0
  60. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  61. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  62. package/dist/core/events/v3/coordination-view.js +69 -8
  63. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  64. package/dist/core/events/v3/producers/intake.js +9 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts +23 -0
  66. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  67. package/dist/core/events/v3/producers/recorder.js +264 -18
  68. package/dist/core/hooks/cli.js +120 -27
  69. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  70. package/dist/core/hooks/resolve/transcript.js +10 -3
  71. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  72. package/dist/core/hooks/session-name-presence.js +4 -1
  73. package/dist/core/qa-artifacts.d.ts +20 -0
  74. package/dist/core/qa-artifacts.d.ts.map +1 -0
  75. package/dist/core/qa-artifacts.js +110 -0
  76. package/dist/core/resources/contract.d.ts +6 -0
  77. package/dist/core/resources/contract.d.ts.map +1 -1
  78. package/dist/core/resources/sampler.d.ts +7 -0
  79. package/dist/core/resources/sampler.d.ts.map +1 -1
  80. package/dist/core/resources/sampler.js +106 -5
  81. package/dist/lib/admission.d.ts +71 -0
  82. package/dist/lib/admission.d.ts.map +1 -0
  83. package/dist/lib/admission.js +264 -0
  84. package/dist/lib/agent-browser/client.d.ts +1 -1
  85. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  86. package/dist/lib/agent-browser/client.js +1 -5
  87. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  88. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  89. package/dist/lib/browser/capture-fidelity.js +84 -0
  90. package/dist/lib/browser/client.d.ts +41 -1
  91. package/dist/lib/browser/client.d.ts.map +1 -1
  92. package/dist/lib/browser/client.js +167 -9
  93. package/dist/lib/browser/critique.d.ts +38 -1
  94. package/dist/lib/browser/critique.d.ts.map +1 -1
  95. package/dist/lib/browser/critique.js +34 -6
  96. package/dist/lib/browser/index.d.ts +4 -2
  97. package/dist/lib/browser/index.d.ts.map +1 -1
  98. package/dist/lib/browser/index.js +2 -0
  99. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  100. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  101. package/dist/lib/browser/page-review-judge.js +270 -0
  102. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  103. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  104. package/dist/lib/browser/page-review-pack.js +1751 -0
  105. package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
  106. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  107. package/dist/lib/browser/qa-run-contracts.js +136 -1
  108. package/dist/lib/browser/qa-run.d.ts +100 -10
  109. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  110. package/dist/lib/browser/qa-run.js +768 -169
  111. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  112. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  113. package/dist/lib/browser/request-diagnostics.js +18 -0
  114. package/dist/lib/browser/tiling.d.ts +19 -0
  115. package/dist/lib/browser/tiling.d.ts.map +1 -1
  116. package/dist/lib/browser/tiling.js +28 -0
  117. package/dist/lib/cookies/client.d.ts +9 -0
  118. package/dist/lib/cookies/client.d.ts.map +1 -1
  119. package/dist/lib/cookies/client.js +197 -44
  120. package/dist/lib/cookies/extra.d.ts +18 -0
  121. package/dist/lib/cookies/extra.d.ts.map +1 -0
  122. package/dist/lib/cookies/extra.js +14 -0
  123. package/dist/lib/cookies/index.d.ts +2 -1
  124. package/dist/lib/cookies/index.d.ts.map +1 -1
  125. package/dist/lib/cookies/index.js +2 -1
  126. package/dist/lib/durable-job.d.ts +124 -0
  127. package/dist/lib/durable-job.d.ts.map +1 -0
  128. package/dist/lib/durable-job.js +296 -0
  129. package/dist/lib/http/client.d.ts +7 -1
  130. package/dist/lib/http/client.d.ts.map +1 -1
  131. package/dist/lib/http/client.js +2 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +5 -2
  134. package/package.json +8 -2
  135. package/src/commander.ts +50 -2
  136. package/src/commands/admission.ts +699 -0
  137. package/src/commands/agents.ts +87 -1
  138. package/src/commands/artifacts.ts +97 -21
  139. package/src/commands/browse-ai.ts +10 -5
  140. package/src/commands/browse.ts +481 -20
  141. package/src/commands/fetch.ts +1 -0
  142. package/src/commands/qa-record.ts +682 -0
  143. package/src/commands/qa-run.ts +335 -16
  144. package/src/commands/qa-status.ts +608 -0
  145. package/src/commands/qa-verify.ts +238 -0
  146. package/src/commands/review-pack.ts +1281 -0
  147. package/src/core/agents/qa-signal.ts +261 -0
  148. package/src/core/agents/session-name-display.ts +78 -7
  149. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  150. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  151. package/src/core/agents/state/live-coordination-view.ts +1 -0
  152. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  153. package/src/core/artifacts/constants.ts +1 -1
  154. package/src/core/artifacts/index.ts +370 -21
  155. package/src/core/config.ts +23 -0
  156. package/src/core/diagnostics/bundle.ts +119 -11
  157. package/src/core/events/v3/bootstrap.ts +10 -0
  158. package/src/core/events/v3/coordination-view.ts +100 -11
  159. package/src/core/events/v3/producers/intake.ts +9 -2
  160. package/src/core/events/v3/producers/recorder.ts +312 -18
  161. package/src/core/hooks/cli.ts +140 -32
  162. package/src/core/hooks/resolve/transcript.ts +10 -3
  163. package/src/core/hooks/session-name-presence.ts +6 -1
  164. package/src/core/qa-artifacts.ts +126 -0
  165. package/src/core/resources/contract.ts +7 -0
  166. package/src/core/resources/sampler.ts +138 -6
  167. package/src/lib/admission.ts +347 -0
  168. package/src/lib/agent-browser/client.ts +2 -10
  169. package/src/lib/browser/capture-fidelity.ts +98 -0
  170. package/src/lib/browser/client.ts +206 -10
  171. package/src/lib/browser/critique.ts +62 -7
  172. package/src/lib/browser/index.ts +36 -0
  173. package/src/lib/browser/page-review-judge.ts +360 -0
  174. package/src/lib/browser/page-review-pack.ts +2384 -0
  175. package/src/lib/browser/qa-run-contracts.ts +366 -3
  176. package/src/lib/browser/qa-run.ts +868 -190
  177. package/src/lib/browser/request-diagnostics.ts +27 -0
  178. package/src/lib/browser/tiling.ts +32 -0
  179. package/src/lib/cookies/client.ts +228 -42
  180. package/src/lib/cookies/extra.ts +28 -0
  181. package/src/lib/cookies/index.ts +2 -0
  182. package/src/lib/durable-job.ts +407 -0
  183. package/src/lib/http/client.ts +13 -1
  184. package/src/lib/instructions/templates.ts +5 -2
@@ -8,16 +8,20 @@ import {
8
8
  realpathSync,
9
9
  } from "node:fs";
10
10
  import { dirname, join, resolve, sep } from "node:path";
11
- import type { ArtifactActor, ArtifactInventoryEntry } from "../artifacts/index.ts";
11
+ import type {
12
+ ArtifactActor,
13
+ ArtifactInventoryEntry,
14
+ ArtifactManifestV2,
15
+ } from "../artifacts/index.ts";
12
16
  import {
13
17
  ARTIFACT_MANIFEST,
14
18
  artifactsRoot,
15
19
  configuredArtifactRetentionDays,
16
20
  createArtifact,
17
21
  inventoryArtifacts,
22
+ parseArtifactManifest,
18
23
  releaseArtifact,
19
24
  resolveArtifactRef,
20
- showArtifact,
21
25
  } from "../artifacts/index.ts";
22
26
  import { currentHarneryRuntimeBuild } from "../events/v3/runtime-build.ts";
23
27
  import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
@@ -104,6 +108,17 @@ export interface DiagnosticBundleListRow {
104
108
  error?: string;
105
109
  }
106
110
 
111
+ export interface DiagnosticBundleCandidateRow {
112
+ artifact_id: string;
113
+ created_at: string;
114
+ expires_at: string | null;
115
+ classification: ArtifactInventoryEntry["classification"];
116
+ selectable: boolean;
117
+ captured_at?: string;
118
+ finding_id?: string;
119
+ error?: string;
120
+ }
121
+
107
122
  export function captureDiagnosticBundle(
108
123
  repoRootRaw: string,
109
124
  input: CaptureDiagnosticBundleInput = {},
@@ -210,10 +225,10 @@ export function captureDiagnosticBundle(
210
225
  return { path: created.path, manifest, summary };
211
226
  }
212
227
 
213
- export function validateDiagnosticBundle(
228
+ function openDiagnosticBundleManifest(
214
229
  repoRootRaw: string,
215
230
  ref: string,
216
- ): ValidatedDiagnosticBundle {
231
+ ): { artifactPath: string; manifest: DiagnosticBundleManifest } {
217
232
  const repoRoot = resolve(repoRootRaw);
218
233
  const artifactPath = resolveArtifactRef(repoRoot, ref);
219
234
  const managedRoot = realpathSync(artifactsRoot(repoRoot));
@@ -225,21 +240,28 @@ export function validateDiagnosticBundle(
225
240
  throw new Error("diagnostic bundle is outside the managed artifact root");
226
241
  }
227
242
  assertBundleTree(artifactPath);
228
- const shown = showArtifact(repoRoot, artifactPath);
243
+ const artifactManifest = readCandidateArtifactManifest(artifactPath);
244
+ const classification = candidateClassification(artifactManifest);
229
245
  if (
230
246
  !["managed-active", "managed-current", "managed-expired", "managed-tracked"].includes(
231
- shown.entry.classification,
247
+ classification,
232
248
  )
233
249
  ) {
234
- throw new Error(
235
- `diagnostic bundle has invalid managed classification ${shown.entry.classification}`,
236
- );
250
+ throw new Error(`diagnostic bundle has invalid managed classification ${classification}`);
237
251
  }
238
- if (shown.manifest.slug !== "diagnostic-bundle") {
252
+ if (artifactManifest.slug !== "diagnostic-bundle") {
239
253
  throw new Error("artifact is not a diagnostic bundle");
240
254
  }
241
255
  const manifest = readJson<DiagnosticBundleManifest>(artifactPath, "diagnostic-manifest.json");
242
- validateManifest(manifest, shown.manifest.artifact_id);
256
+ validateManifest(manifest, artifactManifest.artifact_id);
257
+ return { artifactPath, manifest };
258
+ }
259
+
260
+ export function validateDiagnosticBundle(
261
+ repoRootRaw: string,
262
+ ref: string,
263
+ ): ValidatedDiagnosticBundle {
264
+ const { artifactPath, manifest } = openDiagnosticBundleManifest(repoRootRaw, ref);
243
265
  let totalBytes = 0;
244
266
  for (const file of manifest.files) {
245
267
  const bytes = readValidatedFile(artifactPath, file.path);
@@ -305,6 +327,92 @@ export function listDiagnosticBundles(repoRoot: string): DiagnosticBundleListRow
305
327
  });
306
328
  }
307
329
 
330
+ /**
331
+ * Lists managed bundle candidates without opening their bounded payloads.
332
+ * Selection is not authority: every show, replay, and comparison operation
333
+ * still performs complete digest and schema validation before returning data.
334
+ */
335
+ export function listDiagnosticBundleCandidates(repoRoot: string): DiagnosticBundleCandidateRow[] {
336
+ const root = artifactsRoot(repoRoot);
337
+ if (!existsSync(root)) return [];
338
+ const rows: DiagnosticBundleCandidateRow[] = [];
339
+ for (const name of readdirSync(root).sort()) {
340
+ const artifactPath = join(root, name);
341
+ let artifactManifest: ArtifactManifestV2;
342
+ try {
343
+ const stat = lstatSync(artifactPath);
344
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
345
+ artifactManifest = readCandidateArtifactManifest(artifactPath);
346
+ if (artifactManifest.slug !== "diagnostic-bundle") continue;
347
+ assertBundleTree(artifactPath);
348
+ const manifest = readJson<DiagnosticBundleManifest>(artifactPath, "diagnostic-manifest.json");
349
+ validateManifest(manifest, artifactManifest.artifact_id);
350
+ rows.push({
351
+ artifact_id: artifactManifest.artifact_id,
352
+ created_at: artifactManifest.created_at,
353
+ expires_at: artifactManifest.retention.expires_at,
354
+ classification: candidateClassification(artifactManifest),
355
+ selectable: true,
356
+ captured_at: manifest.captured_at,
357
+ finding_id: manifest.finding_id,
358
+ });
359
+ } catch (error) {
360
+ if (!existsSync(join(artifactPath, "diagnostic-manifest.json"))) continue;
361
+ const fallbackId = candidateArtifactId(artifactPath) ?? name;
362
+ rows.push({
363
+ artifact_id: fallbackId,
364
+ created_at: "",
365
+ expires_at: null,
366
+ classification: "invalid-manifest",
367
+ selectable: false,
368
+ error: error instanceof Error ? error.message : String(error),
369
+ });
370
+ }
371
+ }
372
+ const counts = new Map<string, number>();
373
+ for (const row of rows) counts.set(row.artifact_id, (counts.get(row.artifact_id) ?? 0) + 1);
374
+ return rows.map((row) =>
375
+ counts.get(row.artifact_id) === 1
376
+ ? row
377
+ : { ...row, selectable: false, error: "duplicate managed artifact id" },
378
+ );
379
+ }
380
+
381
+ function readCandidateArtifactManifest(artifactPath: string): ArtifactManifestV2 {
382
+ const manifestPath = join(artifactPath, ARTIFACT_MANIFEST);
383
+ const stat = lstatSync(manifestPath);
384
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 64 * 1024) {
385
+ throw new Error("artifact manifest is not a bounded regular file");
386
+ }
387
+ let manifest: Partial<ArtifactManifestV2>;
388
+ try {
389
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Partial<ArtifactManifestV2>;
390
+ } catch {
391
+ throw new Error("artifact manifest is not valid JSON");
392
+ }
393
+ const parsed = parseArtifactManifest(manifest);
394
+ if (!parsed.ok) throw new Error(parsed.reason);
395
+ return parsed.manifest;
396
+ }
397
+
398
+ function candidateArtifactId(artifactPath: string): string | undefined {
399
+ try {
400
+ return readCandidateArtifactManifest(artifactPath).artifact_id;
401
+ } catch {
402
+ return undefined;
403
+ }
404
+ }
405
+
406
+ function candidateClassification(
407
+ manifest: ArtifactManifestV2,
408
+ ): ArtifactInventoryEntry["classification"] {
409
+ if (manifest.holds.length > 0) return "managed-held";
410
+ if (!manifest.released_at) return "managed-active";
411
+ return Date.parse(manifest.retention.expires_at) <= Date.now()
412
+ ? "managed-expired"
413
+ : "managed-current";
414
+ }
415
+
308
416
  export function replayDiagnosticBundle(
309
417
  repoRoot: string,
310
418
  ref: string,
@@ -30,6 +30,7 @@ import {
30
30
  import { repairEventV3ControlPair } from "./control-writer.ts";
31
31
  import { loadOrCreateFingerprintKeyStoreV3 } from "./fingerprint-keys.ts";
32
32
  import { EVENT_V3_SCHEMA_DIGEST } from "./generated.ts";
33
+ import { reanchorArchivedHookProducersV3 } from "./producers/recorder.ts";
33
34
  import { resolveEventLedgerRotateActiveBytesV3 } from "./rotation-config.ts";
34
35
  import { currentHarneryRuntimeBuild, liveEventV3BuildId, livePlatformV3 } from "./runtime-build.ts";
35
36
  import { drainReadyEventsV3 } from "./writer.ts";
@@ -210,6 +211,15 @@ export function rotateOversizedEventLedgerV3(
210
211
  approvalRecordId: "harnery-runtime-v3-size-rotation",
211
212
  forceNewEpoch: true,
212
213
  });
214
+ if (initialized.archived_epoch) {
215
+ reanchorArchivedHookProducersV3({
216
+ coordRoot: root,
217
+ archivedEpoch: initialized.archived_epoch,
218
+ mode: initialized.control.state,
219
+ build_id: liveEventV3BuildId(currentHarneryRuntimeBuild()),
220
+ platform: livePlatformV3(),
221
+ });
222
+ }
213
223
  return {
214
224
  state: "rotated",
215
225
  active_bytes: activeBytes,
@@ -52,8 +52,11 @@ export interface CoordinationViewV3 {
52
52
  contract_major: 2;
53
53
  reducer_build_id: typeof EVENT_V3_SAFETY_REDUCER_BUILD;
54
54
  source_complete: boolean;
55
+ /** True when no unscoped authority diagnostic can affect every generation. */
55
56
  authority_safe: boolean;
56
57
  diagnostics: SafetyProjectionDiagnosticV3[];
58
+ global_diagnostics: SafetyProjectionDiagnosticV3[];
59
+ diagnostics_by_generation: Record<string, SafetyProjectionDiagnosticV3[]>;
57
60
  instances: Record<string, CoordinationGenerationViewV3>;
58
61
  terminal_generations: Record<string, CoordinationGenerationViewV3>;
59
62
  delegations: SafetyProjectionV3["delegations"];
@@ -95,7 +98,9 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
95
98
  const safety = reduceSafetyProjectionV3(read);
96
99
  const starts = new Map<string, Extract<EventV3, { event_type: "session.started" }>>();
97
100
  const eventGeneration = new Map<string, string>();
101
+ const eventTypes = new Map<string, EventV3["event_type"]>();
98
102
  for (const { event } of read.events) {
103
+ eventTypes.set(event.event_id, event.event_type);
99
104
  const generationId = "generation_id" in event.scope ? event.scope.generation_id : undefined;
100
105
  if (generationId !== undefined) {
101
106
  eventGeneration.set(event.event_id, generationId);
@@ -105,6 +110,15 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
105
110
  }
106
111
  }
107
112
 
113
+ const { globalDiagnostics, diagnosticsByGeneration } = partitionCoordinationDiagnosticsV3(
114
+ safety,
115
+ eventGeneration,
116
+ eventTypes,
117
+ );
118
+ const globalAuthoritySafe = !globalDiagnostics.some(
119
+ (diagnostic) => diagnostic.authority_blocking,
120
+ );
121
+
108
122
  const instances: Record<string, CoordinationGenerationViewV3> = {};
109
123
  const terminalGenerations: Record<string, CoordinationGenerationViewV3> = {};
110
124
  const states = Object.values(safety.generations).sort((left, right) =>
@@ -113,14 +127,7 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
113
127
  for (const state of states) {
114
128
  const started = starts.get(state.generation_id);
115
129
  if (!started) continue;
116
- const diagnostics = safety.diagnostics.filter(
117
- (diagnostic) =>
118
- diagnostic.generation_id === state.generation_id ||
119
- diagnostic.subject_instance_id === state.instance_id ||
120
- (diagnostic.event_id !== undefined &&
121
- eventGeneration.get(diagnostic.event_id) === state.generation_id) ||
122
- diagnostic.code === "ledger_incomplete",
123
- );
130
+ const diagnostics = diagnosticsByGeneration[state.generation_id] ?? [];
124
131
  const delegation = Object.values(safety.delegations).find(
125
132
  (candidate) => candidate.child_generation_id === state.generation_id,
126
133
  );
@@ -173,8 +180,11 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
173
180
  last_context_event_id: state.last_context_event_id,
174
181
  provisional_termination: state.provisional_termination,
175
182
  terminal: state.terminal,
176
- evidence_complete: safety.evidence_complete && diagnostics.length === 0,
177
- authority_eligible: safety.authority_safe && state.phase === "live",
183
+ evidence_complete: globalDiagnostics.length === 0 && diagnostics.length === 0,
184
+ authority_eligible:
185
+ globalAuthoritySafe &&
186
+ !diagnostics.some((diagnostic) => diagnostic.authority_blocking) &&
187
+ state.phase === "live",
178
188
  };
179
189
  if (state.phase === "terminal") {
180
190
  terminalGenerations[state.generation_id] = view;
@@ -188,8 +198,10 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
188
198
  contract_major: 2,
189
199
  reducer_build_id: EVENT_V3_SAFETY_REDUCER_BUILD,
190
200
  source_complete: read.complete,
191
- authority_safe: safety.authority_safe,
201
+ authority_safe: globalAuthoritySafe,
192
202
  diagnostics: safety.diagnostics,
203
+ global_diagnostics: globalDiagnostics,
204
+ diagnostics_by_generation: diagnosticsByGeneration,
193
205
  instances,
194
206
  terminal_generations: terminalGenerations,
195
207
  delegations: safety.delegations,
@@ -200,6 +212,83 @@ export function projectCoordinationViewV3(read: ReadLedgerV3Result): Coordinatio
200
212
  return view;
201
213
  }
202
214
 
215
+ /**
216
+ * Partition reducer findings at the narrowest authority boundary that can be
217
+ * proven from canonical event attribution. A diagnostic stays global when it
218
+ * concerns shared claim/decision state, lacks a live generation witness, or
219
+ * came from the canonical reader. Reader failures deliberately remain global:
220
+ * an incomplete read cannot produce the append checkpoint that proves causal
221
+ * parents and attestations for later writes.
222
+ */
223
+ function partitionCoordinationDiagnosticsV3(
224
+ safety: SafetyProjectionV3,
225
+ eventGeneration: ReadonlyMap<string, string>,
226
+ eventTypes: ReadonlyMap<string, EventV3["event_type"]>,
227
+ ): {
228
+ globalDiagnostics: SafetyProjectionDiagnosticV3[];
229
+ diagnosticsByGeneration: Record<string, SafetyProjectionDiagnosticV3[]>;
230
+ } {
231
+ const globalDiagnostics: SafetyProjectionDiagnosticV3[] = [];
232
+ const diagnosticsByGeneration: Record<string, SafetyProjectionDiagnosticV3[]> = {};
233
+ for (const diagnostic of safety.diagnostics) {
234
+ const generationIds = diagnosticGenerationIdsV3(diagnostic, safety, eventGeneration);
235
+ if (
236
+ generationIds.length === 0 ||
237
+ diagnosticRequiresGlobalAuthorityV3(diagnostic, eventTypes.get(diagnostic.event_id ?? ""))
238
+ ) {
239
+ globalDiagnostics.push(diagnostic);
240
+ continue;
241
+ }
242
+ for (const generationId of generationIds) {
243
+ const generationDiagnostics = diagnosticsByGeneration[generationId] ?? [];
244
+ generationDiagnostics.push(diagnostic);
245
+ diagnosticsByGeneration[generationId] = generationDiagnostics;
246
+ }
247
+ }
248
+ return { globalDiagnostics, diagnosticsByGeneration };
249
+ }
250
+
251
+ function diagnosticGenerationIdsV3(
252
+ diagnostic: SafetyProjectionDiagnosticV3,
253
+ safety: SafetyProjectionV3,
254
+ eventGeneration: ReadonlyMap<string, string>,
255
+ ): string[] {
256
+ const exact = new Set<string>();
257
+ if (diagnostic.generation_id && safety.generations[diagnostic.generation_id]) {
258
+ exact.add(diagnostic.generation_id);
259
+ }
260
+ const eventGenerationId = diagnostic.event_id
261
+ ? eventGeneration.get(diagnostic.event_id)
262
+ : undefined;
263
+ if (eventGenerationId && safety.generations[eventGenerationId]) exact.add(eventGenerationId);
264
+ if (exact.size > 0) return [...exact].sort();
265
+
266
+ const subjectGenerationId = diagnostic.subject_instance_id
267
+ ? safety.current_generation_by_instance[diagnostic.subject_instance_id]
268
+ : undefined;
269
+ return subjectGenerationId && safety.generations[subjectGenerationId]
270
+ ? [subjectGenerationId]
271
+ : [];
272
+ }
273
+
274
+ function diagnosticRequiresGlobalAuthorityV3(
275
+ diagnostic: SafetyProjectionDiagnosticV3,
276
+ eventType: EventV3["event_type"] | undefined,
277
+ ): boolean {
278
+ if (diagnostic.code === "ledger_incomplete") return true;
279
+ if (diagnostic.code === "decision_prior_mismatch") return true;
280
+ if (
281
+ diagnostic.code === "claim_acquire_conflict" ||
282
+ diagnostic.code === "claim_release_without_acquire"
283
+ ) {
284
+ return true;
285
+ }
286
+ return (
287
+ diagnostic.code === "authority_attribution_unverified" &&
288
+ (eventType === "decision.state_changed" || eventType === "coord.claim_changed")
289
+ );
290
+ }
291
+
203
292
  /** Refuse authority decisions while still allowing callers to render diagnostics. */
204
293
  export function requireAuthoritySafeCoordinationViewV3(
205
294
  view: CoordinationViewV3,
@@ -12,7 +12,7 @@ import {
12
12
  unlinkSync,
13
13
  writeFileSync,
14
14
  } from "node:fs";
15
- import { join, resolve } from "node:path";
15
+ import { dirname, join, resolve } from "node:path";
16
16
  import type { Adapter } from "../../../adapter.ts";
17
17
  import type { ParsedPayload } from "../../../hooks/adapter/parse.ts";
18
18
  import { fsyncParentDirectory } from "../../../workflow/durable-record.ts";
@@ -224,7 +224,14 @@ export function listHookIntakeRecordsV3(directory: string): HookIntakeEntryV3[]
224
224
  }
225
225
 
226
226
  export function removeIntakeRecordV3(path: string): void {
227
- if (existsSync(path)) unlinkSync(path);
227
+ if (existsSync(path)) {
228
+ unlinkSync(path);
229
+ } else if (!existsSync(dirname(path))) {
230
+ // Epoch rotation atomically moves the complete intake tree into the
231
+ // archive. A producer that was already draining the old path has nothing
232
+ // left to remove or fsync in the successor.
233
+ return;
234
+ }
228
235
  fsyncParentDirectory(path);
229
236
  }
230
237