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,1142 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ } from "node:fs";
10
+ import { dirname, join, resolve, sep } from "node:path";
11
+ import type { ArtifactActor, ArtifactInventoryEntry } from "../artifacts/index.ts";
12
+ import {
13
+ ARTIFACT_MANIFEST,
14
+ artifactsRoot,
15
+ configuredArtifactRetentionDays,
16
+ createArtifact,
17
+ inventoryArtifacts,
18
+ releaseArtifact,
19
+ resolveArtifactRef,
20
+ showArtifact,
21
+ } from "../artifacts/index.ts";
22
+ import { currentHarneryRuntimeBuild } from "../events/v3/runtime-build.ts";
23
+ import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
24
+ import {
25
+ SUPERVISOR_DIAGNOSTIC_LIMITS,
26
+ SUPERVISOR_EXPLANATION_SCHEMA_VERSION,
27
+ SUPERVISOR_FINDING_POLICY,
28
+ SUPERVISOR_FINDING_SCHEMA_VERSION,
29
+ SUPERVISOR_RESOURCE_BUDGET,
30
+ SUPERVISOR_TIMELINE_SCHEMA_VERSION,
31
+ type SupervisorCapability,
32
+ type SupervisorFinding,
33
+ } from "../supervisor/contract.ts";
34
+ import { readWorkflowProof } from "../workflow/proof.ts";
35
+ import {
36
+ DIAGNOSTIC_ADVICE_LIMITS,
37
+ DIAGNOSTIC_ADVICE_SCHEMA_VERSION,
38
+ DIAGNOSTIC_BUNDLE_FILES,
39
+ DIAGNOSTIC_BUNDLE_SCHEMA_VERSION,
40
+ DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
41
+ DIAGNOSTIC_EXPECTED_SCHEMA_VERSION,
42
+ DIAGNOSTIC_INPUT_SCHEMA_VERSION,
43
+ DIAGNOSTIC_SUMMARY_SCHEMA_VERSION,
44
+ type DiagnosticBundleFile,
45
+ type DiagnosticBundleManifest,
46
+ type DiagnosticCapturedSource,
47
+ type DiagnosticObservations,
48
+ type DiagnosticReplayResult,
49
+ type DiagnosticSelection,
50
+ type DiagnosticSummary,
51
+ type DiagnosticThresholds,
52
+ type ValidatedDiagnosticBundle,
53
+ } from "./contract.ts";
54
+ import { pseudonymousMachineId } from "./identity.ts";
55
+ import { canonicalJson, deriveCapturedExpected, replayDiagnosticInputs, sha256 } from "./replay.ts";
56
+ import { type SanitizationStats, sanitizeDiagnosticValue } from "./sanitize.ts";
57
+
58
+ const SOURCE_FILE_LIMIT_BYTES = 1_500_000;
59
+ const DEFAULT_WINDOW_MS = 15 * 60 * 1_000;
60
+ const SHADOW_ADMISSION_CAPTURE_LIMIT = 20;
61
+ const PAYLOAD_FILES = DIAGNOSTIC_BUNDLE_FILES.filter(
62
+ (path): path is Exclude<(typeof DIAGNOSTIC_BUNDLE_FILES)[number], "diagnostic-manifest.json"> =>
63
+ path !== "diagnostic-manifest.json",
64
+ );
65
+
66
+ const SOURCE_SPECS = [
67
+ ["supervisor.snapshot", ".harnery/supervisor/snapshot.json"],
68
+ ["supervisor.history", ".harnery/supervisor/history.json"],
69
+ ["supervisor.findings", ".harnery/supervisor/findings.json"],
70
+ ["supervisor.activity", ".harnery/supervisor/activity.json"],
71
+ ["supervisor.timelines", ".harnery/supervisor/timelines"],
72
+ ["supervisor.explanations", ".harnery/supervisor/explanations"],
73
+ ["supervisor.log-feed", ".harnery/supervisor/log-feed.json"],
74
+ ["supervisor.hook-health", ".harnery/supervisor/hook-health.json"],
75
+ ["resources.snapshot", ".harnery/resources/snapshot.json"],
76
+ ["coordination.health", ".harnery/supervisor/coordination-health.json"],
77
+ ] as const;
78
+
79
+ export interface CaptureDiagnosticBundleInput {
80
+ findingId?: string;
81
+ startAt?: string;
82
+ endAt?: string;
83
+ actor?: ArtifactActor;
84
+ retentionDays?: number;
85
+ now?: Date;
86
+ machineLabel?: string;
87
+ engineVersion?: string;
88
+ }
89
+
90
+ export interface CapturedDiagnosticBundle {
91
+ path: string;
92
+ manifest: DiagnosticBundleManifest;
93
+ summary: DiagnosticSummary;
94
+ }
95
+
96
+ export interface DiagnosticBundleListRow {
97
+ artifact_id: string;
98
+ created_at: string;
99
+ expires_at: string | null;
100
+ classification: ArtifactInventoryEntry["classification"];
101
+ valid: boolean;
102
+ captured_at?: string;
103
+ finding_id?: string;
104
+ error?: string;
105
+ }
106
+
107
+ export function captureDiagnosticBundle(
108
+ repoRootRaw: string,
109
+ input: CaptureDiagnosticBundleInput = {},
110
+ ): CapturedDiagnosticBundle {
111
+ const repoRoot = resolve(repoRootRaw);
112
+ const now = input.now ?? new Date();
113
+ assertValidDate(now, "now");
114
+ const capturedAt = now.toISOString();
115
+ const aggregateStats: SanitizationStats = { sanitized_value_count: 0, omitted_value_count: 0 };
116
+ const sources = [
117
+ ...SOURCE_SPECS.map(([sourceKind, relativePath]) =>
118
+ readCapturedSource(repoRoot, sourceKind, relativePath, aggregateStats),
119
+ ),
120
+ readShadowAdmissionSource(repoRoot, capturedAt, aggregateStats),
121
+ ];
122
+ const selection = diagnosticSelection(input, sources, now);
123
+ const observations: DiagnosticObservations = {
124
+ schema_version: DIAGNOSTIC_INPUT_SCHEMA_VERSION,
125
+ captured_at: capturedAt,
126
+ selection,
127
+ sources,
128
+ };
129
+ const thresholds: DiagnosticThresholds = {
130
+ schema_version: DIAGNOSTIC_INPUT_SCHEMA_VERSION,
131
+ values: {
132
+ diagnostic_limits: SUPERVISOR_DIAGNOSTIC_LIMITS,
133
+ finding_policy: SUPERVISOR_FINDING_POLICY,
134
+ resource_budget: SUPERVISOR_RESOURCE_BUDGET,
135
+ },
136
+ };
137
+ const expected = deriveCapturedExpected(observations, thresholds);
138
+ if (input.findingId && !expected.findings.some((finding) => finding.id === input.findingId)) {
139
+ throw new Error(
140
+ `finding "${input.findingId}" is not present in the captured findings projection`,
141
+ );
142
+ }
143
+
144
+ const created = createArtifact(repoRoot, {
145
+ slug: "diagnostic-bundle",
146
+ purpose: input.findingId
147
+ ? `Frozen diagnostics for finding ${input.findingId}`
148
+ : `Frozen diagnostics from ${selection.start_at} to ${selection.end_at}`,
149
+ retentionDays: input.retentionDays ?? configuredArtifactRetentionDays(repoRoot),
150
+ actor: input.actor,
151
+ now,
152
+ });
153
+ const inputsPath = join(created.path, "inputs");
154
+ mkdirSync(inputsPath, { recursive: true, mode: 0o700 });
155
+ tryChmod(inputsPath, 0o700);
156
+
157
+ const payloads: Record<(typeof PAYLOAD_FILES)[number], unknown> = {
158
+ "inputs/observations.json": observations,
159
+ "inputs/thresholds.json": thresholds,
160
+ "expected.json": expected,
161
+ "summary.json": {},
162
+ };
163
+ const summaryBase: Omit<DiagnosticSummary, "total_bytes"> = {
164
+ schema_version: DIAGNOSTIC_SUMMARY_SCHEMA_VERSION,
165
+ artifact_id: created.manifest.artifact_id,
166
+ captured_at: capturedAt,
167
+ machine_id: pseudonymousMachineId(input.machineLabel),
168
+ machine_id_kind: "pseudonymous",
169
+ selection,
170
+ source_count: sources.length,
171
+ supported_source_count: sources.filter((source) => source.capability === "supported").length,
172
+ sanitized_value_count: aggregateStats.sanitized_value_count,
173
+ omitted_value_count: aggregateStats.omitted_value_count,
174
+ };
175
+ const summary = stableSummary(summaryBase, payloads);
176
+ payloads["summary.json"] = summary;
177
+
178
+ for (const path of PAYLOAD_FILES)
179
+ writePrivateJsonAtomic(join(created.path, path), payloads[path]);
180
+ const files = PAYLOAD_FILES.map((path) => describeFile(created.path, path));
181
+ const capabilities = sources.map(sourceCapability);
182
+ const manifest: DiagnosticBundleManifest = {
183
+ schema_version: DIAGNOSTIC_BUNDLE_SCHEMA_VERSION,
184
+ artifact_id: created.manifest.artifact_id,
185
+ captured_at: capturedAt,
186
+ machine_id: summary.machine_id,
187
+ finding_id: input.findingId,
188
+ time_range: { start_at: selection.start_at, end_at: selection.end_at },
189
+ engine_version: input.engineVersion ?? currentHarneryRuntimeBuild(),
190
+ threshold_digest: expected.threshold_digest,
191
+ sources: sources.map((source) => ({
192
+ source_kind: source.source_kind,
193
+ schema_version: source.schema_version,
194
+ capability: source.capability,
195
+ entry_count: countEntries(source.value),
196
+ omitted_count: source.value === undefined ? 1 : 0,
197
+ file: "inputs/observations.json",
198
+ })),
199
+ capabilities,
200
+ files,
201
+ replay: {
202
+ input_file: "inputs/observations.json",
203
+ thresholds_file: "inputs/thresholds.json",
204
+ expected_file: "expected.json",
205
+ },
206
+ };
207
+ writePrivateJsonAtomic(join(created.path, "diagnostic-manifest.json"), manifest);
208
+ validateDiagnosticBundle(repoRoot, created.manifest.artifact_id);
209
+ releaseArtifact(repoRoot, created.manifest.artifact_id, { actor: input.actor, now });
210
+ return { path: created.path, manifest, summary };
211
+ }
212
+
213
+ export function validateDiagnosticBundle(
214
+ repoRootRaw: string,
215
+ ref: string,
216
+ ): ValidatedDiagnosticBundle {
217
+ const repoRoot = resolve(repoRootRaw);
218
+ const artifactPath = resolveArtifactRef(repoRoot, ref);
219
+ const managedRoot = realpathSync(artifactsRoot(repoRoot));
220
+ const resolvedArtifact = realpathSync(artifactPath);
221
+ if (
222
+ dirname(resolvedArtifact) !== managedRoot ||
223
+ !resolvedArtifact.startsWith(`${managedRoot}${sep}`)
224
+ ) {
225
+ throw new Error("diagnostic bundle is outside the managed artifact root");
226
+ }
227
+ assertBundleTree(artifactPath);
228
+ const shown = showArtifact(repoRoot, artifactPath);
229
+ if (
230
+ !["managed-active", "managed-current", "managed-expired", "managed-tracked"].includes(
231
+ shown.entry.classification,
232
+ )
233
+ ) {
234
+ throw new Error(
235
+ `diagnostic bundle has invalid managed classification ${shown.entry.classification}`,
236
+ );
237
+ }
238
+ if (shown.manifest.slug !== "diagnostic-bundle") {
239
+ throw new Error("artifact is not a diagnostic bundle");
240
+ }
241
+ const manifest = readJson<DiagnosticBundleManifest>(artifactPath, "diagnostic-manifest.json");
242
+ validateManifest(manifest, shown.manifest.artifact_id);
243
+ let totalBytes = 0;
244
+ for (const file of manifest.files) {
245
+ const bytes = readValidatedFile(artifactPath, file.path);
246
+ totalBytes += bytes.byteLength;
247
+ if (bytes.byteLength !== file.bytes) throw new Error(`byte length mismatch for ${file.path}`);
248
+ if (sha256(bytes) !== file.sha256) throw new Error(`digest mismatch for ${file.path}`);
249
+ }
250
+ if (totalBytes > SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes) {
251
+ throw new Error("diagnostic bundle exceeds the maximum byte limit");
252
+ }
253
+
254
+ const observations = readJson<DiagnosticObservations>(artifactPath, manifest.replay.input_file);
255
+ const thresholds = readJson<DiagnosticThresholds>(artifactPath, manifest.replay.thresholds_file);
256
+ const expected = readJson<ValidatedDiagnosticBundle["expected"]>(
257
+ artifactPath,
258
+ manifest.replay.expected_file,
259
+ );
260
+ const summary = readJson<DiagnosticSummary>(artifactPath, "summary.json");
261
+ validatePayloads(manifest, observations, thresholds, expected, summary);
262
+ return { path: artifactPath, manifest, observations, thresholds, expected, summary };
263
+ }
264
+
265
+ /** Frozen web readers accept an opaque artifact id, never a filesystem path. */
266
+ export function readFrozenDiagnosticBundle(
267
+ repoRoot: string,
268
+ artifactId: string,
269
+ ): ValidatedDiagnosticBundle {
270
+ if (!/^[A-Za-z0-9_-]{1,160}$/.test(artifactId)) {
271
+ throw new Error("diagnostic bundle id must be an opaque artifact id");
272
+ }
273
+ return validateDiagnosticBundle(repoRoot, artifactId);
274
+ }
275
+
276
+ export function showDiagnosticBundle(repoRoot: string, ref: string): ValidatedDiagnosticBundle {
277
+ return validateDiagnosticBundle(repoRoot, ref);
278
+ }
279
+
280
+ export function listDiagnosticBundles(repoRoot: string): DiagnosticBundleListRow[] {
281
+ return inventoryArtifacts(repoRoot)
282
+ .filter((entry) => entry.slug === "diagnostic-bundle" && entry.artifact_id)
283
+ .map((entry) => {
284
+ try {
285
+ const bundle = validateDiagnosticBundle(repoRoot, entry.artifact_id!);
286
+ return {
287
+ artifact_id: entry.artifact_id!,
288
+ created_at: entry.created_at ?? bundle.manifest.captured_at,
289
+ expires_at: entry.expires_at,
290
+ classification: entry.classification,
291
+ valid: true,
292
+ captured_at: bundle.manifest.captured_at,
293
+ finding_id: bundle.manifest.finding_id,
294
+ };
295
+ } catch (error) {
296
+ return {
297
+ artifact_id: entry.artifact_id!,
298
+ created_at: entry.created_at ?? "",
299
+ expires_at: entry.expires_at,
300
+ classification: entry.classification,
301
+ valid: false,
302
+ error: error instanceof Error ? error.message : String(error),
303
+ };
304
+ }
305
+ });
306
+ }
307
+
308
+ export function replayDiagnosticBundle(
309
+ repoRoot: string,
310
+ ref: string,
311
+ now = new Date(),
312
+ ): DiagnosticReplayResult {
313
+ assertValidDate(now, "now");
314
+ const bundle = validateDiagnosticBundle(repoRoot, ref);
315
+ const actual = replayDiagnosticInputs(bundle.observations, bundle.thresholds);
316
+ const expectedDigest = sha256(canonicalJson(bundle.expected));
317
+ const actualDigest = sha256(canonicalJson(actual));
318
+ return {
319
+ schema_version: DIAGNOSTIC_COMMAND_SCHEMA_VERSION,
320
+ artifact_id: bundle.manifest.artifact_id,
321
+ replayed_at: now.toISOString(),
322
+ matched: expectedDigest === actualDigest,
323
+ threshold_digest: actual.threshold_digest,
324
+ expected_digest: expectedDigest,
325
+ actual_digest: actualDigest,
326
+ finding_count: actual.findings.length,
327
+ };
328
+ }
329
+
330
+ function readCapturedSource(
331
+ repoRoot: string,
332
+ sourceKind: string,
333
+ relativePath: string,
334
+ aggregateStats: SanitizationStats,
335
+ ): DiagnosticCapturedSource {
336
+ const path = join(repoRoot, relativePath);
337
+ if (!existsSync(path)) {
338
+ return { source_kind: sourceKind, capability: "unsupported", reason_code: "source_missing" };
339
+ }
340
+ try {
341
+ const stat = lstatSync(path);
342
+ if (stat.isSymbolicLink()) throw new Error("source_is_symlink");
343
+ const parsed = stat.isDirectory()
344
+ ? readJsonDirectory(path)
345
+ : stat.isFile()
346
+ ? readBoundedJsonFile(path)
347
+ : (() => {
348
+ throw new Error("source_is_not_regular_file");
349
+ })();
350
+ const sanitized = sanitizeDiagnosticValue(parsed);
351
+ aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
352
+ aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
353
+ const value = sanitized.value;
354
+ const record = object(value);
355
+ return {
356
+ source_kind: sourceKind,
357
+ capability: "supported",
358
+ schema_version: numeric(record?.schema_version),
359
+ observed_at: timestamp(record),
360
+ value,
361
+ };
362
+ } catch (error) {
363
+ return {
364
+ source_kind: sourceKind,
365
+ capability: "error",
366
+ reason_code: error instanceof Error ? error.message : String(error),
367
+ };
368
+ }
369
+ }
370
+
371
+ function readShadowAdmissionSource(
372
+ repoRoot: string,
373
+ capturedAt: string,
374
+ aggregateStats: SanitizationStats,
375
+ ): DiagnosticCapturedSource {
376
+ const sourceKind = "workflow.diagnostic-admission";
377
+ const workflowsRoot = join(repoRoot, ".harnery", "workflows");
378
+ if (!existsSync(workflowsRoot)) {
379
+ return { source_kind: sourceKind, capability: "unsupported", reason_code: "source_missing" };
380
+ }
381
+ try {
382
+ const rootStat = lstatSync(workflowsRoot);
383
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
384
+ throw new Error("workflow_root_is_not_regular_directory");
385
+ }
386
+ const candidates = readdirSync(workflowsRoot, { withFileTypes: true })
387
+ .filter(
388
+ (entry) => entry.isDirectory() && /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(entry.name),
389
+ )
390
+ .map((entry) => entry.name)
391
+ .sort((left, right) => right.localeCompare(left));
392
+ const records: Record<string, unknown>[] = [];
393
+ let invalidCount = 0;
394
+ const scanLimit = SHADOW_ADMISSION_CAPTURE_LIMIT * 2;
395
+ for (const runId of candidates.slice(0, scanLimit)) {
396
+ try {
397
+ const proof = readWorkflowProof(repoRoot, runId);
398
+ const admission = proof.diagnostic_admission;
399
+ if (!admission) continue;
400
+ records.push({
401
+ run_id: proof.run.id,
402
+ ended_at: proof.run.ended_at,
403
+ state: admission.state,
404
+ action: admission.action,
405
+ ...(admission.reason_code ? { reason_code: admission.reason_code } : {}),
406
+ ...(admission.observation
407
+ ? {
408
+ pressure: admission.observation.advice.pressure,
409
+ fan_out_recommendation: admission.observation.advice.fan_out_recommendation,
410
+ freshness: admission.observation.freshness,
411
+ service_state: admission.observation.service_state,
412
+ wait_ms: admission.observation.wait_ms,
413
+ }
414
+ : {}),
415
+ });
416
+ if (records.length >= SHADOW_ADMISSION_CAPTURE_LIMIT) break;
417
+ } catch {
418
+ invalidCount += 1;
419
+ }
420
+ }
421
+ const omittedCount = Math.max(0, candidates.length - Math.min(candidates.length, scanLimit));
422
+ const projection = {
423
+ schema_version: 1,
424
+ captured_at: capturedAt,
425
+ max_records: SHADOW_ADMISSION_CAPTURE_LIMIT,
426
+ examined_run_count: Math.min(candidates.length, scanLimit),
427
+ omitted_run_count: omittedCount,
428
+ invalid_run_count: invalidCount,
429
+ records,
430
+ };
431
+ const sanitized = sanitizeDiagnosticValue(projection);
432
+ aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
433
+ aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
434
+ return {
435
+ source_kind: sourceKind,
436
+ capability: omittedCount > 0 || invalidCount > 0 ? "partial" : "supported",
437
+ schema_version: 1,
438
+ observed_at: records[0]?.ended_at as string | undefined,
439
+ value: sanitized.value,
440
+ ...(omittedCount > 0
441
+ ? { reason_code: "bounded_workflow_window_truncated" }
442
+ : invalidCount > 0
443
+ ? { reason_code: "invalid_workflow_proofs_ignored" }
444
+ : {}),
445
+ };
446
+ } catch (error) {
447
+ return {
448
+ source_kind: sourceKind,
449
+ capability: "error",
450
+ reason_code: error instanceof Error ? error.message : String(error),
451
+ };
452
+ }
453
+ }
454
+
455
+ function readBoundedJsonFile(path: string): unknown {
456
+ const stat = lstatSync(path);
457
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("source_is_not_regular_file");
458
+ if (stat.size > SOURCE_FILE_LIMIT_BYTES) throw new Error("source_exceeds_byte_limit");
459
+ return JSON.parse(readFileSync(path, "utf8")) as unknown;
460
+ }
461
+
462
+ function readJsonDirectory(path: string): unknown[] {
463
+ const values: unknown[] = [];
464
+ let totalBytes = 0;
465
+ const names = readdirSync(path).sort();
466
+ if (names.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings) {
467
+ throw new Error("source_directory_exceeds_entry_limit");
468
+ }
469
+ for (const name of names) {
470
+ if (!/^[A-Za-z0-9_.-]+\.json$/.test(name)) {
471
+ throw new Error("source_directory_contains_unexpected_entry");
472
+ }
473
+ const file = join(path, name);
474
+ const stat = lstatSync(file);
475
+ if (stat.isSymbolicLink() || !stat.isFile()) {
476
+ throw new Error("source_directory_contains_nonregular_file");
477
+ }
478
+ totalBytes += stat.size;
479
+ if (totalBytes > SOURCE_FILE_LIMIT_BYTES) throw new Error("source_exceeds_byte_limit");
480
+ values.push(readBoundedJsonFile(file));
481
+ }
482
+ return values;
483
+ }
484
+
485
+ function diagnosticSelection(
486
+ input: CaptureDiagnosticBundleInput,
487
+ sources: readonly DiagnosticCapturedSource[],
488
+ now: Date,
489
+ ): DiagnosticSelection {
490
+ if ((input.startAt === undefined) !== (input.endAt === undefined)) {
491
+ throw new Error("--from and --to must be supplied together");
492
+ }
493
+ if (input.findingId && input.startAt) {
494
+ throw new Error("--finding cannot be combined with --from and --to");
495
+ }
496
+ if (input.findingId) {
497
+ if (!/^[A-Za-z0-9_.:-]{1,240}$/.test(input.findingId)) throw new Error("invalid finding id");
498
+ const finding = allFindings(sources).find((candidate) => candidate.id === input.findingId);
499
+ if (!finding) {
500
+ return {
501
+ finding_id: input.findingId,
502
+ start_at: now.toISOString(),
503
+ end_at: now.toISOString(),
504
+ };
505
+ }
506
+ return {
507
+ finding_id: input.findingId,
508
+ start_at: finding.opened_at,
509
+ end_at: finding.resolved_at ?? finding.observed_at,
510
+ };
511
+ }
512
+ const start = input.startAt
513
+ ? parseIso(input.startAt, "--from")
514
+ : new Date(now.getTime() - DEFAULT_WINDOW_MS);
515
+ const end = input.endAt ? parseIso(input.endAt, "--to") : now;
516
+ if (start.getTime() > end.getTime()) throw new Error("--from must not follow --to");
517
+ return { start_at: start.toISOString(), end_at: end.toISOString() };
518
+ }
519
+
520
+ function allFindings(sources: readonly DiagnosticCapturedSource[]): SupervisorFinding[] {
521
+ const value = sources.find((source) => source.source_kind === "supervisor.findings")?.value;
522
+ const record = object(value);
523
+ return [
524
+ ...arrayObjects(record?.active),
525
+ ...arrayObjects(record?.transitions),
526
+ ] as unknown as SupervisorFinding[];
527
+ }
528
+
529
+ function sourceCapability(source: DiagnosticCapturedSource): SupervisorCapability {
530
+ return {
531
+ source_kind: source.source_kind,
532
+ state: source.capability,
533
+ reason_code: source.reason_code,
534
+ };
535
+ }
536
+
537
+ function stableSummary(
538
+ base: Omit<DiagnosticSummary, "total_bytes">,
539
+ payloads: Record<(typeof PAYLOAD_FILES)[number], unknown>,
540
+ ): DiagnosticSummary {
541
+ let summary: DiagnosticSummary = { ...base, total_bytes: 0 };
542
+ for (let attempt = 0; attempt < 8; attempt += 1) {
543
+ const total = PAYLOAD_FILES.reduce((sum, path) => {
544
+ const value = path === "summary.json" ? summary : payloads[path];
545
+ return sum + Buffer.byteLength(`${JSON.stringify(value)}\n`);
546
+ }, 0);
547
+ if (total === summary.total_bytes) return summary;
548
+ summary = { ...base, total_bytes: total };
549
+ }
550
+ return summary;
551
+ }
552
+
553
+ function describeFile(root: string, path: (typeof PAYLOAD_FILES)[number]): DiagnosticBundleFile {
554
+ const bytes = readValidatedFile(root, path);
555
+ return { path, media_type: "application/json", bytes: bytes.byteLength, sha256: sha256(bytes) };
556
+ }
557
+
558
+ function assertBundleTree(root: string): void {
559
+ const rootStat = lstatSync(root);
560
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
561
+ throw new Error("diagnostic bundle root must be a regular directory");
562
+ }
563
+ const top = readdirSync(root).sort();
564
+ const expectedTop = [
565
+ ARTIFACT_MANIFEST,
566
+ "diagnostic-manifest.json",
567
+ "expected.json",
568
+ "inputs",
569
+ "summary.json",
570
+ ].sort();
571
+ if (top.join("\0") !== expectedTop.join("\0"))
572
+ throw new Error("diagnostic bundle file set is not exact");
573
+ const inputs = join(root, "inputs");
574
+ const inputsStat = lstatSync(inputs);
575
+ if (inputsStat.isSymbolicLink() || !inputsStat.isDirectory()) {
576
+ throw new Error("diagnostic bundle inputs must be a regular directory");
577
+ }
578
+ const inputFiles = readdirSync(inputs).sort();
579
+ if (inputFiles.join("\0") !== ["observations.json", "thresholds.json"].join("\0")) {
580
+ throw new Error("diagnostic bundle input file set is not exact");
581
+ }
582
+ for (const path of [...PAYLOAD_FILES, "diagnostic-manifest.json"] as const) {
583
+ readValidatedFile(root, path);
584
+ }
585
+ }
586
+
587
+ function readValidatedFile(rootRaw: string, relativePath: string): Buffer {
588
+ if (
589
+ !PAYLOAD_FILES.includes(relativePath as never) &&
590
+ relativePath !== "diagnostic-manifest.json"
591
+ ) {
592
+ throw new Error(`unexpected diagnostic bundle path ${relativePath}`);
593
+ }
594
+ const root = realpathSync(resolve(rootRaw));
595
+ const path = resolve(root, relativePath);
596
+ if (!path.startsWith(`${root}${sep}`))
597
+ throw new Error(`path escapes diagnostic bundle: ${relativePath}`);
598
+ const stat = lstatSync(path);
599
+ if (stat.isSymbolicLink() || !stat.isFile())
600
+ throw new Error(`${relativePath} is not a regular file`);
601
+ const real = realpathSync(path);
602
+ if (!real.startsWith(`${root}${sep}`))
603
+ throw new Error(`path escapes diagnostic bundle: ${relativePath}`);
604
+ if (stat.size > SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes) {
605
+ throw new Error(`${relativePath} exceeds the maximum byte limit`);
606
+ }
607
+ return readFileSync(real);
608
+ }
609
+
610
+ function readJson<T>(root: string, relativePath: string): T {
611
+ const bytes = readValidatedFile(root, relativePath);
612
+ try {
613
+ const value = JSON.parse(bytes.toString("utf8")) as unknown;
614
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
615
+ throw new Error("root must be an object");
616
+ }
617
+ return value as T;
618
+ } catch (error) {
619
+ throw new Error(
620
+ `${relativePath} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
621
+ );
622
+ }
623
+ }
624
+
625
+ function validateManifest(manifest: DiagnosticBundleManifest, artifactId: string): void {
626
+ assertKeys(
627
+ manifest,
628
+ [
629
+ "schema_version",
630
+ "artifact_id",
631
+ "captured_at",
632
+ "machine_id",
633
+ "time_range",
634
+ "engine_version",
635
+ "threshold_digest",
636
+ "sources",
637
+ "capabilities",
638
+ "files",
639
+ "replay",
640
+ ],
641
+ ["finding_id"],
642
+ "diagnostic manifest",
643
+ );
644
+ if (manifest.schema_version !== DIAGNOSTIC_BUNDLE_SCHEMA_VERSION)
645
+ throw new Error("unsupported diagnostic manifest schema");
646
+ if (manifest.artifact_id !== artifactId)
647
+ throw new Error("diagnostic artifact id does not match managed artifact id");
648
+ if (
649
+ !validIso(manifest.captured_at) ||
650
+ !validIso(manifest.time_range?.start_at) ||
651
+ !validIso(manifest.time_range?.end_at)
652
+ ) {
653
+ throw new Error("diagnostic manifest has an invalid timestamp");
654
+ }
655
+ if (!/^machine_[0-9a-f]{64}$/.test(manifest.machine_id))
656
+ throw new Error("diagnostic machine id is not pseudonymous");
657
+ if (!/^[0-9a-f]{64}$/.test(manifest.threshold_digest))
658
+ throw new Error("invalid threshold digest");
659
+ if (
660
+ typeof manifest.engine_version !== "string" ||
661
+ !/^[0-9A-Za-z._+-]{1,100}$/.test(manifest.engine_version)
662
+ ) {
663
+ throw new Error("invalid diagnostic engine version");
664
+ }
665
+ assertSelection(manifest.time_range, "diagnostic manifest time range", false);
666
+ if (
667
+ !Array.isArray(manifest.sources) ||
668
+ manifest.sources.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities
669
+ ) {
670
+ throw new Error("diagnostic manifest sources exceed the limit");
671
+ }
672
+ for (const source of manifest.sources) {
673
+ assertKeys(
674
+ source,
675
+ ["source_kind", "capability", "entry_count", "omitted_count", "file"],
676
+ ["schema_version"],
677
+ "diagnostic manifest source",
678
+ );
679
+ if (!validSourceKind(source.source_kind) || !validCapabilityState(source.capability))
680
+ throw new Error("invalid diagnostic manifest source");
681
+ if (
682
+ source.schema_version !== undefined &&
683
+ (!Number.isInteger(source.schema_version) || source.schema_version < 0)
684
+ )
685
+ throw new Error("invalid diagnostic source schema version");
686
+ if (
687
+ !boundedInteger(source.entry_count, 0, 50_000) ||
688
+ !boundedInteger(source.omitted_count, 0, 50_000) ||
689
+ source.file !== "inputs/observations.json"
690
+ )
691
+ throw new Error("invalid diagnostic manifest source counts");
692
+ }
693
+ validateCapabilities(manifest.capabilities, "diagnostic manifest capabilities");
694
+ if (!Array.isArray(manifest.files) || manifest.files.length !== PAYLOAD_FILES.length)
695
+ throw new Error("diagnostic manifest file list is incomplete");
696
+ const listed = manifest.files.map((file) => file.path).sort();
697
+ if (listed.join("\0") !== [...PAYLOAD_FILES].sort().join("\0"))
698
+ throw new Error("diagnostic manifest file list is not exact");
699
+ for (const file of manifest.files) {
700
+ assertKeys(file, ["path", "media_type", "bytes", "sha256"], [], "diagnostic file descriptor");
701
+ if (
702
+ file.media_type !== "application/json" ||
703
+ !Number.isInteger(file.bytes) ||
704
+ file.bytes < 1 ||
705
+ !/^[0-9a-f]{64}$/.test(file.sha256)
706
+ ) {
707
+ throw new Error(`invalid diagnostic file descriptor for ${String(file.path)}`);
708
+ }
709
+ }
710
+ assertKeys(
711
+ manifest.replay,
712
+ ["input_file", "thresholds_file", "expected_file"],
713
+ [],
714
+ "diagnostic replay contract",
715
+ );
716
+ if (
717
+ manifest.replay?.input_file !== "inputs/observations.json" ||
718
+ manifest.replay?.thresholds_file !== "inputs/thresholds.json" ||
719
+ manifest.replay?.expected_file !== "expected.json"
720
+ ) {
721
+ throw new Error("diagnostic replay file contract is invalid");
722
+ }
723
+ }
724
+
725
+ function validatePayloads(
726
+ manifest: DiagnosticBundleManifest,
727
+ observations: DiagnosticObservations,
728
+ thresholds: DiagnosticThresholds,
729
+ expected: ValidatedDiagnosticBundle["expected"],
730
+ summary: DiagnosticSummary,
731
+ ): void {
732
+ assertKeys(
733
+ observations,
734
+ ["schema_version", "captured_at", "selection", "sources"],
735
+ [],
736
+ "diagnostic observations",
737
+ );
738
+ assertKeys(thresholds, ["schema_version", "values"], [], "diagnostic thresholds");
739
+ assertKeys(
740
+ expected,
741
+ [
742
+ "schema_version",
743
+ "threshold_digest",
744
+ "selection",
745
+ "findings",
746
+ "timelines",
747
+ "explanations",
748
+ "advice",
749
+ ],
750
+ [],
751
+ "diagnostic expected output",
752
+ );
753
+ assertKeys(
754
+ summary,
755
+ [
756
+ "schema_version",
757
+ "artifact_id",
758
+ "captured_at",
759
+ "machine_id",
760
+ "machine_id_kind",
761
+ "selection",
762
+ "source_count",
763
+ "supported_source_count",
764
+ "sanitized_value_count",
765
+ "omitted_value_count",
766
+ "total_bytes",
767
+ ],
768
+ [],
769
+ "diagnostic summary",
770
+ );
771
+ if (
772
+ observations.schema_version !== DIAGNOSTIC_INPUT_SCHEMA_VERSION ||
773
+ thresholds.schema_version !== DIAGNOSTIC_INPUT_SCHEMA_VERSION
774
+ ) {
775
+ throw new Error("unsupported diagnostic input schema");
776
+ }
777
+ if (!Array.isArray(observations.sources) || !validIso(observations.captured_at))
778
+ throw new Error("invalid diagnostic observations");
779
+ if (observations.sources.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities)
780
+ throw new Error("diagnostic observations exceed the source limit");
781
+ assertSelection(observations.selection, "diagnostic observation selection", true);
782
+ for (const source of observations.sources) {
783
+ assertKeys(
784
+ source,
785
+ ["source_kind", "capability"],
786
+ ["schema_version", "observed_at", "value", "reason_code"],
787
+ "diagnostic captured source",
788
+ );
789
+ if (!validSourceKind(source.source_kind) || !validCapabilityState(source.capability))
790
+ throw new Error("invalid diagnostic captured source");
791
+ if (
792
+ source.schema_version !== undefined &&
793
+ (!Number.isInteger(source.schema_version) || source.schema_version < 0)
794
+ )
795
+ throw new Error("invalid captured source schema version");
796
+ if (source.observed_at !== undefined && !validIso(source.observed_at))
797
+ throw new Error("invalid captured source timestamp");
798
+ if (
799
+ source.reason_code !== undefined &&
800
+ (typeof source.reason_code !== "string" || source.reason_code.length > 240)
801
+ )
802
+ throw new Error("invalid captured source reason code");
803
+ }
804
+ if (
805
+ !thresholds.values ||
806
+ typeof thresholds.values !== "object" ||
807
+ Array.isArray(thresholds.values)
808
+ )
809
+ throw new Error("invalid diagnostic threshold values");
810
+ if (expected.schema_version !== DIAGNOSTIC_EXPECTED_SCHEMA_VERSION)
811
+ throw new Error("unsupported diagnostic expected schema");
812
+ assertSelection(expected.selection, "diagnostic expected selection", true);
813
+ if (canonicalJson(expected.selection) !== canonicalJson(observations.selection))
814
+ throw new Error("diagnostic expected selection does not match inputs");
815
+ validateExpectedRecords(expected);
816
+ if (
817
+ expected.threshold_digest !== sha256(canonicalJson(thresholds)) ||
818
+ expected.threshold_digest !== manifest.threshold_digest
819
+ ) {
820
+ throw new Error("captured threshold digest does not match manifest");
821
+ }
822
+ if (
823
+ summary.schema_version !== DIAGNOSTIC_SUMMARY_SCHEMA_VERSION ||
824
+ summary.artifact_id !== manifest.artifact_id ||
825
+ summary.machine_id !== manifest.machine_id ||
826
+ summary.machine_id_kind !== "pseudonymous"
827
+ ) {
828
+ throw new Error("invalid diagnostic summary");
829
+ }
830
+ assertSelection(summary.selection, "diagnostic summary selection", true);
831
+ for (const count of [
832
+ summary.source_count,
833
+ summary.supported_source_count,
834
+ summary.sanitized_value_count,
835
+ summary.omitted_value_count,
836
+ summary.total_bytes,
837
+ ]) {
838
+ if (!boundedInteger(count, 0, SUPERVISOR_DIAGNOSTIC_LIMITS.max_bundle_bytes))
839
+ throw new Error("invalid diagnostic summary count");
840
+ }
841
+ if (
842
+ summary.source_count !== observations.sources.length ||
843
+ summary.supported_source_count > summary.source_count
844
+ )
845
+ throw new Error("diagnostic summary source counts do not match inputs");
846
+ }
847
+
848
+ function validateExpectedRecords(expected: ValidatedDiagnosticBundle["expected"]): void {
849
+ if (
850
+ !Array.isArray(expected.findings) ||
851
+ expected.findings.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings
852
+ )
853
+ throw new Error("diagnostic expected findings exceed the limit");
854
+ if (
855
+ !Array.isArray(expected.timelines) ||
856
+ expected.timelines.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings
857
+ )
858
+ throw new Error("diagnostic expected timelines exceed the limit");
859
+ if (
860
+ !Array.isArray(expected.explanations) ||
861
+ expected.explanations.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings
862
+ )
863
+ throw new Error("diagnostic expected explanations exceed the limit");
864
+ for (const finding of expected.findings) {
865
+ if (
866
+ !finding ||
867
+ typeof finding !== "object" ||
868
+ finding.schema_version !== SUPERVISOR_FINDING_SCHEMA_VERSION
869
+ )
870
+ throw new Error("invalid diagnostic expected finding schema");
871
+ if (
872
+ !Array.isArray(finding.evidence) ||
873
+ finding.evidence.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_evidence_per_finding
874
+ )
875
+ throw new Error("diagnostic finding evidence exceeds the limit");
876
+ validateCapabilities(finding.capabilities, "diagnostic finding capabilities");
877
+ }
878
+ for (const timeline of expected.timelines) {
879
+ if (
880
+ !timeline ||
881
+ typeof timeline !== "object" ||
882
+ timeline.schema_version !== SUPERVISOR_TIMELINE_SCHEMA_VERSION
883
+ )
884
+ throw new Error("invalid diagnostic timeline schema");
885
+ if (
886
+ !Array.isArray(timeline.entries) ||
887
+ timeline.entries.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_timeline_entries
888
+ )
889
+ throw new Error("diagnostic timeline entries exceed the limit");
890
+ validateCapabilities(timeline.capabilities, "diagnostic timeline capabilities");
891
+ if (!boundedInteger(timeline.compacted_entries, 0, 50_000))
892
+ throw new Error("invalid diagnostic timeline compaction count");
893
+ for (const entry of timeline.entries) {
894
+ if (
895
+ !entry ||
896
+ !validIso(entry.first_occurred_at) ||
897
+ !validIso(entry.last_occurred_at) ||
898
+ !boundedInteger(entry.occurrence_count, 1, 50_000) ||
899
+ Date.parse(entry.first_occurred_at) > Date.parse(entry.last_occurred_at)
900
+ ) {
901
+ throw new Error("invalid diagnostic timeline entry cluster");
902
+ }
903
+ }
904
+ }
905
+ for (const explanation of expected.explanations) {
906
+ if (
907
+ !explanation ||
908
+ typeof explanation !== "object" ||
909
+ explanation.schema_version !== SUPERVISOR_EXPLANATION_SCHEMA_VERSION
910
+ )
911
+ throw new Error("invalid diagnostic explanation schema");
912
+ for (const rows of [
913
+ explanation.observed,
914
+ explanation.related,
915
+ explanation.possible,
916
+ explanation.missing_capabilities,
917
+ ]) {
918
+ if (!Array.isArray(rows) || rows.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_explanation_items)
919
+ throw new Error("diagnostic explanation entries exceed the limit");
920
+ }
921
+ }
922
+ validateAdvice(expected.advice);
923
+ }
924
+
925
+ function validateAdvice(advice: ValidatedDiagnosticBundle["expected"]["advice"]): void {
926
+ assertKeys(
927
+ advice,
928
+ [
929
+ "schema_version",
930
+ "evaluated_at",
931
+ "pressure",
932
+ "fan_out_recommendation",
933
+ "observer_only",
934
+ "summary",
935
+ "source_capability",
936
+ "active_finding_count",
937
+ "contributing_finding_count",
938
+ "omitted_contributing_finding_count",
939
+ "contributing_findings",
940
+ "reasons",
941
+ ],
942
+ [],
943
+ "diagnostic advice",
944
+ );
945
+ if (
946
+ advice.schema_version !== DIAGNOSTIC_ADVICE_SCHEMA_VERSION ||
947
+ !validIso(advice.evaluated_at) ||
948
+ !["normal", "elevated", "critical", "unknown"].includes(advice.pressure) ||
949
+ !["proceed", "use-caution", "avoid-new-fan-out", "unknown"].includes(
950
+ advice.fan_out_recommendation,
951
+ ) ||
952
+ advice.observer_only !== true ||
953
+ typeof advice.summary !== "string" ||
954
+ advice.summary.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_summary_chars
955
+ ) {
956
+ throw new Error("invalid diagnostic advice contract");
957
+ }
958
+ validateCapabilities([advice.source_capability], "diagnostic advice capability");
959
+ for (const count of [
960
+ advice.active_finding_count,
961
+ advice.contributing_finding_count,
962
+ advice.omitted_contributing_finding_count,
963
+ ]) {
964
+ if (!boundedInteger(count, 0, SUPERVISOR_DIAGNOSTIC_LIMITS.max_findings))
965
+ throw new Error("invalid diagnostic advice count");
966
+ }
967
+ if (
968
+ !Array.isArray(advice.contributing_findings) ||
969
+ advice.contributing_findings.length > DIAGNOSTIC_ADVICE_LIMITS.max_contributing_findings ||
970
+ !Array.isArray(advice.reasons) ||
971
+ advice.reasons.length > DIAGNOSTIC_ADVICE_LIMITS.max_reasons
972
+ ) {
973
+ throw new Error("diagnostic advice entries exceed the limit");
974
+ }
975
+ for (const finding of advice.contributing_findings) {
976
+ assertKeys(
977
+ finding,
978
+ [
979
+ "finding_id",
980
+ "finding_kind",
981
+ "severity",
982
+ "summary",
983
+ "scope_kind",
984
+ "scope_id",
985
+ "occurrence_count",
986
+ ],
987
+ ["owner_kind", "owner_id", "workload_relationship"],
988
+ "diagnostic advice finding",
989
+ );
990
+ if (
991
+ !["warning", "critical"].includes(finding.severity) ||
992
+ typeof finding.finding_id !== "string" ||
993
+ typeof finding.summary !== "string" ||
994
+ !boundedInteger(finding.occurrence_count, 1, 50_000)
995
+ ) {
996
+ throw new Error("invalid diagnostic advice finding");
997
+ }
998
+ }
999
+ for (const reason of advice.reasons) {
1000
+ assertKeys(reason, ["code", "summary", "finding_ids"], [], "diagnostic advice reason");
1001
+ if (
1002
+ ![
1003
+ "critical_findings_active",
1004
+ "warning_findings_active",
1005
+ "findings_source_unavailable",
1006
+ "no_active_pressure_findings",
1007
+ ].includes(reason.code) ||
1008
+ typeof reason.summary !== "string" ||
1009
+ !Array.isArray(reason.finding_ids) ||
1010
+ reason.finding_ids.length > DIAGNOSTIC_ADVICE_LIMITS.max_contributing_findings ||
1011
+ reason.finding_ids.some((id: unknown) => typeof id !== "string")
1012
+ ) {
1013
+ throw new Error("invalid diagnostic advice reason");
1014
+ }
1015
+ }
1016
+ }
1017
+
1018
+ function validateCapabilities(value: unknown, label: string): void {
1019
+ if (!Array.isArray(value) || value.length > SUPERVISOR_DIAGNOSTIC_LIMITS.max_capabilities)
1020
+ throw new Error(`${label} exceed the limit`);
1021
+ for (const capability of value) {
1022
+ if (!capability || typeof capability !== "object" || Array.isArray(capability))
1023
+ throw new Error(`invalid ${label}`);
1024
+ assertKeys(capability, ["source_kind", "state"], ["reason_code", "detail"], label);
1025
+ const record = capability as Record<string, unknown>;
1026
+ if (!validSourceKind(record.source_kind) || !validCapabilityState(record.state))
1027
+ throw new Error(`invalid ${label}`);
1028
+ if (
1029
+ record.reason_code !== undefined &&
1030
+ (typeof record.reason_code !== "string" || record.reason_code.length > 240)
1031
+ )
1032
+ throw new Error(`invalid ${label}`);
1033
+ if (
1034
+ record.detail !== undefined &&
1035
+ (typeof record.detail !== "string" || record.detail.length > 2_000)
1036
+ )
1037
+ throw new Error(`invalid ${label}`);
1038
+ }
1039
+ }
1040
+
1041
+ function assertSelection(value: unknown, label: string, findingAllowed: boolean): void {
1042
+ if (!value || typeof value !== "object" || Array.isArray(value))
1043
+ throw new Error(`invalid ${label}`);
1044
+ const record = value as Record<string, unknown>;
1045
+ assertKeys(record, ["start_at", "end_at"], findingAllowed ? ["finding_id"] : [], label);
1046
+ if (
1047
+ !validIso(record.start_at) ||
1048
+ !validIso(record.end_at) ||
1049
+ Date.parse(record.start_at as string) > Date.parse(record.end_at as string)
1050
+ )
1051
+ throw new Error(`invalid ${label}`);
1052
+ if (
1053
+ record.finding_id !== undefined &&
1054
+ (typeof record.finding_id !== "string" || !/^[A-Za-z0-9_.:-]{1,240}$/.test(record.finding_id))
1055
+ )
1056
+ throw new Error(`invalid ${label}`);
1057
+ }
1058
+
1059
+ function assertKeys(
1060
+ value: object,
1061
+ required: readonly string[],
1062
+ optional: readonly string[],
1063
+ label: string,
1064
+ ): void {
1065
+ const keys = Object.keys(value);
1066
+ const allowed = new Set([...required, ...optional]);
1067
+ const missing = required.filter((key) => !keys.includes(key));
1068
+ const unknown = keys.filter((key) => !allowed.has(key));
1069
+ if (missing.length > 0 || unknown.length > 0) {
1070
+ throw new Error(`${label} keys are invalid`);
1071
+ }
1072
+ }
1073
+
1074
+ function validCapabilityState(value: unknown): boolean {
1075
+ return ["supported", "partial", "unsupported", "error"].includes(String(value));
1076
+ }
1077
+
1078
+ function validSourceKind(value: unknown): boolean {
1079
+ return typeof value === "string" && /^[a-z][a-z0-9_.:-]{0,119}$/.test(value);
1080
+ }
1081
+
1082
+ function boundedInteger(value: unknown, minimum: number, maximum: number): boolean {
1083
+ return (
1084
+ typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum
1085
+ );
1086
+ }
1087
+
1088
+ function countEntries(value: unknown): number {
1089
+ if (Array.isArray(value)) return value.length;
1090
+ const record = object(value);
1091
+ if (!record) return value === undefined ? 0 : 1;
1092
+ for (const key of ["active", "transitions", "points", "lanes", "rows"]) {
1093
+ if (Array.isArray(record[key])) return record[key].length;
1094
+ }
1095
+ return 1;
1096
+ }
1097
+
1098
+ function timestamp(record: Record<string, unknown> | undefined): string | undefined {
1099
+ if (!record) return undefined;
1100
+ for (const key of ["captured_at", "sampled_at", "generated_at", "heartbeat_at"]) {
1101
+ if (validIso(record[key])) return record[key] as string;
1102
+ }
1103
+ return undefined;
1104
+ }
1105
+
1106
+ function numeric(value: unknown): number | undefined {
1107
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
1108
+ }
1109
+
1110
+ function arrayObjects(value: unknown): Record<string, unknown>[] {
1111
+ return Array.isArray(value)
1112
+ ? value.filter((item) => !!item && typeof item === "object" && !Array.isArray(item))
1113
+ : [];
1114
+ }
1115
+
1116
+ function object(value: unknown): Record<string, unknown> | undefined {
1117
+ return value && typeof value === "object" && !Array.isArray(value)
1118
+ ? (value as Record<string, unknown>)
1119
+ : undefined;
1120
+ }
1121
+
1122
+ function parseIso(value: string, label: string): Date {
1123
+ const date = new Date(value);
1124
+ assertValidDate(date, label);
1125
+ return date;
1126
+ }
1127
+
1128
+ function validIso(value: unknown): boolean {
1129
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
1130
+ }
1131
+
1132
+ function assertValidDate(value: Date, label: string): void {
1133
+ if (!Number.isFinite(value.getTime())) throw new Error(`${label} must be a valid timestamp`);
1134
+ }
1135
+
1136
+ function tryChmod(path: string, mode: number): void {
1137
+ try {
1138
+ chmodSync(path, mode);
1139
+ } catch {
1140
+ // Windows does not expose POSIX modes. The private atomic writer still applies.
1141
+ }
1142
+ }