harnery 0.36.0 → 0.37.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 (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,874 @@
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, } from "node:fs";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { ARTIFACT_MANIFEST, artifactsRoot, configuredArtifactRetentionDays, createArtifact, inventoryArtifacts, releaseArtifact, resolveArtifactRef, showArtifact, } from "../artifacts/index.js";
4
+ import { currentHarneryRuntimeBuild } from "../events/v3/runtime-build.js";
5
+ import { writePrivateJsonAtomic } from "../storage/atomic-json.js";
6
+ import { SUPERVISOR_DIAGNOSTIC_LIMITS, SUPERVISOR_EXPLANATION_SCHEMA_VERSION, SUPERVISOR_FINDING_POLICY, SUPERVISOR_FINDING_SCHEMA_VERSION, SUPERVISOR_RESOURCE_BUDGET, SUPERVISOR_TIMELINE_SCHEMA_VERSION, } from "../supervisor/contract.js";
7
+ import { readWorkflowProof } from "../workflow/proof.js";
8
+ import { DIAGNOSTIC_ADVICE_LIMITS, DIAGNOSTIC_ADVICE_SCHEMA_VERSION, DIAGNOSTIC_BUNDLE_FILES, DIAGNOSTIC_BUNDLE_SCHEMA_VERSION, DIAGNOSTIC_COMMAND_SCHEMA_VERSION, DIAGNOSTIC_EXPECTED_SCHEMA_VERSION, DIAGNOSTIC_INPUT_SCHEMA_VERSION, DIAGNOSTIC_SUMMARY_SCHEMA_VERSION, } from "./contract.js";
9
+ import { pseudonymousMachineId } from "./identity.js";
10
+ import { canonicalJson, deriveCapturedExpected, replayDiagnosticInputs, sha256 } from "./replay.js";
11
+ import { sanitizeDiagnosticValue } from "./sanitize.js";
12
+ const SOURCE_FILE_LIMIT_BYTES = 1_500_000;
13
+ const DEFAULT_WINDOW_MS = 15 * 60 * 1_000;
14
+ const SHADOW_ADMISSION_CAPTURE_LIMIT = 20;
15
+ const PAYLOAD_FILES = DIAGNOSTIC_BUNDLE_FILES.filter((path) => path !== "diagnostic-manifest.json");
16
+ const SOURCE_SPECS = [
17
+ ["supervisor.snapshot", ".harnery/supervisor/snapshot.json"],
18
+ ["supervisor.history", ".harnery/supervisor/history.json"],
19
+ ["supervisor.findings", ".harnery/supervisor/findings.json"],
20
+ ["supervisor.activity", ".harnery/supervisor/activity.json"],
21
+ ["supervisor.timelines", ".harnery/supervisor/timelines"],
22
+ ["supervisor.explanations", ".harnery/supervisor/explanations"],
23
+ ["supervisor.log-feed", ".harnery/supervisor/log-feed.json"],
24
+ ["supervisor.hook-health", ".harnery/supervisor/hook-health.json"],
25
+ ["resources.snapshot", ".harnery/resources/snapshot.json"],
26
+ ["coordination.health", ".harnery/supervisor/coordination-health.json"],
27
+ ];
28
+ export function captureDiagnosticBundle(repoRootRaw, input = {}) {
29
+ const repoRoot = resolve(repoRootRaw);
30
+ const now = input.now ?? new Date();
31
+ assertValidDate(now, "now");
32
+ const capturedAt = now.toISOString();
33
+ const aggregateStats = { sanitized_value_count: 0, omitted_value_count: 0 };
34
+ const sources = [
35
+ ...SOURCE_SPECS.map(([sourceKind, relativePath]) => readCapturedSource(repoRoot, sourceKind, relativePath, aggregateStats)),
36
+ readShadowAdmissionSource(repoRoot, capturedAt, aggregateStats),
37
+ ];
38
+ const selection = diagnosticSelection(input, sources, now);
39
+ const observations = {
40
+ schema_version: DIAGNOSTIC_INPUT_SCHEMA_VERSION,
41
+ captured_at: capturedAt,
42
+ selection,
43
+ sources,
44
+ };
45
+ const thresholds = {
46
+ schema_version: DIAGNOSTIC_INPUT_SCHEMA_VERSION,
47
+ values: {
48
+ diagnostic_limits: SUPERVISOR_DIAGNOSTIC_LIMITS,
49
+ finding_policy: SUPERVISOR_FINDING_POLICY,
50
+ resource_budget: SUPERVISOR_RESOURCE_BUDGET,
51
+ },
52
+ };
53
+ const expected = deriveCapturedExpected(observations, thresholds);
54
+ if (input.findingId && !expected.findings.some((finding) => finding.id === input.findingId)) {
55
+ throw new Error(`finding "${input.findingId}" is not present in the captured findings projection`);
56
+ }
57
+ const created = createArtifact(repoRoot, {
58
+ slug: "diagnostic-bundle",
59
+ purpose: input.findingId
60
+ ? `Frozen diagnostics for finding ${input.findingId}`
61
+ : `Frozen diagnostics from ${selection.start_at} to ${selection.end_at}`,
62
+ retentionDays: input.retentionDays ?? configuredArtifactRetentionDays(repoRoot),
63
+ actor: input.actor,
64
+ now,
65
+ });
66
+ const inputsPath = join(created.path, "inputs");
67
+ mkdirSync(inputsPath, { recursive: true, mode: 0o700 });
68
+ tryChmod(inputsPath, 0o700);
69
+ const payloads = {
70
+ "inputs/observations.json": observations,
71
+ "inputs/thresholds.json": thresholds,
72
+ "expected.json": expected,
73
+ "summary.json": {},
74
+ };
75
+ const summaryBase = {
76
+ schema_version: DIAGNOSTIC_SUMMARY_SCHEMA_VERSION,
77
+ artifact_id: created.manifest.artifact_id,
78
+ captured_at: capturedAt,
79
+ machine_id: pseudonymousMachineId(input.machineLabel),
80
+ machine_id_kind: "pseudonymous",
81
+ selection,
82
+ source_count: sources.length,
83
+ supported_source_count: sources.filter((source) => source.capability === "supported").length,
84
+ sanitized_value_count: aggregateStats.sanitized_value_count,
85
+ omitted_value_count: aggregateStats.omitted_value_count,
86
+ };
87
+ const summary = stableSummary(summaryBase, payloads);
88
+ payloads["summary.json"] = summary;
89
+ for (const path of PAYLOAD_FILES)
90
+ writePrivateJsonAtomic(join(created.path, path), payloads[path]);
91
+ const files = PAYLOAD_FILES.map((path) => describeFile(created.path, path));
92
+ const capabilities = sources.map(sourceCapability);
93
+ const manifest = {
94
+ schema_version: DIAGNOSTIC_BUNDLE_SCHEMA_VERSION,
95
+ artifact_id: created.manifest.artifact_id,
96
+ captured_at: capturedAt,
97
+ machine_id: summary.machine_id,
98
+ finding_id: input.findingId,
99
+ time_range: { start_at: selection.start_at, end_at: selection.end_at },
100
+ engine_version: input.engineVersion ?? currentHarneryRuntimeBuild(),
101
+ threshold_digest: expected.threshold_digest,
102
+ sources: sources.map((source) => ({
103
+ source_kind: source.source_kind,
104
+ schema_version: source.schema_version,
105
+ capability: source.capability,
106
+ entry_count: countEntries(source.value),
107
+ omitted_count: source.value === undefined ? 1 : 0,
108
+ file: "inputs/observations.json",
109
+ })),
110
+ capabilities,
111
+ files,
112
+ replay: {
113
+ input_file: "inputs/observations.json",
114
+ thresholds_file: "inputs/thresholds.json",
115
+ expected_file: "expected.json",
116
+ },
117
+ };
118
+ writePrivateJsonAtomic(join(created.path, "diagnostic-manifest.json"), manifest);
119
+ validateDiagnosticBundle(repoRoot, created.manifest.artifact_id);
120
+ releaseArtifact(repoRoot, created.manifest.artifact_id, { actor: input.actor, now });
121
+ return { path: created.path, manifest, summary };
122
+ }
123
+ export function validateDiagnosticBundle(repoRootRaw, ref) {
124
+ const repoRoot = resolve(repoRootRaw);
125
+ const artifactPath = resolveArtifactRef(repoRoot, ref);
126
+ const managedRoot = realpathSync(artifactsRoot(repoRoot));
127
+ const resolvedArtifact = realpathSync(artifactPath);
128
+ if (dirname(resolvedArtifact) !== managedRoot ||
129
+ !resolvedArtifact.startsWith(`${managedRoot}${sep}`)) {
130
+ throw new Error("diagnostic bundle is outside the managed artifact root");
131
+ }
132
+ assertBundleTree(artifactPath);
133
+ const shown = showArtifact(repoRoot, artifactPath);
134
+ if (!["managed-active", "managed-current", "managed-expired", "managed-tracked"].includes(shown.entry.classification)) {
135
+ throw new Error(`diagnostic bundle has invalid managed classification ${shown.entry.classification}`);
136
+ }
137
+ if (shown.manifest.slug !== "diagnostic-bundle") {
138
+ throw new Error("artifact is not a diagnostic bundle");
139
+ }
140
+ const manifest = readJson(artifactPath, "diagnostic-manifest.json");
141
+ validateManifest(manifest, shown.manifest.artifact_id);
142
+ let totalBytes = 0;
143
+ for (const file of manifest.files) {
144
+ const bytes = readValidatedFile(artifactPath, file.path);
145
+ totalBytes += bytes.byteLength;
146
+ if (bytes.byteLength !== file.bytes)
147
+ throw new Error(`byte length mismatch for ${file.path}`);
148
+ if (sha256(bytes) !== file.sha256)
149
+ throw new Error(`digest mismatch for ${file.path}`);
150
+ }
151
+ if (totalBytes > SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes) {
152
+ throw new Error("diagnostic bundle exceeds the maximum byte limit");
153
+ }
154
+ const observations = readJson(artifactPath, manifest.replay.input_file);
155
+ const thresholds = readJson(artifactPath, manifest.replay.thresholds_file);
156
+ const expected = readJson(artifactPath, manifest.replay.expected_file);
157
+ const summary = readJson(artifactPath, "summary.json");
158
+ validatePayloads(manifest, observations, thresholds, expected, summary);
159
+ return { path: artifactPath, manifest, observations, thresholds, expected, summary };
160
+ }
161
+ /** Frozen web readers accept an opaque artifact id, never a filesystem path. */
162
+ export function readFrozenDiagnosticBundle(repoRoot, artifactId) {
163
+ if (!/^[A-Za-z0-9_-]{1,160}$/.test(artifactId)) {
164
+ throw new Error("diagnostic bundle id must be an opaque artifact id");
165
+ }
166
+ return validateDiagnosticBundle(repoRoot, artifactId);
167
+ }
168
+ export function showDiagnosticBundle(repoRoot, ref) {
169
+ return validateDiagnosticBundle(repoRoot, ref);
170
+ }
171
+ export function listDiagnosticBundles(repoRoot) {
172
+ return inventoryArtifacts(repoRoot)
173
+ .filter((entry) => entry.slug === "diagnostic-bundle" && entry.artifact_id)
174
+ .map((entry) => {
175
+ try {
176
+ const bundle = validateDiagnosticBundle(repoRoot, entry.artifact_id);
177
+ return {
178
+ artifact_id: entry.artifact_id,
179
+ created_at: entry.created_at ?? bundle.manifest.captured_at,
180
+ expires_at: entry.expires_at,
181
+ classification: entry.classification,
182
+ valid: true,
183
+ captured_at: bundle.manifest.captured_at,
184
+ finding_id: bundle.manifest.finding_id,
185
+ };
186
+ }
187
+ catch (error) {
188
+ return {
189
+ artifact_id: entry.artifact_id,
190
+ created_at: entry.created_at ?? "",
191
+ expires_at: entry.expires_at,
192
+ classification: entry.classification,
193
+ valid: false,
194
+ error: error instanceof Error ? error.message : String(error),
195
+ };
196
+ }
197
+ });
198
+ }
199
+ export function replayDiagnosticBundle(repoRoot, ref, now = new Date()) {
200
+ assertValidDate(now, "now");
201
+ const bundle = validateDiagnosticBundle(repoRoot, ref);
202
+ const actual = replayDiagnosticInputs(bundle.observations, bundle.thresholds);
203
+ const expectedDigest = sha256(canonicalJson(bundle.expected));
204
+ const actualDigest = sha256(canonicalJson(actual));
205
+ return {
206
+ schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
207
+ artifact_id: bundle.manifest.artifact_id,
208
+ replayed_at: now.toISOString(),
209
+ matched: expectedDigest === actualDigest,
210
+ threshold_digest: actual.threshold_digest,
211
+ expected_digest: expectedDigest,
212
+ actual_digest: actualDigest,
213
+ finding_count: actual.findings.length,
214
+ };
215
+ }
216
+ function readCapturedSource(repoRoot, sourceKind, relativePath, aggregateStats) {
217
+ const path = join(repoRoot, relativePath);
218
+ if (!existsSync(path)) {
219
+ return { source_kind: sourceKind, capability: "unsupported", reason_code: "source_missing" };
220
+ }
221
+ try {
222
+ const stat = lstatSync(path);
223
+ if (stat.isSymbolicLink())
224
+ throw new Error("source_is_symlink");
225
+ const parsed = stat.isDirectory()
226
+ ? readJsonDirectory(path)
227
+ : stat.isFile()
228
+ ? readBoundedJsonFile(path)
229
+ : (() => {
230
+ throw new Error("source_is_not_regular_file");
231
+ })();
232
+ const sanitized = sanitizeDiagnosticValue(parsed);
233
+ aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
234
+ aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
235
+ const value = sanitized.value;
236
+ const record = object(value);
237
+ return {
238
+ source_kind: sourceKind,
239
+ capability: "supported",
240
+ schema_version: numeric(record?.schema_version),
241
+ observed_at: timestamp(record),
242
+ value,
243
+ };
244
+ }
245
+ catch (error) {
246
+ return {
247
+ source_kind: sourceKind,
248
+ capability: "error",
249
+ reason_code: error instanceof Error ? error.message : String(error),
250
+ };
251
+ }
252
+ }
253
+ function readShadowAdmissionSource(repoRoot, capturedAt, aggregateStats) {
254
+ const sourceKind = "workflow.diagnostic-admission";
255
+ const workflowsRoot = join(repoRoot, ".harnery", "workflows");
256
+ if (!existsSync(workflowsRoot)) {
257
+ return { source_kind: sourceKind, capability: "unsupported", reason_code: "source_missing" };
258
+ }
259
+ try {
260
+ const rootStat = lstatSync(workflowsRoot);
261
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
262
+ throw new Error("workflow_root_is_not_regular_directory");
263
+ }
264
+ const candidates = readdirSync(workflowsRoot, { withFileTypes: true })
265
+ .filter((entry) => entry.isDirectory() && /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(entry.name))
266
+ .map((entry) => entry.name)
267
+ .sort((left, right) => right.localeCompare(left));
268
+ const records = [];
269
+ let invalidCount = 0;
270
+ const scanLimit = SHADOW_ADMISSION_CAPTURE_LIMIT * 2;
271
+ for (const runId of candidates.slice(0, scanLimit)) {
272
+ try {
273
+ const proof = readWorkflowProof(repoRoot, runId);
274
+ const admission = proof.diagnostic_admission;
275
+ if (!admission)
276
+ continue;
277
+ records.push({
278
+ run_id: proof.run.id,
279
+ ended_at: proof.run.ended_at,
280
+ state: admission.state,
281
+ action: admission.action,
282
+ ...(admission.reason_code ? { reason_code: admission.reason_code } : {}),
283
+ ...(admission.observation
284
+ ? {
285
+ pressure: admission.observation.advice.pressure,
286
+ fan_out_recommendation: admission.observation.advice.fan_out_recommendation,
287
+ freshness: admission.observation.freshness,
288
+ service_state: admission.observation.service_state,
289
+ wait_ms: admission.observation.wait_ms,
290
+ }
291
+ : {}),
292
+ });
293
+ if (records.length >= SHADOW_ADMISSION_CAPTURE_LIMIT)
294
+ break;
295
+ }
296
+ catch {
297
+ invalidCount += 1;
298
+ }
299
+ }
300
+ const omittedCount = Math.max(0, candidates.length - Math.min(candidates.length, scanLimit));
301
+ const projection = {
302
+ schema_version: 1,
303
+ captured_at: capturedAt,
304
+ max_records: SHADOW_ADMISSION_CAPTURE_LIMIT,
305
+ examined_run_count: Math.min(candidates.length, scanLimit),
306
+ omitted_run_count: omittedCount,
307
+ invalid_run_count: invalidCount,
308
+ records,
309
+ };
310
+ const sanitized = sanitizeDiagnosticValue(projection);
311
+ aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
312
+ aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
313
+ return {
314
+ source_kind: sourceKind,
315
+ capability: omittedCount > 0 || invalidCount > 0 ? "partial" : "supported",
316
+ schema_version: 1,
317
+ observed_at: records[0]?.ended_at,
318
+ value: sanitized.value,
319
+ ...(omittedCount > 0
320
+ ? { reason_code: "bounded_workflow_window_truncated" }
321
+ : invalidCount > 0
322
+ ? { reason_code: "invalid_workflow_proofs_ignored" }
323
+ : {}),
324
+ };
325
+ }
326
+ catch (error) {
327
+ return {
328
+ source_kind: sourceKind,
329
+ capability: "error",
330
+ reason_code: error instanceof Error ? error.message : String(error),
331
+ };
332
+ }
333
+ }
334
+ function readBoundedJsonFile(path) {
335
+ const stat = lstatSync(path);
336
+ if (stat.isSymbolicLink() || !stat.isFile())
337
+ throw new Error("source_is_not_regular_file");
338
+ if (stat.size > SOURCE_FILE_LIMIT_BYTES)
339
+ throw new Error("source_exceeds_byte_limit");
340
+ return JSON.parse(readFileSync(path, "utf8"));
341
+ }
342
+ function readJsonDirectory(path) {
343
+ const values = [];
344
+ let totalBytes = 0;
345
+ const names = readdirSync(path).sort();
346
+ if (names.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings) {
347
+ throw new Error("source_directory_exceeds_entry_limit");
348
+ }
349
+ for (const name of names) {
350
+ if (!/^[A-Za-z0-9_.-]+\.json$/.test(name)) {
351
+ throw new Error("source_directory_contains_unexpected_entry");
352
+ }
353
+ const file = join(path, name);
354
+ const stat = lstatSync(file);
355
+ if (stat.isSymbolicLink() || !stat.isFile()) {
356
+ throw new Error("source_directory_contains_nonregular_file");
357
+ }
358
+ totalBytes += stat.size;
359
+ if (totalBytes > SOURCE_FILE_LIMIT_BYTES)
360
+ throw new Error("source_exceeds_byte_limit");
361
+ values.push(readBoundedJsonFile(file));
362
+ }
363
+ return values;
364
+ }
365
+ function diagnosticSelection(input, sources, now) {
366
+ if ((input.startAt === undefined) !== (input.endAt === undefined)) {
367
+ throw new Error("--from and --to must be supplied together");
368
+ }
369
+ if (input.findingId && input.startAt) {
370
+ throw new Error("--finding cannot be combined with --from and --to");
371
+ }
372
+ if (input.findingId) {
373
+ if (!/^[A-Za-z0-9_.:-]{1,240}$/.test(input.findingId))
374
+ throw new Error("invalid finding id");
375
+ const finding = allFindings(sources).find((candidate) => candidate.id === input.findingId);
376
+ if (!finding) {
377
+ return {
378
+ finding_id: input.findingId,
379
+ start_at: now.toISOString(),
380
+ end_at: now.toISOString(),
381
+ };
382
+ }
383
+ return {
384
+ finding_id: input.findingId,
385
+ start_at: finding.opened_at,
386
+ end_at: finding.resolved_at ?? finding.observed_at,
387
+ };
388
+ }
389
+ const start = input.startAt
390
+ ? parseIso(input.startAt, "--from")
391
+ : new Date(now.getTime() - DEFAULT_WINDOW_MS);
392
+ const end = input.endAt ? parseIso(input.endAt, "--to") : now;
393
+ if (start.getTime() > end.getTime())
394
+ throw new Error("--from must not follow --to");
395
+ return { start_at: start.toISOString(), end_at: end.toISOString() };
396
+ }
397
+ function allFindings(sources) {
398
+ const value = sources.find((source) => source.source_kind === "supervisor.findings")?.value;
399
+ const record = object(value);
400
+ return [
401
+ ...arrayObjects(record?.active),
402
+ ...arrayObjects(record?.transitions),
403
+ ];
404
+ }
405
+ function sourceCapability(source) {
406
+ return {
407
+ source_kind: source.source_kind,
408
+ state: source.capability,
409
+ reason_code: source.reason_code,
410
+ };
411
+ }
412
+ function stableSummary(base, payloads) {
413
+ let summary = { ...base, total_bytes: 0 };
414
+ for (let attempt = 0; attempt < 8; attempt += 1) {
415
+ const total = PAYLOAD_FILES.reduce((sum, path) => {
416
+ const value = path === "summary.json" ? summary : payloads[path];
417
+ return sum + Buffer.byteLength(`${JSON.stringify(value)}\n`);
418
+ }, 0);
419
+ if (total === summary.total_bytes)
420
+ return summary;
421
+ summary = { ...base, total_bytes: total };
422
+ }
423
+ return summary;
424
+ }
425
+ function describeFile(root, path) {
426
+ const bytes = readValidatedFile(root, path);
427
+ return { path, media_type: "application/json", bytes: bytes.byteLength, sha256: sha256(bytes) };
428
+ }
429
+ function assertBundleTree(root) {
430
+ const rootStat = lstatSync(root);
431
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
432
+ throw new Error("diagnostic bundle root must be a regular directory");
433
+ }
434
+ const top = readdirSync(root).sort();
435
+ const expectedTop = [
436
+ ARTIFACT_MANIFEST,
437
+ "diagnostic-manifest.json",
438
+ "expected.json",
439
+ "inputs",
440
+ "summary.json",
441
+ ].sort();
442
+ if (top.join("\0") !== expectedTop.join("\0"))
443
+ throw new Error("diagnostic bundle file set is not exact");
444
+ const inputs = join(root, "inputs");
445
+ const inputsStat = lstatSync(inputs);
446
+ if (inputsStat.isSymbolicLink() || !inputsStat.isDirectory()) {
447
+ throw new Error("diagnostic bundle inputs must be a regular directory");
448
+ }
449
+ const inputFiles = readdirSync(inputs).sort();
450
+ if (inputFiles.join("\0") !== ["observations.json", "thresholds.json"].join("\0")) {
451
+ throw new Error("diagnostic bundle input file set is not exact");
452
+ }
453
+ for (const path of [...PAYLOAD_FILES, "diagnostic-manifest.json"]) {
454
+ readValidatedFile(root, path);
455
+ }
456
+ }
457
+ function readValidatedFile(rootRaw, relativePath) {
458
+ if (!PAYLOAD_FILES.includes(relativePath) &&
459
+ relativePath !== "diagnostic-manifest.json") {
460
+ throw new Error(`unexpected diagnostic bundle path ${relativePath}`);
461
+ }
462
+ const root = realpathSync(resolve(rootRaw));
463
+ const path = resolve(root, relativePath);
464
+ if (!path.startsWith(`${root}${sep}`))
465
+ throw new Error(`path escapes diagnostic bundle: ${relativePath}`);
466
+ const stat = lstatSync(path);
467
+ if (stat.isSymbolicLink() || !stat.isFile())
468
+ throw new Error(`${relativePath} is not a regular file`);
469
+ const real = realpathSync(path);
470
+ if (!real.startsWith(`${root}${sep}`))
471
+ throw new Error(`path escapes diagnostic bundle: ${relativePath}`);
472
+ if (stat.size > SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes) {
473
+ throw new Error(`${relativePath} exceeds the maximum byte limit`);
474
+ }
475
+ return readFileSync(real);
476
+ }
477
+ function readJson(root, relativePath) {
478
+ const bytes = readValidatedFile(root, relativePath);
479
+ try {
480
+ const value = JSON.parse(bytes.toString("utf8"));
481
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
482
+ throw new Error("root must be an object");
483
+ }
484
+ return value;
485
+ }
486
+ catch (error) {
487
+ throw new Error(`${relativePath} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
488
+ }
489
+ }
490
+ function validateManifest(manifest, artifactId) {
491
+ assertKeys(manifest, [
492
+ "schema_version",
493
+ "artifact_id",
494
+ "captured_at",
495
+ "machine_id",
496
+ "time_range",
497
+ "engine_version",
498
+ "threshold_digest",
499
+ "sources",
500
+ "capabilities",
501
+ "files",
502
+ "replay",
503
+ ], ["finding_id"], "diagnostic manifest");
504
+ if (manifest.schema_version !== DIAGNOSTIC_BUNDLE_SCHEMA_VERSION)
505
+ throw new Error("unsupported diagnostic manifest schema");
506
+ if (manifest.artifact_id !== artifactId)
507
+ throw new Error("diagnostic artifact id does not match managed artifact id");
508
+ if (!validIso(manifest.captured_at) ||
509
+ !validIso(manifest.time_range?.start_at) ||
510
+ !validIso(manifest.time_range?.end_at)) {
511
+ throw new Error("diagnostic manifest has an invalid timestamp");
512
+ }
513
+ if (!/^machine_[0-9a-f]{64}$/.test(manifest.machine_id))
514
+ throw new Error("diagnostic machine id is not pseudonymous");
515
+ if (!/^[0-9a-f]{64}$/.test(manifest.threshold_digest))
516
+ throw new Error("invalid threshold digest");
517
+ if (typeof manifest.engine_version !== "string" ||
518
+ !/^[0-9A-Za-z._+-]{1,100}$/.test(manifest.engine_version)) {
519
+ throw new Error("invalid diagnostic engine version");
520
+ }
521
+ assertSelection(manifest.time_range, "diagnostic manifest time range", false);
522
+ if (!Array.isArray(manifest.sources) ||
523
+ manifest.sources.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities) {
524
+ throw new Error("diagnostic manifest sources exceed the limit");
525
+ }
526
+ for (const source of manifest.sources) {
527
+ assertKeys(source, ["source_kind", "capability", "entry_count", "omitted_count", "file"], ["schema_version"], "diagnostic manifest source");
528
+ if (!validSourceKind(source.source_kind) || !validCapabilityState(source.capability))
529
+ throw new Error("invalid diagnostic manifest source");
530
+ if (source.schema_version !== undefined &&
531
+ (!Number.isInteger(source.schema_version) || source.schema_version < 0))
532
+ throw new Error("invalid diagnostic source schema version");
533
+ if (!boundedInteger(source.entry_count, 0, 50_000) ||
534
+ !boundedInteger(source.omitted_count, 0, 50_000) ||
535
+ source.file !== "inputs/observations.json")
536
+ throw new Error("invalid diagnostic manifest source counts");
537
+ }
538
+ validateCapabilities(manifest.capabilities, "diagnostic manifest capabilities");
539
+ if (!Array.isArray(manifest.files) || manifest.files.length !== PAYLOAD_FILES.length)
540
+ throw new Error("diagnostic manifest file list is incomplete");
541
+ const listed = manifest.files.map((file) => file.path).sort();
542
+ if (listed.join("\0") !== [...PAYLOAD_FILES].sort().join("\0"))
543
+ throw new Error("diagnostic manifest file list is not exact");
544
+ for (const file of manifest.files) {
545
+ assertKeys(file, ["path", "media_type", "bytes", "sha256"], [], "diagnostic file descriptor");
546
+ if (file.media_type !== "application/json" ||
547
+ !Number.isInteger(file.bytes) ||
548
+ file.bytes < 1 ||
549
+ !/^[0-9a-f]{64}$/.test(file.sha256)) {
550
+ throw new Error(`invalid diagnostic file descriptor for ${String(file.path)}`);
551
+ }
552
+ }
553
+ assertKeys(manifest.replay, ["input_file", "thresholds_file", "expected_file"], [], "diagnostic replay contract");
554
+ if (manifest.replay?.input_file !== "inputs/observations.json" ||
555
+ manifest.replay?.thresholds_file !== "inputs/thresholds.json" ||
556
+ manifest.replay?.expected_file !== "expected.json") {
557
+ throw new Error("diagnostic replay file contract is invalid");
558
+ }
559
+ }
560
+ function validatePayloads(manifest, observations, thresholds, expected, summary) {
561
+ assertKeys(observations, ["schema_version", "captured_at", "selection", "sources"], [], "diagnostic observations");
562
+ assertKeys(thresholds, ["schema_version", "values"], [], "diagnostic thresholds");
563
+ assertKeys(expected, [
564
+ "schema_version",
565
+ "threshold_digest",
566
+ "selection",
567
+ "findings",
568
+ "timelines",
569
+ "explanations",
570
+ "advice",
571
+ ], [], "diagnostic expected output");
572
+ assertKeys(summary, [
573
+ "schema_version",
574
+ "artifact_id",
575
+ "captured_at",
576
+ "machine_id",
577
+ "machine_id_kind",
578
+ "selection",
579
+ "source_count",
580
+ "supported_source_count",
581
+ "sanitized_value_count",
582
+ "omitted_value_count",
583
+ "total_bytes",
584
+ ], [], "diagnostic summary");
585
+ if (observations.schema_version !== DIAGNOSTIC_INPUT_SCHEMA_VERSION ||
586
+ thresholds.schema_version !== DIAGNOSTIC_INPUT_SCHEMA_VERSION) {
587
+ throw new Error("unsupported diagnostic input schema");
588
+ }
589
+ if (!Array.isArray(observations.sources) || !validIso(observations.captured_at))
590
+ throw new Error("invalid diagnostic observations");
591
+ if (observations.sources.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities)
592
+ throw new Error("diagnostic observations exceed the source limit");
593
+ assertSelection(observations.selection, "diagnostic observation selection", true);
594
+ for (const source of observations.sources) {
595
+ assertKeys(source, ["source_kind", "capability"], ["schema_version", "observed_at", "value", "reason_code"], "diagnostic captured source");
596
+ if (!validSourceKind(source.source_kind) || !validCapabilityState(source.capability))
597
+ throw new Error("invalid diagnostic captured source");
598
+ if (source.schema_version !== undefined &&
599
+ (!Number.isInteger(source.schema_version) || source.schema_version < 0))
600
+ throw new Error("invalid captured source schema version");
601
+ if (source.observed_at !== undefined && !validIso(source.observed_at))
602
+ throw new Error("invalid captured source timestamp");
603
+ if (source.reason_code !== undefined &&
604
+ (typeof source.reason_code !== "string" || source.reason_code.length > 240))
605
+ throw new Error("invalid captured source reason code");
606
+ }
607
+ if (!thresholds.values ||
608
+ typeof thresholds.values !== "object" ||
609
+ Array.isArray(thresholds.values))
610
+ throw new Error("invalid diagnostic threshold values");
611
+ if (expected.schema_version !== DIAGNOSTIC_EXPECTED_SCHEMA_VERSION)
612
+ throw new Error("unsupported diagnostic expected schema");
613
+ assertSelection(expected.selection, "diagnostic expected selection", true);
614
+ if (canonicalJson(expected.selection) !== canonicalJson(observations.selection))
615
+ throw new Error("diagnostic expected selection does not match inputs");
616
+ validateExpectedRecords(expected);
617
+ if (expected.threshold_digest !== sha256(canonicalJson(thresholds)) ||
618
+ expected.threshold_digest !== manifest.threshold_digest) {
619
+ throw new Error("captured threshold digest does not match manifest");
620
+ }
621
+ if (summary.schema_version !== DIAGNOSTIC_SUMMARY_SCHEMA_VERSION ||
622
+ summary.artifact_id !== manifest.artifact_id ||
623
+ summary.machine_id !== manifest.machine_id ||
624
+ summary.machine_id_kind !== "pseudonymous") {
625
+ throw new Error("invalid diagnostic summary");
626
+ }
627
+ assertSelection(summary.selection, "diagnostic summary selection", true);
628
+ for (const count of [
629
+ summary.source_count,
630
+ summary.supported_source_count,
631
+ summary.sanitized_value_count,
632
+ summary.omitted_value_count,
633
+ summary.total_bytes,
634
+ ]) {
635
+ if (!boundedInteger(count, 0, SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes))
636
+ throw new Error("invalid diagnostic summary count");
637
+ }
638
+ if (summary.source_count !== observations.sources.length ||
639
+ summary.supported_source_count > summary.source_count)
640
+ throw new Error("diagnostic summary source counts do not match inputs");
641
+ }
642
+ function validateExpectedRecords(expected) {
643
+ if (!Array.isArray(expected.findings) ||
644
+ expected.findings.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings)
645
+ throw new Error("diagnostic expected findings exceed the limit");
646
+ if (!Array.isArray(expected.timelines) ||
647
+ expected.timelines.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings)
648
+ throw new Error("diagnostic expected timelines exceed the limit");
649
+ if (!Array.isArray(expected.explanations) ||
650
+ expected.explanations.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings)
651
+ throw new Error("diagnostic expected explanations exceed the limit");
652
+ for (const finding of expected.findings) {
653
+ if (!finding ||
654
+ typeof finding !== "object" ||
655
+ finding.schema_version !== SUPERVISOR_FINDING_SCHEMA_VERSION)
656
+ throw new Error("invalid diagnostic expected finding schema");
657
+ if (!Array.isArray(finding.evidence) ||
658
+ finding.evidence.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_evidence_per_finding)
659
+ throw new Error("diagnostic finding evidence exceeds the limit");
660
+ validateCapabilities(finding.capabilities, "diagnostic finding capabilities");
661
+ }
662
+ for (const timeline of expected.timelines) {
663
+ if (!timeline ||
664
+ typeof timeline !== "object" ||
665
+ timeline.schema_version !== SUPERVISOR_TIMELINE_SCHEMA_VERSION)
666
+ throw new Error("invalid diagnostic timeline schema");
667
+ if (!Array.isArray(timeline.entries) ||
668
+ timeline.entries.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_timeline_entries)
669
+ throw new Error("diagnostic timeline entries exceed the limit");
670
+ validateCapabilities(timeline.capabilities, "diagnostic timeline capabilities");
671
+ if (!boundedInteger(timeline.compacted_entries, 0, 50_000))
672
+ throw new Error("invalid diagnostic timeline compaction count");
673
+ for (const entry of timeline.entries) {
674
+ if (!entry ||
675
+ !validIso(entry.first_occurred_at) ||
676
+ !validIso(entry.last_occurred_at) ||
677
+ !boundedInteger(entry.occurrence_count, 1, 50_000) ||
678
+ Date.parse(entry.first_occurred_at) > Date.parse(entry.last_occurred_at)) {
679
+ throw new Error("invalid diagnostic timeline entry cluster");
680
+ }
681
+ }
682
+ }
683
+ for (const explanation of expected.explanations) {
684
+ if (!explanation ||
685
+ typeof explanation !== "object" ||
686
+ explanation.schema_version !== SUPERVISOR_EXPLANATION_SCHEMA_VERSION)
687
+ throw new Error("invalid diagnostic explanation schema");
688
+ for (const rows of [
689
+ explanation.observed,
690
+ explanation.related,
691
+ explanation.possible,
692
+ explanation.missing_capabilities,
693
+ ]) {
694
+ if (!Array.isArray(rows) || rows.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_explanation_items)
695
+ throw new Error("diagnostic explanation entries exceed the limit");
696
+ }
697
+ }
698
+ validateAdvice(expected.advice);
699
+ }
700
+ function validateAdvice(advice) {
701
+ assertKeys(advice, [
702
+ "schema_version",
703
+ "evaluated_at",
704
+ "pressure",
705
+ "fan_out_recommendation",
706
+ "observer_only",
707
+ "summary",
708
+ "source_capability",
709
+ "active_finding_count",
710
+ "contributing_finding_count",
711
+ "omitted_contributing_finding_count",
712
+ "contributing_findings",
713
+ "reasons",
714
+ ], [], "diagnostic advice");
715
+ if (advice.schema_version !== DIAGNOSTIC_ADVICE_SCHEMA_VERSION ||
716
+ !validIso(advice.evaluated_at) ||
717
+ !["normal", "elevated", "critical", "unknown"].includes(advice.pressure) ||
718
+ !["proceed", "use-caution", "avoid-new-fan-out", "unknown"].includes(advice.fan_out_recommendation) ||
719
+ advice.observer_only !== true ||
720
+ typeof advice.summary !== "string" ||
721
+ advice.summary.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_summary_chars) {
722
+ throw new Error("invalid diagnostic advice contract");
723
+ }
724
+ validateCapabilities([advice.source_capability], "diagnostic advice capability");
725
+ for (const count of [
726
+ advice.active_finding_count,
727
+ advice.contributing_finding_count,
728
+ advice.omitted_contributing_finding_count,
729
+ ]) {
730
+ if (!boundedInteger(count, 0, SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings))
731
+ throw new Error("invalid diagnostic advice count");
732
+ }
733
+ if (!Array.isArray(advice.contributing_findings) ||
734
+ advice.contributing_findings.length > DIAGNOSTIC_ADVICE_LIMITS.max_contributing_findings ||
735
+ !Array.isArray(advice.reasons) ||
736
+ advice.reasons.length > DIAGNOSTIC_ADVICE_LIMITS.max_reasons) {
737
+ throw new Error("diagnostic advice entries exceed the limit");
738
+ }
739
+ for (const finding of advice.contributing_findings) {
740
+ assertKeys(finding, [
741
+ "finding_id",
742
+ "finding_kind",
743
+ "severity",
744
+ "summary",
745
+ "scope_kind",
746
+ "scope_id",
747
+ "occurrence_count",
748
+ ], ["owner_kind", "owner_id", "workload_relationship"], "diagnostic advice finding");
749
+ if (!["warning", "critical"].includes(finding.severity) ||
750
+ typeof finding.finding_id !== "string" ||
751
+ typeof finding.summary !== "string" ||
752
+ !boundedInteger(finding.occurrence_count, 1, 50_000)) {
753
+ throw new Error("invalid diagnostic advice finding");
754
+ }
755
+ }
756
+ for (const reason of advice.reasons) {
757
+ assertKeys(reason, ["code", "summary", "finding_ids"], [], "diagnostic advice reason");
758
+ if (![
759
+ "critical_findings_active",
760
+ "warning_findings_active",
761
+ "findings_source_unavailable",
762
+ "no_active_pressure_findings",
763
+ ].includes(reason.code) ||
764
+ typeof reason.summary !== "string" ||
765
+ !Array.isArray(reason.finding_ids) ||
766
+ reason.finding_ids.length > DIAGNOSTIC_ADVICE_LIMITS.max_contributing_findings ||
767
+ reason.finding_ids.some((id) => typeof id !== "string")) {
768
+ throw new Error("invalid diagnostic advice reason");
769
+ }
770
+ }
771
+ }
772
+ function validateCapabilities(value, label) {
773
+ if (!Array.isArray(value) || value.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities)
774
+ throw new Error(`${label} exceed the limit`);
775
+ for (const capability of value) {
776
+ if (!capability || typeof capability !== "object" || Array.isArray(capability))
777
+ throw new Error(`invalid ${label}`);
778
+ assertKeys(capability, ["source_kind", "state"], ["reason_code", "detail"], label);
779
+ const record = capability;
780
+ if (!validSourceKind(record.source_kind) || !validCapabilityState(record.state))
781
+ throw new Error(`invalid ${label}`);
782
+ if (record.reason_code !== undefined &&
783
+ (typeof record.reason_code !== "string" || record.reason_code.length > 240))
784
+ throw new Error(`invalid ${label}`);
785
+ if (record.detail !== undefined &&
786
+ (typeof record.detail !== "string" || record.detail.length > 2_000))
787
+ throw new Error(`invalid ${label}`);
788
+ }
789
+ }
790
+ function assertSelection(value, label, findingAllowed) {
791
+ if (!value || typeof value !== "object" || Array.isArray(value))
792
+ throw new Error(`invalid ${label}`);
793
+ const record = value;
794
+ assertKeys(record, ["start_at", "end_at"], findingAllowed ? ["finding_id"] : [], label);
795
+ if (!validIso(record.start_at) ||
796
+ !validIso(record.end_at) ||
797
+ Date.parse(record.start_at) > Date.parse(record.end_at))
798
+ throw new Error(`invalid ${label}`);
799
+ if (record.finding_id !== undefined &&
800
+ (typeof record.finding_id !== "string" || !/^[A-Za-z0-9_.:-]{1,240}$/.test(record.finding_id)))
801
+ throw new Error(`invalid ${label}`);
802
+ }
803
+ function assertKeys(value, required, optional, label) {
804
+ const keys = Object.keys(value);
805
+ const allowed = new Set([...required, ...optional]);
806
+ const missing = required.filter((key) => !keys.includes(key));
807
+ const unknown = keys.filter((key) => !allowed.has(key));
808
+ if (missing.length > 0 || unknown.length > 0) {
809
+ throw new Error(`${label} keys are invalid`);
810
+ }
811
+ }
812
+ function validCapabilityState(value) {
813
+ return ["supported", "partial", "unsupported", "error"].includes(String(value));
814
+ }
815
+ function validSourceKind(value) {
816
+ return typeof value === "string" && /^[a-z][a-z0-9_.:-]{0,119}$/.test(value);
817
+ }
818
+ function boundedInteger(value, minimum, maximum) {
819
+ return (typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum);
820
+ }
821
+ function countEntries(value) {
822
+ if (Array.isArray(value))
823
+ return value.length;
824
+ const record = object(value);
825
+ if (!record)
826
+ return value === undefined ? 0 : 1;
827
+ for (const key of ["active", "transitions", "points", "lanes", "rows"]) {
828
+ if (Array.isArray(record[key]))
829
+ return record[key].length;
830
+ }
831
+ return 1;
832
+ }
833
+ function timestamp(record) {
834
+ if (!record)
835
+ return undefined;
836
+ for (const key of ["captured_at", "sampled_at", "generated_at", "heartbeat_at"]) {
837
+ if (validIso(record[key]))
838
+ return record[key];
839
+ }
840
+ return undefined;
841
+ }
842
+ function numeric(value) {
843
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
844
+ }
845
+ function arrayObjects(value) {
846
+ return Array.isArray(value)
847
+ ? value.filter((item) => !!item && typeof item === "object" && !Array.isArray(item))
848
+ : [];
849
+ }
850
+ function object(value) {
851
+ return value && typeof value === "object" && !Array.isArray(value)
852
+ ? value
853
+ : undefined;
854
+ }
855
+ function parseIso(value, label) {
856
+ const date = new Date(value);
857
+ assertValidDate(date, label);
858
+ return date;
859
+ }
860
+ function validIso(value) {
861
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
862
+ }
863
+ function assertValidDate(value, label) {
864
+ if (!Number.isFinite(value.getTime()))
865
+ throw new Error(`${label} must be a valid timestamp`);
866
+ }
867
+ function tryChmod(path, mode) {
868
+ try {
869
+ chmodSync(path, mode);
870
+ }
871
+ catch {
872
+ // Windows does not expose POSIX modes. The private atomic writer still applies.
873
+ }
874
+ }