harnery 0.45.1 → 0.46.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 (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -0,0 +1,179 @@
1
+ import type {
2
+ CoordinationGenerationViewV3,
3
+ CoordinationViewV3,
4
+ } from "../../events/v3/coordination-view.ts";
5
+ import { readCoordinationViewV3 } from "../../events/v3/coordination-view.ts";
6
+ import { liveInstanceIdV3, nativeInstanceIdV3 } from "../../events/v3/live-route-observer.ts";
7
+ import {
8
+ type NativeParentObservation,
9
+ type NativeParentRequest,
10
+ readNativeParentReference,
11
+ } from "../../hooks/adapter/native-parent.ts";
12
+ import { resolveName } from "./names.ts";
13
+ import type { AgentParentReference } from "./parent-reference-contract.ts";
14
+
15
+ export type { AgentParentReference } from "./parent-reference-contract.ts";
16
+
17
+ export interface AgentParentSubject {
18
+ instance_id: string;
19
+ generation_id?: string;
20
+ v3_generation_id?: string;
21
+ platform?: string;
22
+ native_session_id?: string;
23
+ }
24
+
25
+ export interface AgentParentReferenceOptions {
26
+ view?: CoordinationViewV3;
27
+ readNativeParent?: (request: NativeParentRequest) => NativeParentObservation;
28
+ }
29
+
30
+ /** Display/recipient evidence only. Never use native evidence as a claim or lifecycle waiver. */
31
+ export function resolveAgentParentReference(
32
+ coordRoot: string,
33
+ child: AgentParentSubject,
34
+ options: AgentParentReferenceOptions = {},
35
+ ): AgentParentReference {
36
+ try {
37
+ const view = options.view ?? readCoordinationViewV3(coordRoot);
38
+ if (!view.source_complete || !view.authority_safe) return unknown("parent_authority_unsafe");
39
+ if (
40
+ child.generation_id &&
41
+ child.v3_generation_id &&
42
+ child.generation_id !== child.v3_generation_id
43
+ ) {
44
+ return unknown("child_generation_mismatch");
45
+ }
46
+ const instance = liveInstanceIdV3(child.instance_id);
47
+ const generationId = child.generation_id ?? child.v3_generation_id;
48
+ const generations = [
49
+ ...Object.values(view.instances),
50
+ ...Object.values(view.terminal_generations),
51
+ ];
52
+ const generation = generationId
53
+ ? generations.find(
54
+ (row) => row.generation_id === generationId && row.instance_id === instance,
55
+ )
56
+ : view.instances[instance];
57
+ if (!generation) return unknown("child_generation_unavailable");
58
+ if (!safe(view, generation)) return unknown("child_authority_unsafe");
59
+ const canonical = generation.parent_generation_id
60
+ ? generations.find((row) => row.generation_id === generation.parent_generation_id)
61
+ : undefined;
62
+ if (generation.parent_generation_id && !canonical)
63
+ return unknown("parent_generation_unavailable");
64
+ if (canonical && cyclic(generations, generation)) {
65
+ return { state: "conflict", reason: "parent_lineage_cycle" };
66
+ }
67
+ const adapter = generation.runtime_attestation.adapter;
68
+ const platform = adapter.state === "observed" ? adapter.value.id : child.platform;
69
+ const nativeId = child.native_session_id ?? nativeInstanceIdV3(instance);
70
+ const native =
71
+ platform === "codex" && liveInstanceIdV3(nativeId) === instance
72
+ ? (options.readNativeParent ?? readNativeParentReference)({
73
+ adapter: platform,
74
+ native_session_id: nativeId,
75
+ })
76
+ : ({ state: "unknown", reason: "native_child_identity_unavailable" } as const);
77
+ if (canonical) {
78
+ if (
79
+ native.state === "none" ||
80
+ (native.state === "observed" &&
81
+ liveInstanceIdV3(native.parent_session_id) !== canonical.instance_id)
82
+ ) {
83
+ return { state: "conflict", reason: "parent_sources_disagree" };
84
+ }
85
+ return reference(coordRoot, view, canonical.instance_id, canonical, "v3-session-link");
86
+ }
87
+ if (native.state === "observed") {
88
+ const parentInstance = liveInstanceIdV3(native.parent_session_id);
89
+ if (parentInstance === instance)
90
+ return { state: "conflict", reason: "parent_self_reference" };
91
+ const parent = recipientGeneration(view, parentInstance);
92
+ if (parent && cyclic(generations, generation, parent)) {
93
+ return { state: "conflict", reason: "parent_lineage_cycle" };
94
+ }
95
+ return reference(
96
+ coordRoot,
97
+ view,
98
+ parentInstance,
99
+ parent,
100
+ "native-session-metadata",
101
+ native.source_witness,
102
+ );
103
+ }
104
+ return { state: native.state, reason: native.reason };
105
+ } catch {
106
+ return unknown("parent_reference_unavailable");
107
+ }
108
+ }
109
+
110
+ function reference(
111
+ root: string,
112
+ view: CoordinationViewV3,
113
+ instance: string,
114
+ parent: CoordinationGenerationViewV3 | undefined,
115
+ source: "v3-session-link" | "native-session-metadata",
116
+ witness?: string,
117
+ ): AgentParentReference {
118
+ const native = nativeInstanceIdV3(instance);
119
+ const name = resolveName(root, native)?.name ?? null;
120
+ const recipient = recipientGeneration(view, instance);
121
+ return {
122
+ state: "verified",
123
+ relationship: source === "v3-session-link" ? "delegated-child" : "native-child",
124
+ parent_instance_id: native,
125
+ parent_v3_instance_id: instance,
126
+ parent_generation_id: parent?.generation_id ?? null,
127
+ parent_session_id: parent?.session_id ?? null,
128
+ parent_name: name,
129
+ source,
130
+ ...(witness ? { source_witness: witness } : {}),
131
+ addressable: Boolean(
132
+ parent && recipient?.generation_id === parent.generation_id && safe(view, parent) && name,
133
+ ),
134
+ };
135
+ }
136
+
137
+ function recipientGeneration(view: CoordinationViewV3, instance: string) {
138
+ return (
139
+ view.instances[instance] ??
140
+ Object.values(view.terminal_generations)
141
+ .filter((row) => row.instance_id === instance)
142
+ .sort(
143
+ (a, b) =>
144
+ b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
145
+ b.source_position.byte_offset - a.source_position.byte_offset,
146
+ )[0]
147
+ );
148
+ }
149
+
150
+ function safe(view: CoordinationViewV3, generation: CoordinationGenerationViewV3): boolean {
151
+ return !(view.diagnostics_by_generation[generation.generation_id] ?? []).some(
152
+ (d) => d.authority_blocking,
153
+ );
154
+ }
155
+
156
+ function cyclic(
157
+ generations: CoordinationGenerationViewV3[],
158
+ child: CoordinationGenerationViewV3,
159
+ first?: CoordinationGenerationViewV3,
160
+ ): boolean {
161
+ const seen = new Set([child.generation_id]);
162
+ let cursor = first ?? child;
163
+ for (let depth = 0; depth <= generations.length; depth++) {
164
+ if (cursor !== child) {
165
+ if (seen.has(cursor.generation_id) || cursor.instance_id === child.instance_id) return true;
166
+ seen.add(cursor.generation_id);
167
+ }
168
+ if (!cursor.parent_generation_id) return false;
169
+ const next = generations.find((g) => g.generation_id === cursor.parent_generation_id);
170
+ if (!next) return false;
171
+ if (seen.has(next.generation_id)) return true;
172
+ cursor = next;
173
+ }
174
+ return true;
175
+ }
176
+
177
+ function unknown(reason: string): AgentParentReference {
178
+ return { state: "unknown", reason };
179
+ }
@@ -24,7 +24,7 @@ import {
24
24
  writeFileSync,
25
25
  } from "node:fs";
26
26
  import { dirname, join } from "node:path";
27
- import { checkPidToken, processStartToken } from "./proc-start.ts";
27
+ import { processStartToken, verifyProcessGeneration } from "./proc-start.ts";
28
28
 
29
29
  /**
30
30
  * Row count that triggers a dead-row sweep on the next write.
@@ -59,7 +59,7 @@ export function parsePidmapRow(row: string): {
59
59
  */
60
60
  function rowStillNamesItsProcess(pid: number, startToken: string | undefined): boolean {
61
61
  if (!pidIsAlive(pid)) return false;
62
- return checkPidToken(pid, startToken) !== "mismatch";
62
+ return verifyProcessGeneration(pid, startToken) !== "different";
63
63
  }
64
64
 
65
65
  function atomicWrite(path: string, content: string): void {
@@ -34,10 +34,9 @@
34
34
  import { spawnSync } from "node:child_process";
35
35
  import { existsSync, readFileSync } from "node:fs";
36
36
  import { coordEnv } from "../../../lib/env.ts";
37
- import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.ts";
38
37
 
39
38
  /** Which probe answers "when did this process start?" on this machine. */
40
- export type StartTokenProbe = "procfs" | "ps" | "windows";
39
+ export type StartTokenProbe = "procfs" | "ps";
41
40
 
42
41
  /**
43
42
  * The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
@@ -61,7 +60,6 @@ let cachedBootId: string | null | undefined;
61
60
  export function startTokenProbe(): StartTokenProbe {
62
61
  const override = coordEnv("PID_PROBE");
63
62
  if (override === "procfs" || override === "ps") return override;
64
- if (process.platform === "win32") return "windows";
65
63
  if (cachedProbe === undefined) {
66
64
  cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
67
65
  }
@@ -104,7 +102,7 @@ export function resetStartTokenCaches(): void {
104
102
  * than by splitting the whole line, because comm is the executable name in
105
103
  * parentheses and may itself contain spaces and parens. The boot segment is
106
104
  * dropped rather than faked when the kernel will not name its boot, which
107
- * yields the pre-boot-id shape that `tokensAgree` still accepts.
105
+ * yields the pre-boot-id shape that `verifyProcessGeneration` compares by ticks.
108
106
  */
109
107
  export function procfsStartToken(pid: number): string | null {
110
108
  let stat: string;
@@ -159,19 +157,26 @@ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
159
157
  */
160
158
  export function processStartToken(pid: number): string | null {
161
159
  if (!Number.isFinite(pid) || pid <= 0) return null;
162
- const probe = startTokenProbe();
163
- return probe === "windows"
164
- ? windowsProcessStartToken(pid)
165
- : probe === "procfs"
166
- ? procfsStartToken(pid)
167
- : psStartToken(pid);
160
+ return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
168
161
  }
169
162
 
170
163
  export type ProcessGenerationVerdict = "same" | "different" | "unknown";
171
164
 
172
165
  type ReadProcessStartToken = (pid: number) => string | null;
173
166
 
174
- /** A missing or unreadable token never establishes process identity. */
167
+ /**
168
+ * Does `pid` still name the run that wrote `recordedToken`?
169
+ *
170
+ * - `"same"`: both tokens present and equal.
171
+ * - `"different"`: the pid was recycled. A liveness check cannot see this,
172
+ * because a recycled pid is alive.
173
+ * - `"unknown"`: a token is missing, malformed, or unreadable. Never treat
174
+ * this as proof of identity.
175
+ *
176
+ * A Linux token written without a boot segment cannot identify a run across
177
+ * reboots, so it is compared on its tick count: a different count proves a
178
+ * different run, and an equal count stays unknown.
179
+ */
175
180
  export function verifyProcessGeneration(
176
181
  pid: number,
177
182
  recordedToken: string | null | undefined,
@@ -184,26 +189,38 @@ export function verifyProcessGeneration(
184
189
  !recordedToken.trim()
185
190
  )
186
191
  return "unknown";
187
- // A bootless Linux token cannot identify a process across machine boots.
188
- if (/^l\d+$/.test(recordedToken)) return "unknown";
192
+ let currentToken: string | null;
189
193
  try {
190
- const currentToken = readToken(pid);
191
- if (typeof currentToken !== "string" || !currentToken.trim()) return "unknown";
192
- if (/^l\d+$/.test(currentToken)) return "unknown";
193
- return currentToken === recordedToken ? "same" : "different";
194
+ currentToken = readToken(pid);
194
195
  } catch {
195
196
  return "unknown";
196
197
  }
198
+ if (typeof currentToken !== "string" || !currentToken.trim()) return "unknown";
199
+ const bootless = (token: string) => /^l\d+$/.test(token);
200
+ if (
201
+ recordedToken[0] === "l" &&
202
+ currentToken[0] === "l" &&
203
+ (bootless(recordedToken) || bootless(currentToken))
204
+ )
205
+ return ticksOf(recordedToken) === ticksOf(currentToken) ? "unknown" : "different";
206
+ return currentToken === recordedToken ? "same" : "different";
207
+ }
208
+
209
+ function ticksOf(token: string): string {
210
+ const dot = token.indexOf(".");
211
+ return dot === -1 ? token.slice(1) : token.slice(dot + 1);
197
212
  }
198
213
 
199
214
  export interface ProcessGenerationSignalDependencies {
200
215
  readToken?: ReadProcessStartToken;
201
- signalProcess?: typeof signalProcessWithHandle;
216
+ kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
202
217
  }
203
218
 
204
219
  /**
205
- * Observations may refuse a signal, but only the retained native process handle
206
- * can authorize it. No numeric-PID effect follows the preliminary token read.
220
+ * Recheck the process generation immediately before sending the signal.
221
+ *
222
+ * The process can still exit and its PID can be reused between the token read
223
+ * and the signal. Eliminating that race requires a generation-bound OS handle.
207
224
  */
208
225
  export function signalIfSameGeneration(
209
226
  pid: number,
@@ -211,58 +228,13 @@ export function signalIfSameGeneration(
211
228
  signal: NodeJS.Signals | number,
212
229
  dependencies: ProcessGenerationSignalDependencies = {},
213
230
  ): { sent: boolean; verdict: ProcessGenerationVerdict } {
231
+ const kill = dependencies.kill ?? process.kill;
214
232
  const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
215
233
  if (verdict !== "same") return { sent: false, verdict };
216
234
  try {
217
- return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken!, signal);
235
+ kill(pid, signal);
236
+ return { sent: true, verdict };
218
237
  } catch {
219
- return { sent: false, verdict: "unknown" };
238
+ return { sent: false, verdict };
220
239
  }
221
240
  }
222
-
223
- /**
224
- * Do two tokens describe the same run?
225
- *
226
- * Equality, plus one concession: a row written before tokens carried a boot
227
- * segment recorded the tick count alone, so it is compared on the part it
228
- * actually recorded. Refusing that would call every pre-upgrade row a recycled
229
- * pid and prune a working machine's live rows the first time the new code ran.
230
- * Such a row loses its reboot protection until the next write rewrites it,
231
- * which is a strictly better position than the one it came from.
232
- */
233
- export function tokensAgree(recorded: string, current: string): boolean {
234
- if (recorded === current) return true;
235
- // Only the Linux form has a boot segment to disagree about, and only when
236
- // exactly one side carries one is this a shape difference rather than two
237
- // genuinely different start instants.
238
- if (recorded[0] !== "l" || current[0] !== "l") return false;
239
- if (recorded.includes(".") === current.includes(".")) return false;
240
- return ticksOf(recorded) === ticksOf(current);
241
- }
242
-
243
- function ticksOf(token: string): string {
244
- const dot = token.indexOf(".");
245
- return dot === -1 ? token.slice(1) : token.slice(dot + 1);
246
- }
247
-
248
- /**
249
- * Does `pid` still name the process a row recorded?
250
- *
251
- * Three answers, and the difference between the last two matters:
252
- *
253
- * - `"match"` — recorded token, current token, same run. The row is trustworthy.
254
- * - `"mismatch"` — both present and different. The pid was recycled. This is
255
- * the case a liveness check cannot see, because a recycled pid is alive.
256
- * - `"unverified"` — no recorded token (a row written before tokens existed) or
257
- * no current one (an unsupported platform). Callers treat this the way they
258
- * treated every row previously: trust it if the pid is alive.
259
- */
260
- export function checkPidToken(
261
- pid: number,
262
- recorded: string | undefined,
263
- ): "match" | "mismatch" | "unverified" {
264
- if (!recorded) return "unverified";
265
- const current = processStartToken(pid);
266
- if (!current) return "unverified";
267
- return tokensAgree(recorded, current) ? "match" : "mismatch";
268
- }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * One owner check for every singleton background service (governor, local
3
+ * supervisor, legacy resource observer): is the recorded process still the
4
+ * run that wrote the record, and how is a stop requested?
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import {
8
+ closeSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ openSync,
12
+ readFileSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { hostname } from "node:os";
17
+ import { dirname } from "node:path";
18
+ import { writePrivateJsonAtomic } from "../../storage/atomic-json.ts";
19
+ import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
20
+ import {
21
+ type ProcessGenerationSignalDependencies,
22
+ type ProcessGenerationVerdict,
23
+ processStartToken,
24
+ signalIfSameGeneration,
25
+ verifyProcessGeneration,
26
+ } from "./proc-start.ts";
27
+
28
+ export interface ServiceProcessDependencies extends ProcessGenerationSignalDependencies {
29
+ /** Throws ESRCH, or returns false, when the process is gone. */
30
+ probePid?: (pid: number) => unknown;
31
+ }
32
+
33
+ export type ServiceLiveness = "running" | "stopped" | "unknown";
34
+
35
+ export interface ServiceProcessOwner {
36
+ pid: number;
37
+ host: string;
38
+ start_token?: string;
39
+ state?: string;
40
+ }
41
+
42
+ /**
43
+ * A stale heartbeat does not prove that a process has exited. Another host, a
44
+ * probe error other than ESRCH, or an unverifiable token answers unknown. A
45
+ * recorded `stopped` state counts only while the generation still matches.
46
+ */
47
+ export function observeServiceProcess(
48
+ owner: ServiceProcessOwner,
49
+ dependencies: ServiceProcessDependencies = {},
50
+ ): { generation: ProcessGenerationVerdict; liveness: ServiceLiveness } {
51
+ if (owner.host !== hostname()) return { generation: "unknown", liveness: "unknown" };
52
+ try {
53
+ if ((dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid) === false)
54
+ return { generation: "unknown", liveness: "stopped" };
55
+ } catch (error) {
56
+ return {
57
+ generation: "unknown",
58
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
59
+ };
60
+ }
61
+ const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
62
+ return {
63
+ generation,
64
+ liveness:
65
+ generation === "same"
66
+ ? owner.state === "stopped"
67
+ ? "stopped"
68
+ : "running"
69
+ : generation === "different"
70
+ ? "stopped"
71
+ : "unknown",
72
+ };
73
+ }
74
+
75
+ /** Refuse a start while another owner runs or cannot be identified. */
76
+ export function serviceStartRefusal(
77
+ label: string,
78
+ stopCommand: string,
79
+ pid: number | undefined,
80
+ liveness: ServiceLiveness,
81
+ ): Error {
82
+ return new Error(
83
+ liveness === "running"
84
+ ? `${label} is already running under pid ${pid}`
85
+ : `${label} process generation is unknown${pid ? ` for pid ${pid}` : ""}; run \`${stopCommand}\` to write a durable stop request before starting it again`,
86
+ );
87
+ }
88
+
89
+ export interface ServiceLease {
90
+ pid: number;
91
+ start_token?: string;
92
+ host: string;
93
+ nonce: string;
94
+ created_at?: string;
95
+ }
96
+
97
+ export function readServiceLease(path: string): ServiceLease | undefined {
98
+ try {
99
+ const value = JSON.parse(readFileSync(path, "utf8")) as Partial<ServiceLease>;
100
+ if (
101
+ !Number.isSafeInteger(value.pid) ||
102
+ (value.pid ?? 0) <= 0 ||
103
+ typeof value.host !== "string" ||
104
+ !value.host ||
105
+ typeof value.nonce !== "string" ||
106
+ !value.nonce
107
+ )
108
+ return undefined;
109
+ return {
110
+ ...value,
111
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
112
+ } as ServiceLease;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /** An absent lease is stopped; an unreadable one cannot be identified. */
119
+ export function serviceLeaseOwner(path: string): {
120
+ lease?: ServiceLease;
121
+ liveness: ServiceLiveness;
122
+ } {
123
+ if (!existsSync(path)) return { liveness: "stopped" };
124
+ const lease = readServiceLease(path);
125
+ return { lease, liveness: lease ? observeServiceProcess(lease).liveness : "unknown" };
126
+ }
127
+
128
+ /**
129
+ * Take the singleton lease with an exclusive create. A lease whose owner is
130
+ * proven stopped is replaced; any other owner refuses the start.
131
+ */
132
+ export function acquireServiceLease(
133
+ path: string,
134
+ refuse: (pid: number | undefined, liveness: ServiceLiveness) => Error,
135
+ fields: Record<string, unknown> = {},
136
+ ): { nonce: string; release: () => void } {
137
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
138
+ for (let attempt = 0; attempt < 2; attempt++) {
139
+ const startToken = processStartToken(process.pid);
140
+ const lease = {
141
+ ...fields,
142
+ pid: process.pid,
143
+ ...(startToken ? { start_token: startToken } : {}),
144
+ host: hostname(),
145
+ nonce: randomUUID(),
146
+ created_at: new Date().toISOString(),
147
+ };
148
+ try {
149
+ const fd = openSync(path, "wx", stateFileMode());
150
+ try {
151
+ writeFileSync(fd, `${JSON.stringify(lease)}\n`, "utf8");
152
+ } finally {
153
+ closeSync(fd);
154
+ }
155
+ return {
156
+ nonce: lease.nonce,
157
+ release: () => {
158
+ if (readServiceLease(path)?.nonce !== lease.nonce) return;
159
+ try {
160
+ unlinkSync(path);
161
+ } catch {
162
+ // A missing lease is already released.
163
+ }
164
+ },
165
+ };
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
168
+ const owner = serviceLeaseOwner(path);
169
+ if (owner.liveness !== "stopped") throw refuse(owner.lease?.pid, owner.liveness);
170
+ try {
171
+ unlinkSync(path);
172
+ } catch {
173
+ // A concurrent starter may have replaced or removed the stale lease.
174
+ }
175
+ }
176
+ }
177
+ throw new Error(`could not acquire the singleton lease at ${path}`);
178
+ }
179
+
180
+ export interface ServiceStopFields {
181
+ signal_sent: boolean;
182
+ signal_verdict: ProcessGenerationVerdict;
183
+ }
184
+
185
+ /**
186
+ * Write the durable stop request, then SIGTERM the owner `status` observed as
187
+ * running on this host, after a fresh generation check. The request stays for
188
+ * the daemon either way; `read` reports the state afterward.
189
+ */
190
+ export function requestServiceStop<
191
+ S extends {
192
+ liveness: ServiceLiveness;
193
+ generation: ProcessGenerationVerdict;
194
+ record?: ServiceProcessOwner;
195
+ },
196
+ >(
197
+ status: S,
198
+ read: () => S,
199
+ stopPath: string,
200
+ dependencies: ServiceProcessDependencies = {},
201
+ options: { signal?: boolean } = {},
202
+ ): S & ServiceStopFields {
203
+ writePrivateJsonAtomic(stopPath, {
204
+ requested_at: new Date().toISOString(),
205
+ requested_by_pid: process.pid,
206
+ });
207
+ const signal =
208
+ options.signal !== false && status.liveness === "running" && status.record?.host === hostname()
209
+ ? signalIfSameGeneration(
210
+ status.record.pid,
211
+ status.record.start_token,
212
+ "SIGTERM",
213
+ dependencies,
214
+ )
215
+ : { sent: false, verdict: status.generation };
216
+ return { ...read(), signal_sent: signal.sent, signal_verdict: signal.verdict };
217
+ }
@@ -92,6 +92,8 @@ interface HarneryConfig {
92
92
  requireGitFinalization?: boolean;
93
93
  finalizationRoots?: AgentFinalizationRoot[];
94
94
  };
95
+ /** Admission resource defaults and the heavy-command guard. Project config only. */
96
+ admission?: unknown;
95
97
  /** Host-owned agent instruction settings. */
96
98
  instructions?: {
97
99
  hostAddendumFile?: unknown;
@@ -633,6 +635,89 @@ export function ripgrepAutoInstall(coordRoot?: string | null): boolean {
633
635
  return readConfig(root).tools?.ripgrep?.autoInstall === true;
634
636
  }
635
637
 
638
+ /** Wait before queueing while Linux memory pressure is high (PSI `full avg10`). */
639
+ export interface AdmissionPressureConfig {
640
+ psi_full_avg10: number;
641
+ max_wait_seconds: number;
642
+ }
643
+
644
+ /** Per-resource defaults for `admission run`; command-line flags win. */
645
+ export interface AdmissionResourceConfig {
646
+ capacity?: number;
647
+ timeout_minutes?: number;
648
+ defer_on_pressure?: AdmissionPressureConfig;
649
+ }
650
+
651
+ export interface AdmissionProjectConfig {
652
+ resources: Record<string, AdmissionResourceConfig>;
653
+ /** Present when the agent hook should wrap heavy shell commands in a slot. */
654
+ guard?: { resource: string };
655
+ }
656
+
657
+ function positiveNumber(value: unknown, max: number): number | undefined {
658
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max
659
+ ? value
660
+ : undefined;
661
+ }
662
+
663
+ /**
664
+ * Admission settings from `.harnery/config.jsonc`:
665
+ *
666
+ * ```jsonc
667
+ * "admission": {
668
+ * "resources": {
669
+ * "build": {
670
+ * "capacity": 2,
671
+ * "timeout_minutes": 30,
672
+ * "defer_on_pressure": { "psi_full_avg10": 40, "max_wait_seconds": 300 }
673
+ * }
674
+ * },
675
+ * "guard": { "resource": "build" }
676
+ * }
677
+ * ```
678
+ *
679
+ * Project config only, like other machine-shaping policy. Malformed values are
680
+ * dropped one by one rather than rejecting the whole block. Undefined when the
681
+ * project declares no admission settings.
682
+ */
683
+ export function readAdmissionConfig(coordRoot?: string | null): AdmissionProjectConfig | undefined {
684
+ const root = coordRoot ?? findCoordRoot();
685
+ if (!root) return undefined;
686
+ const raw = readProjectConfig(root).admission;
687
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
688
+ const record = raw as { resources?: unknown; guard?: unknown };
689
+ const resources: Record<string, AdmissionResourceConfig> = {};
690
+ if (
691
+ record.resources &&
692
+ typeof record.resources === "object" &&
693
+ !Array.isArray(record.resources)
694
+ ) {
695
+ for (const [name, value] of Object.entries(record.resources as Record<string, unknown>)) {
696
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
697
+ const entry = value as Record<string, unknown>;
698
+ const capacity = positiveNumber(entry.capacity, 32);
699
+ const timeout = positiveNumber(entry.timeout_minutes, 1440);
700
+ const pressure = entry.defer_on_pressure as Record<string, unknown> | undefined;
701
+ const threshold = positiveNumber(pressure?.psi_full_avg10, 100);
702
+ const maxWait = positiveNumber(pressure?.max_wait_seconds, 86_400);
703
+ resources[name] = {
704
+ ...(capacity !== undefined && Number.isInteger(capacity) ? { capacity } : {}),
705
+ ...(timeout !== undefined && Number.isInteger(timeout) ? { timeout_minutes: timeout } : {}),
706
+ ...(threshold !== undefined && maxWait !== undefined
707
+ ? { defer_on_pressure: { psi_full_avg10: threshold, max_wait_seconds: maxWait } }
708
+ : {}),
709
+ };
710
+ }
711
+ }
712
+ const guard = record.guard as { resource?: unknown } | undefined;
713
+ return {
714
+ resources,
715
+ ...(guard && typeof guard.resource === "string" && guard.resource
716
+ ? { guard: { resource: guard.resource } }
717
+ : {}),
718
+ };
719
+ }
720
+
636
721
  /**
637
722
  * Whether this repo pins workflow runs to subscription billing:
638
723
  * `.harnery/config.jsonc` `{ "workflow": { "subscriptionOnly": true } }`.