harnery 0.30.0 → 0.31.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 (501) hide show
  1. package/README.md +4 -4
  2. package/dist/commander.d.ts +8 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +14 -13
  5. package/dist/commands/adapter.d.ts +12 -0
  6. package/dist/commands/adapter.d.ts.map +1 -0
  7. package/dist/commands/{harness.js → adapter.js} +35 -35
  8. package/dist/commands/agents.d.ts +3 -2
  9. package/dist/commands/agents.d.ts.map +1 -1
  10. package/dist/commands/agents.js +61 -60
  11. package/dist/commands/artifacts.d.ts +4 -0
  12. package/dist/commands/artifacts.d.ts.map +1 -0
  13. package/dist/commands/artifacts.js +143 -0
  14. package/dist/commands/backup.d.ts +1 -1
  15. package/dist/commands/backup.d.ts.map +1 -1
  16. package/dist/commands/backup.js +5 -2
  17. package/dist/commands/browse.d.ts.map +1 -1
  18. package/dist/commands/browse.js +310 -7
  19. package/dist/commands/checkpoint.d.ts +12 -0
  20. package/dist/commands/checkpoint.d.ts.map +1 -0
  21. package/dist/commands/checkpoint.js +162 -0
  22. package/dist/commands/context.d.ts +2 -2
  23. package/dist/commands/context.d.ts.map +1 -1
  24. package/dist/commands/context.js +3 -151
  25. package/dist/commands/decision.d.ts.map +1 -1
  26. package/dist/commands/decision.js +31 -7
  27. package/dist/commands/deinit.d.ts +2 -2
  28. package/dist/commands/deinit.js +15 -15
  29. package/dist/commands/doctor.js +23 -23
  30. package/dist/commands/governor.d.ts +4 -0
  31. package/dist/commands/governor.d.ts.map +1 -0
  32. package/dist/commands/{supervisor.js → governor.js} +133 -108
  33. package/dist/commands/init.d.ts +12 -12
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +44 -30
  36. package/dist/commands/{session.d.ts → journal.d.ts} +2 -2
  37. package/dist/commands/journal.d.ts.map +1 -0
  38. package/dist/commands/{scratch.js → journal.js} +47 -47
  39. package/dist/commands/policy.js +1 -1
  40. package/dist/commands/presence.js +2 -2
  41. package/dist/commands/sync.d.ts +1 -1
  42. package/dist/commands/sync.js +3 -3
  43. package/dist/commands/tunnel.d.ts +16 -0
  44. package/dist/commands/tunnel.d.ts.map +1 -1
  45. package/dist/commands/tunnel.js +193 -23
  46. package/dist/commands/web-fetch.js +1 -1
  47. package/dist/commands/web.d.ts +19 -0
  48. package/dist/commands/web.d.ts.map +1 -1
  49. package/dist/commands/web.js +51 -0
  50. package/dist/commands/work.d.ts.map +1 -1
  51. package/dist/commands/work.js +38 -22
  52. package/dist/commands/workflow.d.ts.map +1 -1
  53. package/dist/commands/workflow.js +35 -28
  54. package/dist/core/{harnesses → adapters}/attest-projection.d.ts +2 -2
  55. package/dist/core/adapters/attest-projection.d.ts.map +1 -0
  56. package/dist/core/{harnesses → adapters}/attest.d.ts +16 -16
  57. package/dist/core/adapters/attest.d.ts.map +1 -0
  58. package/dist/core/{harnesses → adapters}/attest.js +9 -9
  59. package/dist/core/{harnesses → adapters}/attestation.d.ts +16 -16
  60. package/dist/core/adapters/attestation.d.ts.map +1 -0
  61. package/dist/core/{harnesses → adapters}/attestation.js +20 -20
  62. package/dist/core/{harnesses → adapters}/bench.d.ts +12 -12
  63. package/dist/core/adapters/bench.d.ts.map +1 -0
  64. package/dist/core/{harnesses → adapters}/bench.js +12 -12
  65. package/dist/core/adapters/index.d.ts +13 -0
  66. package/dist/core/adapters/index.d.ts.map +1 -0
  67. package/dist/core/adapters/index.js +6 -0
  68. package/dist/core/{harnesses → adapters}/profiles.d.ts +11 -11
  69. package/dist/core/adapters/profiles.d.ts.map +1 -0
  70. package/dist/core/{harnesses → adapters}/profiles.js +11 -11
  71. package/dist/core/adapters/registry.d.ts +15 -0
  72. package/dist/core/adapters/registry.d.ts.map +1 -0
  73. package/dist/core/{harnesses → adapters}/registry.js +11 -11
  74. package/dist/core/{harnesses → adapters}/types.d.ts +23 -23
  75. package/dist/core/adapters/types.d.ts.map +1 -0
  76. package/dist/core/{harnesses → adapters}/types.js +1 -1
  77. package/dist/core/agents/canonical-emit.d.ts +4 -4
  78. package/dist/core/agents/canonical-emit.js +5 -5
  79. package/dist/core/agents/cli-emit.js +1 -1
  80. package/dist/core/agents/cli.js +30 -26
  81. package/dist/core/agents/codex-replay.d.ts +1 -1
  82. package/dist/core/agents/codex-replay.js +6 -6
  83. package/dist/core/agents/coord-client.d.ts +21 -2
  84. package/dist/core/agents/coord-client.d.ts.map +1 -1
  85. package/dist/core/agents/coord-client.js +122 -27
  86. package/dist/core/agents/events/consume.d.ts +1 -1
  87. package/dist/core/agents/events/emit.d.ts +5 -6
  88. package/dist/core/agents/events/emit.d.ts.map +1 -1
  89. package/dist/core/agents/events/emit.js +2 -2
  90. package/dist/core/agents/render/prompt-context.d.ts +21 -9
  91. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  92. package/dist/core/agents/render/prompt-context.js +82 -30
  93. package/dist/core/agents/render/session-context.d.ts +1 -1
  94. package/dist/core/agents/render/session-context.js +5 -5
  95. package/dist/core/agents/rules/claim-conflict.d.ts +2 -6
  96. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  97. package/dist/core/agents/rules/claim-conflict.js +1 -1
  98. package/dist/core/agents/rules/commit-conflict.js +98 -22
  99. package/dist/core/agents/rules/stop-hook.d.ts +9 -9
  100. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  101. package/dist/core/agents/rules/stop-hook.js +23 -5
  102. package/dist/core/agents/rules/verdict.d.ts +14 -0
  103. package/dist/core/agents/rules/verdict.d.ts.map +1 -0
  104. package/dist/core/agents/rules/verdict.js +1 -0
  105. package/dist/core/agents/session-events.js +3 -3
  106. package/dist/core/agents/state/council.d.ts +1 -1
  107. package/dist/core/agents/state/council.js +1 -1
  108. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -2
  109. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  110. package/dist/core/agents/state/heartbeat-projector.js +14 -10
  111. package/dist/core/agents/state/heartbeat-writer.d.ts +1 -1
  112. package/dist/core/agents/state/heartbeat-writer.js +12 -12
  113. package/dist/core/agents/state/{scratch.d.ts → journal.d.ts} +7 -7
  114. package/dist/core/agents/state/{scratch.d.ts.map → journal.d.ts.map} +1 -1
  115. package/dist/core/agents/state/{scratch.js → journal.js} +12 -12
  116. package/dist/core/agents/state/pidmap.d.ts +28 -4
  117. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  118. package/dist/core/agents/state/pidmap.js +130 -15
  119. package/dist/core/agents/state/proc-start.d.ts +95 -0
  120. package/dist/core/agents/state/proc-start.d.ts.map +1 -0
  121. package/dist/core/agents/state/proc-start.js +199 -0
  122. package/dist/core/agents/state/shell-mutation.d.ts +1 -1
  123. package/dist/core/agents/state/shell-mutation.js +1 -1
  124. package/dist/core/agents/state/stale-sweep.js +7 -7
  125. package/dist/core/artifacts/index.d.ts +86 -0
  126. package/dist/core/artifacts/index.d.ts.map +1 -0
  127. package/dist/core/artifacts/index.js +421 -0
  128. package/dist/core/config.d.ts +9 -2
  129. package/dist/core/config.d.ts.map +1 -1
  130. package/dist/core/config.js +26 -2
  131. package/dist/core/context/index.d.ts +12 -12
  132. package/dist/core/context/index.js +10 -10
  133. package/dist/core/governor/adapter-spread.d.ts +52 -0
  134. package/dist/core/governor/adapter-spread.d.ts.map +1 -0
  135. package/dist/core/governor/adapter-spread.js +52 -0
  136. package/dist/core/governor/index.d.ts +7 -0
  137. package/dist/core/governor/index.d.ts.map +1 -0
  138. package/dist/core/governor/index.js +6 -0
  139. package/dist/core/governor/plan-api.d.ts +38 -0
  140. package/dist/core/governor/plan-api.d.ts.map +1 -0
  141. package/dist/core/governor/plan-api.js +83 -0
  142. package/dist/core/governor/plan-read.d.ts +5 -0
  143. package/dist/core/governor/plan-read.d.ts.map +1 -0
  144. package/dist/core/{supervisor → governor}/plan-read.js +66 -52
  145. package/dist/core/{supervisor → governor}/plan-types.d.ts +57 -56
  146. package/dist/core/governor/plan-types.d.ts.map +1 -0
  147. package/dist/core/{supervisor → governor}/plan-types.js +4 -4
  148. package/dist/core/governor/planning.d.ts +48 -0
  149. package/dist/core/governor/planning.d.ts.map +1 -0
  150. package/dist/core/{supervisor → governor}/planning.js +125 -94
  151. package/dist/core/governor/read.d.ts +6 -0
  152. package/dist/core/governor/read.d.ts.map +1 -0
  153. package/dist/core/governor/read.js +4 -0
  154. package/dist/core/governor/runner.d.ts +37 -0
  155. package/dist/core/governor/runner.d.ts.map +1 -0
  156. package/dist/core/{supervisor → governor}/runner.js +15 -15
  157. package/dist/core/governor/service-read.d.ts +6 -0
  158. package/dist/core/governor/service-read.d.ts.map +1 -0
  159. package/dist/core/{supervisor → governor}/service-read.js +12 -12
  160. package/dist/core/governor/service.d.ts +114 -0
  161. package/dist/core/governor/service.d.ts.map +1 -0
  162. package/dist/core/{supervisor → governor}/service.js +57 -57
  163. package/dist/core/governor/state.d.ts +116 -0
  164. package/dist/core/governor/state.d.ts.map +1 -0
  165. package/dist/core/{supervisor → governor}/state.js +228 -108
  166. package/dist/core/hooks/adapter/detect.d.ts +9 -0
  167. package/dist/core/hooks/{harness → adapter}/detect.d.ts.map +1 -1
  168. package/dist/core/hooks/{harness → adapter}/detect.js +8 -8
  169. package/dist/core/hooks/{harness → adapter}/events.d.ts +15 -14
  170. package/dist/core/hooks/adapter/events.d.ts.map +1 -0
  171. package/dist/core/hooks/{harness → adapter}/events.js +5 -5
  172. package/dist/core/hooks/{harness → adapter}/output.d.ts +20 -14
  173. package/dist/core/hooks/adapter/output.d.ts.map +1 -0
  174. package/dist/core/hooks/{harness → adapter}/output.js +30 -22
  175. package/dist/core/hooks/{harness → adapter}/parse.d.ts +5 -5
  176. package/dist/core/hooks/{harness → adapter}/parse.d.ts.map +1 -1
  177. package/dist/core/hooks/{harness → adapter}/parse.js +4 -4
  178. package/dist/core/hooks/{harness → adapter}/wiring.d.ts +15 -15
  179. package/dist/core/hooks/{harness → adapter}/wiring.d.ts.map +1 -1
  180. package/dist/core/hooks/{harness → adapter}/wiring.js +13 -13
  181. package/dist/core/hooks/cli.d.ts +3 -3
  182. package/dist/core/hooks/cli.js +175 -151
  183. package/dist/core/hooks/effects/image-capture.d.ts +5 -5
  184. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  185. package/dist/core/hooks/effects/image-capture.js +3 -3
  186. package/dist/core/hooks/effects/index.d.ts +12 -12
  187. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  188. package/dist/core/hooks/effects/index.js +17 -17
  189. package/dist/core/hooks/events/emit.d.ts +2 -2
  190. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  191. package/dist/core/hooks/events/emit.js +1 -1
  192. package/dist/core/hooks/events/rotate.js +1 -1
  193. package/dist/core/hooks/events/schema.d.ts +13 -5
  194. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  195. package/dist/core/hooks/guard-path.d.ts +2 -2
  196. package/dist/core/hooks/guard-path.js +2 -2
  197. package/dist/core/hooks/index.d.ts +1 -1
  198. package/dist/core/hooks/index.d.ts.map +1 -1
  199. package/dist/core/hooks/resolve/anchor.d.ts +14 -5
  200. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -1
  201. package/dist/core/hooks/resolve/anchor.js +64 -7
  202. package/dist/core/hooks/resolve/coord-root.d.ts +3 -3
  203. package/dist/core/hooks/resolve/coord-root.js +4 -4
  204. package/dist/core/hooks/resolve/intent.js +1 -1
  205. package/dist/core/hooks/resolve/owner.d.ts +1 -1
  206. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  207. package/dist/core/hooks/resolve/owner.js +11 -5
  208. package/dist/core/{scratch → journal}/index.d.ts +25 -25
  209. package/dist/core/{scratch → journal}/index.d.ts.map +1 -1
  210. package/dist/core/{scratch → journal}/index.js +38 -38
  211. package/dist/core/policy/evaluate.d.ts +1 -1
  212. package/dist/core/policy/evaluate.d.ts.map +1 -1
  213. package/dist/core/policy/evaluate.js +7 -7
  214. package/dist/core/policy/types.d.ts +3 -3
  215. package/dist/core/policy/types.d.ts.map +1 -1
  216. package/dist/core/work/state.d.ts +12 -4
  217. package/dist/core/work/state.d.ts.map +1 -1
  218. package/dist/core/work/state.js +50 -17
  219. package/dist/core/workflow/adapters.d.ts +17 -0
  220. package/dist/core/workflow/adapters.d.ts.map +1 -0
  221. package/dist/core/workflow/adapters.js +21 -0
  222. package/dist/core/workflow/approvals.js +6 -6
  223. package/dist/core/workflow/billing.d.ts +9 -9
  224. package/dist/core/workflow/billing.js +9 -9
  225. package/dist/core/workflow/child-env.d.ts +10 -2
  226. package/dist/core/workflow/child-env.d.ts.map +1 -1
  227. package/dist/core/workflow/child-env.js +12 -4
  228. package/dist/core/workflow/engine.d.ts +15 -5
  229. package/dist/core/workflow/engine.d.ts.map +1 -1
  230. package/dist/core/workflow/engine.js +138 -83
  231. package/dist/core/workflow/evidence-preflight.d.ts +24 -0
  232. package/dist/core/workflow/evidence-preflight.d.ts.map +1 -0
  233. package/dist/core/workflow/evidence-preflight.js +134 -0
  234. package/dist/core/workflow/index.d.ts +1 -1
  235. package/dist/core/workflow/index.d.ts.map +1 -1
  236. package/dist/core/workflow/proof.d.ts +11 -5
  237. package/dist/core/workflow/proof.d.ts.map +1 -1
  238. package/dist/core/workflow/proof.js +45 -36
  239. package/dist/core/workflow/run-state.d.ts +1 -1
  240. package/dist/core/workflow/run-state.js +7 -7
  241. package/dist/core/workflow/sandbox-projection.d.ts +6 -6
  242. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  243. package/dist/core/workflow/sandbox-projection.js +11 -11
  244. package/dist/core/workflow/spawn-claude.d.ts +3 -3
  245. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  246. package/dist/core/workflow/spawn-claude.js +9 -6
  247. package/dist/core/workflow/spawn-codex.d.ts +4 -4
  248. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  249. package/dist/core/workflow/spawn-codex.js +9 -6
  250. package/dist/core/workflow/spawn-cursor.d.ts +4 -4
  251. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  252. package/dist/core/workflow/spawn-cursor.js +9 -6
  253. package/dist/core/workflow/spawn-failure.d.ts +1 -1
  254. package/dist/core/workflow/spawn-failure.js +1 -1
  255. package/dist/core/workflow/specialists.js +2 -2
  256. package/dist/core/workflow/{journal.d.ts → transcript.d.ts} +8 -8
  257. package/dist/core/workflow/transcript.d.ts.map +1 -0
  258. package/dist/core/workflow/{journal.js → transcript.js} +14 -14
  259. package/dist/core/workflow/types.d.ts +98 -51
  260. package/dist/core/workflow/types.d.ts.map +1 -1
  261. package/dist/core/workflow/types.js +11 -1
  262. package/dist/core/workflow/validate.d.ts +1 -1
  263. package/dist/core/workflow/validate.js +1 -1
  264. package/dist/core/workflow/workspaces/cancellation.js +2 -2
  265. package/dist/core/workflow/workspaces/cleanup.js +2 -2
  266. package/dist/core/workflow/workspaces/execution.js +6 -6
  267. package/dist/core/workflow/workspaces/inspect.js +9 -9
  268. package/dist/core/workflow/workspaces/integration.js +14 -14
  269. package/dist/core/workflow/workspaces/lifecycle.d.ts +1 -1
  270. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -1
  271. package/dist/core/workflow/workspaces/lifecycle.js +9 -9
  272. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  273. package/dist/core/workflow/workspaces/local-git.js +39 -5
  274. package/dist/core/workflow/workspaces/recovery.js +3 -3
  275. package/dist/core/workflow/workspaces/state.d.ts +1 -1
  276. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  277. package/dist/core/workflow/workspaces/state.js +3 -3
  278. package/dist/core/workflow/workspaces/types.d.ts +1 -1
  279. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  280. package/dist/lib/browser/asserts.d.ts +28 -0
  281. package/dist/lib/browser/asserts.d.ts.map +1 -0
  282. package/dist/lib/browser/asserts.js +152 -0
  283. package/dist/lib/browser/client.d.ts +49 -0
  284. package/dist/lib/browser/client.d.ts.map +1 -1
  285. package/dist/lib/browser/client.js +155 -14
  286. package/dist/lib/browser/content-checks.d.ts +108 -0
  287. package/dist/lib/browser/content-checks.d.ts.map +1 -0
  288. package/dist/lib/browser/content-checks.js +474 -0
  289. package/dist/lib/browser/critique.d.ts +91 -0
  290. package/dist/lib/browser/critique.d.ts.map +1 -0
  291. package/dist/lib/browser/critique.js +167 -0
  292. package/dist/lib/browser/geometry.d.ts +32 -0
  293. package/dist/lib/browser/geometry.d.ts.map +1 -1
  294. package/dist/lib/browser/geometry.js +244 -1
  295. package/dist/lib/browser/index.d.ts +4 -1
  296. package/dist/lib/browser/index.d.ts.map +1 -1
  297. package/dist/lib/browser/index.js +2 -0
  298. package/dist/lib/claude-desktop.d.ts.map +1 -1
  299. package/dist/lib/claude-desktop.js +4 -1
  300. package/dist/lib/council/index.d.ts +6 -6
  301. package/dist/lib/council/index.d.ts.map +1 -1
  302. package/dist/lib/council/index.js +8 -8
  303. package/dist/lib/identities/assume.d.ts +2 -2
  304. package/dist/lib/identities/assume.js +7 -7
  305. package/dist/lib/instructions/apply.d.ts +3 -3
  306. package/dist/lib/instructions/apply.d.ts.map +1 -1
  307. package/dist/lib/instructions/apply.js +75 -27
  308. package/dist/lib/instructions/host-addendum.d.ts +54 -0
  309. package/dist/lib/instructions/host-addendum.d.ts.map +1 -0
  310. package/dist/lib/instructions/host-addendum.js +107 -0
  311. package/dist/lib/instructions/templates.d.ts +10 -3
  312. package/dist/lib/instructions/templates.d.ts.map +1 -1
  313. package/dist/lib/instructions/templates.js +41 -12
  314. package/dist/lib/machine.d.ts +2 -2
  315. package/dist/lib/machine.js +2 -2
  316. package/package.json +26 -21
  317. package/schemas/config.schema.json +26 -1
  318. package/src/commander.ts +22 -13
  319. package/src/commands/{harness.ts → adapter.ts} +46 -46
  320. package/src/commands/agents.ts +71 -64
  321. package/src/commands/artifacts.ts +168 -0
  322. package/src/commands/backup.ts +47 -33
  323. package/src/commands/browse.ts +438 -2
  324. package/src/commands/checkpoint.ts +196 -0
  325. package/src/commands/context.ts +3 -176
  326. package/src/commands/decision.ts +41 -7
  327. package/src/commands/deinit.ts +16 -16
  328. package/src/commands/doctor.ts +28 -28
  329. package/src/commands/{supervisor.ts → governor.ts} +184 -154
  330. package/src/commands/init.ts +50 -34
  331. package/src/commands/{scratch.ts → journal.ts} +60 -60
  332. package/src/commands/policy.ts +1 -1
  333. package/src/commands/presence.ts +2 -2
  334. package/src/commands/sync.ts +3 -3
  335. package/src/commands/tunnel.ts +237 -34
  336. package/src/commands/web-fetch.ts +1 -1
  337. package/src/commands/web.ts +56 -1
  338. package/src/commands/work.ts +50 -35
  339. package/src/commands/workflow.ts +43 -34
  340. package/src/core/{harnesses → adapters}/attest-projection.ts +2 -2
  341. package/src/core/{harnesses → adapters}/attest.ts +27 -27
  342. package/src/core/{harnesses → adapters}/attestation.ts +41 -41
  343. package/src/core/{harnesses → adapters}/bench.ts +34 -43
  344. package/src/core/adapters/index.ts +56 -0
  345. package/src/core/{harnesses → adapters}/profiles.ts +16 -16
  346. package/src/core/{harnesses → adapters}/registry.ts +23 -23
  347. package/src/core/{harnesses → adapters}/types.ts +23 -23
  348. package/src/core/agents/canonical-emit.ts +6 -6
  349. package/src/core/agents/cli-emit.ts +1 -1
  350. package/src/core/agents/cli.ts +30 -28
  351. package/src/core/agents/codex-replay.ts +6 -6
  352. package/src/core/agents/coord-client.ts +119 -28
  353. package/src/core/agents/events/consume.ts +1 -1
  354. package/src/core/agents/events/emit.ts +7 -6
  355. package/src/core/agents/render/prompt-context.ts +107 -31
  356. package/src/core/agents/render/session-context.ts +6 -6
  357. package/src/core/agents/rules/claim-conflict.ts +4 -7
  358. package/src/core/agents/rules/commit-conflict.ts +103 -22
  359. package/src/core/agents/rules/stop-hook.ts +33 -17
  360. package/src/core/agents/rules/verdict.ts +13 -0
  361. package/src/core/agents/session-events.ts +4 -4
  362. package/src/core/agents/state/council.ts +1 -1
  363. package/src/core/agents/state/heartbeat-projector.ts +16 -12
  364. package/src/core/agents/state/heartbeat-writer.ts +12 -12
  365. package/src/core/agents/state/{scratch.ts → journal.ts} +12 -12
  366. package/src/core/agents/state/pidmap.ts +130 -15
  367. package/src/core/agents/state/proc-start.ts +208 -0
  368. package/src/core/agents/state/shell-mutation.ts +1 -1
  369. package/src/core/agents/state/stale-sweep.ts +7 -7
  370. package/src/core/artifacts/index.ts +557 -0
  371. package/src/core/config.ts +30 -2
  372. package/src/core/context/index.ts +15 -15
  373. package/src/core/governor/adapter-spread.ts +82 -0
  374. package/src/core/governor/index.ts +91 -0
  375. package/src/core/governor/plan-api.ts +115 -0
  376. package/src/core/{supervisor → governor}/plan-read.ts +112 -97
  377. package/src/core/{supervisor → governor}/plan-types.ts +59 -56
  378. package/src/core/{supervisor → governor}/planning.ts +223 -192
  379. package/src/core/governor/read.ts +59 -0
  380. package/src/core/{supervisor → governor}/runner.ts +39 -39
  381. package/src/core/{supervisor → governor}/service-read.ts +22 -22
  382. package/src/core/{supervisor → governor}/service.ts +117 -119
  383. package/src/core/{supervisor → governor}/state.ts +325 -167
  384. package/src/core/hooks/adapter/detect.ts +30 -0
  385. package/src/core/hooks/{harness → adapter}/events.ts +16 -14
  386. package/src/core/hooks/{harness → adapter}/output.ts +31 -23
  387. package/src/core/hooks/{harness → adapter}/parse.ts +5 -5
  388. package/src/core/hooks/{harness → adapter}/wiring.ts +21 -21
  389. package/src/core/hooks/cli.ts +185 -162
  390. package/src/core/hooks/effects/image-capture.ts +6 -6
  391. package/src/core/hooks/effects/index.ts +17 -17
  392. package/src/core/hooks/events/emit.ts +3 -3
  393. package/src/core/hooks/events/rotate.ts +1 -1
  394. package/src/core/hooks/events/schema.ts +15 -7
  395. package/src/core/hooks/guard-path.ts +2 -2
  396. package/src/core/hooks/index.ts +1 -1
  397. package/src/core/hooks/resolve/anchor.ts +63 -8
  398. package/src/core/hooks/resolve/coord-root.ts +4 -4
  399. package/src/core/hooks/resolve/intent.ts +1 -1
  400. package/src/core/hooks/resolve/owner.ts +11 -5
  401. package/src/core/{scratch → journal}/index.ts +53 -53
  402. package/src/core/policy/evaluate.ts +8 -8
  403. package/src/core/policy/types.ts +3 -3
  404. package/src/core/work/state.ts +65 -23
  405. package/src/core/workflow/adapters.ts +33 -0
  406. package/src/core/workflow/approvals.ts +7 -7
  407. package/src/core/workflow/billing.ts +12 -12
  408. package/src/core/workflow/child-env.ts +13 -4
  409. package/src/core/workflow/engine.ts +155 -90
  410. package/src/core/workflow/evidence-preflight.ts +165 -0
  411. package/src/core/workflow/index.ts +3 -3
  412. package/src/core/workflow/proof.ts +60 -50
  413. package/src/core/workflow/run-state.ts +7 -7
  414. package/src/core/workflow/sandbox-projection.ts +16 -16
  415. package/src/core/workflow/spawn-claude.ts +13 -10
  416. package/src/core/workflow/spawn-codex.ts +13 -10
  417. package/src/core/workflow/spawn-cursor.ts +13 -10
  418. package/src/core/workflow/spawn-failure.ts +1 -1
  419. package/src/core/workflow/specialists.ts +2 -2
  420. package/src/core/workflow/{journal.ts → transcript.ts} +16 -16
  421. package/src/core/workflow/types.ts +108 -51
  422. package/src/core/workflow/validate.ts +1 -1
  423. package/src/core/workflow/workspaces/cancellation.ts +2 -2
  424. package/src/core/workflow/workspaces/cleanup.ts +2 -2
  425. package/src/core/workflow/workspaces/execution.ts +6 -6
  426. package/src/core/workflow/workspaces/inspect.ts +14 -11
  427. package/src/core/workflow/workspaces/integration.ts +18 -14
  428. package/src/core/workflow/workspaces/lifecycle.ts +10 -10
  429. package/src/core/workflow/workspaces/local-git.ts +47 -5
  430. package/src/core/workflow/workspaces/recovery.ts +3 -3
  431. package/src/core/workflow/workspaces/state.ts +4 -3
  432. package/src/core/workflow/workspaces/types.ts +1 -1
  433. package/src/lib/browser/asserts.ts +177 -0
  434. package/src/lib/browser/client.ts +189 -14
  435. package/src/lib/browser/content-checks.ts +571 -0
  436. package/src/lib/browser/critique.ts +231 -0
  437. package/src/lib/browser/geometry.ts +297 -1
  438. package/src/lib/browser/index.ts +34 -0
  439. package/src/lib/claude-desktop.ts +4 -1
  440. package/src/lib/council/index.ts +9 -9
  441. package/src/lib/identities/assume.ts +7 -7
  442. package/src/lib/instructions/apply.ts +87 -25
  443. package/src/lib/instructions/host-addendum.ts +126 -0
  444. package/src/lib/instructions/templates.ts +44 -15
  445. package/src/lib/machine.ts +2 -2
  446. package/dist/commands/harness.d.ts +0 -12
  447. package/dist/commands/harness.d.ts.map +0 -1
  448. package/dist/commands/scratch.d.ts +0 -4
  449. package/dist/commands/scratch.d.ts.map +0 -1
  450. package/dist/commands/session.d.ts.map +0 -1
  451. package/dist/commands/session.js +0 -162
  452. package/dist/commands/supervisor.d.ts +0 -4
  453. package/dist/commands/supervisor.d.ts.map +0 -1
  454. package/dist/core/harnesses/attest-projection.d.ts.map +0 -1
  455. package/dist/core/harnesses/attest.d.ts.map +0 -1
  456. package/dist/core/harnesses/attestation.d.ts.map +0 -1
  457. package/dist/core/harnesses/bench.d.ts.map +0 -1
  458. package/dist/core/harnesses/index.d.ts +0 -13
  459. package/dist/core/harnesses/index.d.ts.map +0 -1
  460. package/dist/core/harnesses/index.js +0 -6
  461. package/dist/core/harnesses/profiles.d.ts.map +0 -1
  462. package/dist/core/harnesses/registry.d.ts +0 -15
  463. package/dist/core/harnesses/registry.d.ts.map +0 -1
  464. package/dist/core/harnesses/types.d.ts.map +0 -1
  465. package/dist/core/hooks/harness/detect.d.ts +0 -9
  466. package/dist/core/hooks/harness/events.d.ts.map +0 -1
  467. package/dist/core/hooks/harness/output.d.ts.map +0 -1
  468. package/dist/core/supervisor/index.d.ts +0 -7
  469. package/dist/core/supervisor/index.d.ts.map +0 -1
  470. package/dist/core/supervisor/index.js +0 -6
  471. package/dist/core/supervisor/plan-api.d.ts +0 -23
  472. package/dist/core/supervisor/plan-api.d.ts.map +0 -1
  473. package/dist/core/supervisor/plan-api.js +0 -50
  474. package/dist/core/supervisor/plan-read.d.ts +0 -5
  475. package/dist/core/supervisor/plan-read.d.ts.map +0 -1
  476. package/dist/core/supervisor/plan-types.d.ts.map +0 -1
  477. package/dist/core/supervisor/planning.d.ts +0 -33
  478. package/dist/core/supervisor/planning.d.ts.map +0 -1
  479. package/dist/core/supervisor/read.d.ts +0 -6
  480. package/dist/core/supervisor/read.d.ts.map +0 -1
  481. package/dist/core/supervisor/read.js +0 -4
  482. package/dist/core/supervisor/runner.d.ts +0 -37
  483. package/dist/core/supervisor/runner.d.ts.map +0 -1
  484. package/dist/core/supervisor/service-read.d.ts +0 -6
  485. package/dist/core/supervisor/service-read.d.ts.map +0 -1
  486. package/dist/core/supervisor/service.d.ts +0 -114
  487. package/dist/core/supervisor/service.d.ts.map +0 -1
  488. package/dist/core/supervisor/state.d.ts +0 -95
  489. package/dist/core/supervisor/state.d.ts.map +0 -1
  490. package/dist/core/workflow/harnesses.d.ts +0 -17
  491. package/dist/core/workflow/harnesses.d.ts.map +0 -1
  492. package/dist/core/workflow/harnesses.js +0 -21
  493. package/dist/core/workflow/journal.d.ts.map +0 -1
  494. package/src/commands/session.ts +0 -187
  495. package/src/core/harnesses/index.ts +0 -56
  496. package/src/core/hooks/harness/detect.ts +0 -30
  497. package/src/core/supervisor/index.ts +0 -89
  498. package/src/core/supervisor/plan-api.ts +0 -73
  499. package/src/core/supervisor/read.ts +0 -59
  500. package/src/core/workflow/harnesses.ts +0 -33
  501. /package/dist/core/{harnesses → adapters}/attest-projection.js +0 -0
@@ -16,7 +16,7 @@ import type { CanonicalEvent } from "../events/consume.ts";
16
16
  export interface V2Heartbeat {
17
17
  instance_id: string;
18
18
  session_id: string;
19
- harness: string;
19
+ adapter: string;
20
20
  agent_id?: string;
21
21
  name?: string;
22
22
  kind?: "session" | "subagent" | "transient";
@@ -24,8 +24,9 @@ export interface V2Heartbeat {
24
24
  platform?: string;
25
25
  subagent_call_id?: string;
26
26
  parent_session_id?: string;
27
- /** Set iff this owner is a `workflow run` child (joins to the run journal). */
27
+ /** Set iff this owner is a `workflow run` child (joins to the run transcript). */
28
28
  workflow_run_id?: string;
29
+ workflow_agent_id?: string;
29
30
  started_at?: string;
30
31
  last_heartbeat: string;
31
32
  last_tool?: string;
@@ -120,7 +121,7 @@ function seed(ev: CanonicalEvent, coordRoot: string): V2Heartbeat {
120
121
  const hb: V2Heartbeat = {
121
122
  instance_id: ev.instance_id,
122
123
  session_id: ev.session_id,
123
- harness: ev.harness,
124
+ adapter: ev.adapter,
124
125
  last_heartbeat: ev.ts,
125
126
  last_event_id: ev.event_id,
126
127
  events_applied: 0,
@@ -167,11 +168,11 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
167
168
  switch (ev.event_type) {
168
169
  case "session.start":
169
170
  hb.started_at = pickStr(d, "started_at") ?? ev.ts;
170
- hb.harness = ev.harness;
171
+ hb.adapter = ev.adapter;
171
172
  {
172
173
  const model = pickStr(d, "model");
173
174
  if (model) hb.model = model;
174
- const platform = pickStr(d, "platform") ?? harnessToPlatform(ev.harness);
175
+ const platform = pickStr(d, "platform") ?? adapterToPlatform(ev.adapter);
175
176
  hb.platform = platform;
176
177
  const name = pickStr(d, "name");
177
178
  if (name) hb.name = name;
@@ -189,6 +190,8 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
189
190
  if (parentSession) hb.parent_session_id = parentSession;
190
191
  const workflowRunId = pickStr(d, "workflow_run_id");
191
192
  if (workflowRunId) hb.workflow_run_id = workflowRunId;
193
+ const workflowAgentId = pickStr(d, "workflow_agent_id");
194
+ if (workflowAgentId) hb.workflow_agent_id = workflowAgentId;
192
195
  if (!hb.files_touched) hb.files_touched = [];
193
196
  }
194
197
  break;
@@ -209,7 +212,7 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
209
212
  hb.agent_id = ev.instance_id;
210
213
  hb.started_at = ev.ts;
211
214
  if (!hb.files_touched) hb.files_touched = [];
212
- hb.platform = harnessToPlatform(ev.harness);
215
+ hb.platform = adapterToPlatform(ev.adapter);
213
216
  break;
214
217
  }
215
218
 
@@ -231,7 +234,7 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
231
234
  hb.turn_summary = summary;
232
235
  hb.turn_summary_updated_at = ev.ts;
233
236
  }
234
- // Backfill model for harnesses that omit it at session.start (Claude
237
+ // Backfill model for adapters that omit it at session.start (Claude
235
238
  // Code). The Stop hook resolves it from the transcript by this point;
236
239
  // only set when present so we never clobber a known model.
237
240
  const model = pickStr(d, "model");
@@ -320,11 +323,11 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
320
323
  }
321
324
  }
322
325
 
323
- function harnessToPlatform(harness: string): string {
324
- if (harness === "claude-code") return "claude_code";
325
- if (harness === "cursor") return "cursor";
326
- if (harness === "codex") return "codex";
327
- return harness;
326
+ function adapterToPlatform(adapter: string): string {
327
+ if (adapter === "claude-code") return "claude_code";
328
+ if (adapter === "cursor") return "cursor";
329
+ if (adapter === "codex") return "codex";
330
+ return adapter;
328
331
  }
329
332
 
330
333
  function extractFilePath(data: Record<string, unknown>): string | undefined {
@@ -458,6 +461,7 @@ function writeHeartbeat(coordRoot: string, instanceId: string, hb: V2Heartbeat):
458
461
  setIfDefined(merged, "agent_id", hb.agent_id);
459
462
  setIfDefined(merged, "subagent_call_id", hb.subagent_call_id);
460
463
  setIfDefined(merged, "workflow_run_id", hb.workflow_run_id);
464
+ setIfDefined(merged, "workflow_agent_id", hb.workflow_agent_id);
461
465
  setIfDefined(merged, "model", hb.model);
462
466
  setIfDefined(merged, "platform", hb.platform);
463
467
  setIfDefined(merged, "started_at", hb.started_at);
@@ -25,21 +25,21 @@ import {
25
25
  import { dirname, join } from "node:path";
26
26
  import { emit } from "../events/emit.ts";
27
27
 
28
- /** Inline harness normalizer (mirrors canonical-emit.normalizeHarness), kept
28
+ /** Inline adapter normalizer (mirrors canonical-emit.normalizeAdapter), kept
29
29
  * local so the writer's only cross-module dep is the in-process emit(). */
30
- function harnessOf(platform: string | undefined): "claude-code" | "cursor" | "codex" {
30
+ function adapterOf(platform: string | undefined): "claude-code" | "cursor" | "codex" {
31
31
  if (platform === "cursor") return "cursor";
32
32
  if (platform === "codex") return "codex";
33
33
  return "claude-code";
34
34
  }
35
35
 
36
- /** Inverse of harnessOf: maps the canonical harness ("claude-code"/"cursor"/
36
+ /** Inverse of adapterOf: maps the canonical adapter ("claude-code"/"cursor"/
37
37
  * "codex") to the legacy underscore platform label stored on the heartbeat.
38
- * Mirrors heartbeat-projector.harnessToPlatform so a healed heartbeat carries
38
+ * Mirrors heartbeat-projector.adapterToPlatform so a healed heartbeat carries
39
39
  * the same platform value projection would have written. */
40
- function harnessToPlatform(harness: string | undefined): string {
41
- if (harness === "cursor") return "cursor";
42
- if (harness === "codex") return "codex";
40
+ function adapterToPlatform(adapter: string | undefined): string {
41
+ if (adapter === "cursor") return "cursor";
42
+ if (adapter === "codex") return "codex";
43
43
  return "claude_code";
44
44
  }
45
45
 
@@ -63,7 +63,7 @@ function emitHealthHeal(
63
63
  event_type: type,
64
64
  instance_id: instanceId,
65
65
  session_id: hb?.session_id ?? instanceId,
66
- harness: harnessOf(hb?.platform),
66
+ adapter: adapterOf(hb?.platform),
67
67
  source: "agent-coord",
68
68
  data,
69
69
  });
@@ -290,7 +290,7 @@ export function healHeartbeat(
290
290
  instanceId: string,
291
291
  sessionId?: string,
292
292
  model?: string,
293
- harness?: string,
293
+ adapter?: string,
294
294
  ): Heartbeat | null {
295
295
  const path = heartbeatPath(coordRoot, instanceId);
296
296
  if (existsSync(path)) {
@@ -330,11 +330,11 @@ export function healHeartbeat(
330
330
  started_at: now,
331
331
  last_heartbeat: now,
332
332
  files_touched: [],
333
- // Default to claude_code only when the caller can't tell us the harness
333
+ // Default to claude_code only when the caller can't tell us the adapter
334
334
  // (e.g. manual `harn agents heal`). The live tool.pre_use heal threads the
335
- // detected harness so a pruned Cursor/Codex heartbeat is recreated with
335
+ // detected adapter so a pruned Cursor/Codex heartbeat is recreated with
336
336
  // the correct platform instead of being mislabeled claude_code.
337
- platform: harnessToPlatform(harness),
337
+ platform: adapterToPlatform(adapter),
338
338
  };
339
339
  atomicWrite(path, JSON.stringify(hb, null, 2));
340
340
  // Write-only telemetry: only the actual-recreate branch reaches here (the
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Scratchpad mutations: append-only timestamped journal at
3
- * `.harnery/scratch/<instance_id>.md`.
2
+ * Journal mutations: append-only timestamped transcript at
3
+ * `.harnery/journal/<instance_id>.md`.
4
4
  *
5
5
  * Used by the agents-coord web UI route handlers for both operator-nudge
6
6
  * appends and inline edits.
@@ -32,12 +32,12 @@ function nowIsoSeconds(): string {
32
32
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
33
33
  }
34
34
 
35
- function scratchPath(coordRoot: string, instanceId: string): string {
36
- return join(coordRoot, ".harnery", "scratch", `${instanceId}.md`);
35
+ function journalPath(coordRoot: string, instanceId: string): string {
36
+ return join(coordRoot, ".harnery", "journal", `${instanceId}.md`);
37
37
  }
38
38
 
39
39
  function archivePath(coordRoot: string, instanceId: string, suffix: string): string {
40
- return join(coordRoot, ".harnery", "scratch", "archived", `${instanceId}-${suffix}.md`);
40
+ return join(coordRoot, ".harnery", "journal", "archived", `${instanceId}-${suffix}.md`);
41
41
  }
42
42
 
43
43
  function atomicWriteText(path: string, content: string): void {
@@ -48,10 +48,10 @@ function atomicWriteText(path: string, content: string): void {
48
48
  }
49
49
 
50
50
  /**
51
- * Append a timestamped entry to the owner's scratchpad.
51
+ * Append a timestamped entry to the owner's journal.
52
52
  * Returns true on success, false if validation failed.
53
53
  */
54
- export function appendScratch(
54
+ export function appendJournal(
55
55
  coordRoot: string,
56
56
  instanceId: string,
57
57
  category: string,
@@ -66,7 +66,7 @@ export function appendScratch(
66
66
 
67
67
  const trimmedBody =
68
68
  body.length > APPEND_BODY_CAP ? `${body.slice(0, APPEND_BODY_CAP - 3)}...` : body;
69
- const target = scratchPath(coordRoot, instanceId);
69
+ const target = journalPath(coordRoot, instanceId);
70
70
  const ts = nowIsoSeconds();
71
71
 
72
72
  let prior = "";
@@ -80,10 +80,10 @@ export function appendScratch(
80
80
  }
81
81
 
82
82
  /**
83
- * Replace the scratchpad with new body, archiving prior contents and
83
+ * Replace the journal with new body, archiving prior contents and
84
84
  * appending an "(edited via UI by the operator)" audit-marker note.
85
85
  */
86
- export function editScratchpad(
86
+ export function editJournal(
87
87
  coordRoot: string,
88
88
  instanceId: string,
89
89
  newBody: string,
@@ -92,7 +92,7 @@ export function editScratchpad(
92
92
  if (typeof newBody !== "string") {
93
93
  return { ok: false, reason: "newBody required" };
94
94
  }
95
- const target = scratchPath(coordRoot, instanceId);
95
+ const target = journalPath(coordRoot, instanceId);
96
96
  const ts = nowIsoSeconds();
97
97
  const archiveSuffix = `pre-ui-${ts.replace(/:/g, "-")}`;
98
98
  const archive = archivePath(coordRoot, instanceId, archiveSuffix);
@@ -114,7 +114,7 @@ export function editScratchpad(
114
114
  const auditMarker =
115
115
  `## [${ts}] note (edited via UI by the operator)\n` +
116
116
  `${summaryText}\n` +
117
- `Pre-edit archived at .harnery/scratch/archived/${instanceId}-${archiveSuffix}.md\n\n`;
117
+ `Pre-edit archived at .harnery/journal/archived/${instanceId}-${archiveSuffix}.md\n\n`;
118
118
 
119
119
  atomicWriteText(target, prior + auditMarker + newBody + (newBody.endsWith("\n") ? "" : "\n"));
120
120
  return { ok: true, archivePath: archive, path: target };
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Pid-map writer + liveness helpers.
3
3
  *
4
- * Per-harness pid-map at `.harnery/pid-map/<pid>` containing
5
- * `<instance_id>\t<platform>`. `harn agents whoami` walks ppid up 20 hops looking
6
- * for a matching entry (preferring the harness, falling back to any platform).
4
+ * Per-adapter pid-map at `.harnery/pid-map/<pid>` containing
5
+ * `<instance_id>\t<platform>\t<start_token>`. `harn agents whoami` walks ppid up
6
+ * 20 hops looking for a matching entry (preferring the adapter, falling back to
7
+ * any platform).
8
+ *
9
+ * The start token pins each row to one *run* of that pid, because a pid on its
10
+ * own is a number the OS re-issues — see `proc-start.ts` for how fast. Rows
11
+ * written before tokens existed carry two fields and read as unverified, which
12
+ * behaves exactly as they always did.
7
13
  *
8
14
  * Atomic temp+rename. Idempotent: re-writing the same row is a no-op.
9
15
  */
@@ -18,6 +24,43 @@ import {
18
24
  writeFileSync,
19
25
  } from "node:fs";
20
26
  import { dirname, join } from "node:path";
27
+ import { checkPidToken, processStartToken } from "./proc-start.ts";
28
+
29
+ /**
30
+ * Row count that triggers a dead-row sweep on the next write.
31
+ *
32
+ * Rows are written per hook shell, and those shells exit immediately, so an
33
+ * unattended map only grows. One repo was observed holding 512 rows of which 510
34
+ * were dead. That matters beyond disk: pids get recycled, so a stale row whose
35
+ * number is later reused makes an identity walk resolve to a long-gone agent,
36
+ * and `agents whoami` starts reporting somebody else's session.
37
+ */
38
+ const PRUNE_AT_ROWS = 200;
39
+
40
+ /** Split a row into its fields, tolerating the pre-token two-field shape. */
41
+ export function parsePidmapRow(row: string): {
42
+ instanceId: string;
43
+ platform: string;
44
+ startToken: string | undefined;
45
+ } {
46
+ const [instanceId = "", platform = "", startToken = ""] = row.trim().split("\t");
47
+ return { instanceId, platform, startToken: startToken || undefined };
48
+ }
49
+
50
+ /**
51
+ * Is this row still about the process it was written for?
52
+ *
53
+ * Two ways to fail. The pid may have exited, which a liveness probe catches.
54
+ * Or the pid may have been re-issued to an unrelated process, which a liveness
55
+ * probe cannot catch — that pid is alive — and which the start token does.
56
+ * Getting the second one wrong resolves an identity walk to a long-gone agent
57
+ * and keeps a dead agent's heartbeat looking live, so the check belongs
58
+ * everywhere a row is believed, not only where it is swept.
59
+ */
60
+ function rowStillNamesItsProcess(pid: number, startToken: string | undefined): boolean {
61
+ if (!pidIsAlive(pid)) return false;
62
+ return checkPidToken(pid, startToken) !== "mismatch";
63
+ }
21
64
 
22
65
  function atomicWrite(path: string, content: string): void {
23
66
  mkdirSync(dirname(path), { recursive: true });
@@ -26,14 +69,73 @@ function atomicWrite(path: string, content: string): void {
26
69
  renameSync(tmp, path);
27
70
  }
28
71
 
72
+ /**
73
+ * Is this pid a running process?
74
+ *
75
+ * `EPERM` means the process exists but belongs to another user, which is still
76
+ * alive. Treating every failure as "dead" would drop a live foreign row and
77
+ * weaken the checks that depend on knowing a claim is anchored.
78
+ */
79
+ function pidIsAlive(pid: number): boolean {
80
+ try {
81
+ process.kill(pid, 0); // signal 0 = liveness probe
82
+ return true;
83
+ } catch (error) {
84
+ return (error as NodeJS.ErrnoException).code === "EPERM";
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Delete every pid-map row that no longer names its process — exited, or its
90
+ * pid re-issued to something else. Returns the number removed.
91
+ */
92
+ export function prunePidmapDeadRows(coordRoot: string): number {
93
+ const dir = join(coordRoot, ".harnery", "pid-map");
94
+ if (!existsSync(dir)) return 0;
95
+ let removed = 0;
96
+ let names: string[];
97
+ try {
98
+ names = readdirSync(dir);
99
+ } catch {
100
+ return 0;
101
+ }
102
+ for (const f of names) {
103
+ const pid = Number.parseInt(f, 10);
104
+ // Only touch files whose whole name is a pid, so a stray note or a
105
+ // half-written `.tmp.<pid>` file is left where it is.
106
+ if (!Number.isFinite(pid) || String(pid) !== f) continue;
107
+ let token: string | undefined;
108
+ try {
109
+ token = parsePidmapRow(readFileSync(join(dir, f), "utf8")).startToken;
110
+ } catch {
111
+ /* unreadable row: fall back to the liveness check alone */
112
+ }
113
+ if (rowStillNamesItsProcess(pid, token)) continue;
114
+ try {
115
+ unlinkSync(join(dir, f));
116
+ removed += 1;
117
+ } catch {
118
+ /* best-effort cleanup */
119
+ }
120
+ }
121
+ return removed;
122
+ }
123
+
29
124
  export function writePidmapRow(
30
125
  coordRoot: string,
31
126
  pid: number,
32
127
  instanceId: string,
33
128
  platform: string,
34
129
  ): void {
35
- const path = join(coordRoot, ".harnery", "pid-map", String(pid));
36
- const row = `${instanceId}\t${platform}`;
130
+ const dir = join(coordRoot, ".harnery", "pid-map");
131
+ const path = join(dir, String(pid));
132
+ // Stamped at write time, when the pid provably belongs to the process we
133
+ // mean. A platform that will not say gets no third field rather than an empty
134
+ // one, so an unverifiable row stays byte-identical to what came before.
135
+ const startToken = processStartToken(pid);
136
+ const row = startToken
137
+ ? `${instanceId}\t${platform}\t${startToken}`
138
+ : `${instanceId}\t${platform}`;
37
139
  // Read-then-write idempotency: skip the rename churn when already current.
38
140
  if (existsSync(path)) {
39
141
  try {
@@ -42,10 +144,27 @@ export function writePidmapRow(
42
144
  /* fall through to write */
43
145
  }
44
146
  }
147
+ // Sweep on the way past the threshold. Writes are the only event guaranteed
148
+ // to happen while agents are active, so hanging the sweep here keeps the map
149
+ // bounded without a scheduled job, and the common write stays a single stat.
150
+ try {
151
+ if (existsSync(dir) && readdirSync(dir).length > PRUNE_AT_ROWS) {
152
+ prunePidmapDeadRows(coordRoot);
153
+ }
154
+ } catch {
155
+ /* pruning is best-effort; never fail a write over it */
156
+ }
45
157
  atomicWrite(path, row);
46
158
  }
47
159
 
48
- /** True when any pid-map row for `instanceId` still belongs to a live process. */
160
+ /**
161
+ * True when any pid-map row for `instanceId` still belongs to a live process.
162
+ *
163
+ * Callers spend this answer on consequential things — whether a commit guard
164
+ * treats a heartbeat as a live peer, whether a name can be reclaimed from an
165
+ * abandoned session — so a re-issued pid must not read as that agent still
166
+ * running.
167
+ */
49
168
  export function instanceHasLivePid(coordRoot: string, instanceId: string): boolean {
50
169
  const dir = join(coordRoot, ".harnery", "pid-map");
51
170
  if (!existsSync(dir)) return false;
@@ -56,16 +175,13 @@ export function instanceHasLivePid(coordRoot: string, instanceId: string): boole
56
175
  } catch {
57
176
  continue;
58
177
  }
59
- const owner = row.split("\t")[0]?.trim() ?? "";
178
+ const { instanceId: owner, startToken } = parsePidmapRow(row);
60
179
  if (owner !== instanceId) continue;
61
180
  const pid = Number.parseInt(f, 10);
62
181
  if (!Number.isFinite(pid)) continue;
63
- try {
64
- process.kill(pid, 0); // signal 0 = liveness probe
65
- return true;
66
- } catch {
67
- // ESRCH (no such process): pid-map entry is stale, keep scanning
68
- }
182
+ // Rows that no longer name their process keep the scan going; one that does
183
+ // settles it.
184
+ if (rowStillNamesItsProcess(pid, startToken)) return true;
69
185
  }
70
186
  return false;
71
187
  }
@@ -82,8 +198,7 @@ export function removePidmapRowsForInstance(coordRoot: string, instanceId: strin
82
198
  } catch {
83
199
  continue;
84
200
  }
85
- const owner = row.split("\t")[0]?.trim() ?? "";
86
- if (owner !== instanceId) continue;
201
+ if (parsePidmapRow(row).instanceId !== instanceId) continue;
87
202
  try {
88
203
  unlinkSync(join(dir, f));
89
204
  removed += 1;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Process start-token probe: the "is this still the same process?" half of pid
3
+ * identity.
4
+ *
5
+ * A pid alone does not identify a process. Operating systems hand numbers back
6
+ * out, and faster than intuition suggests: one development machine runs a
7
+ * `pid_max` of 99999 against roughly 100 new processes a second, turning the
8
+ * whole space over about every quarter hour. Anything that remembers a pid for
9
+ * longer than that is remembering a number, not a process.
10
+ *
11
+ * A start token pins the pair down. Two processes can share a pid but never a
12
+ * pid *and* a start instant, so a recorded `(pid, token)` either still names the
13
+ * process that was recorded or plainly does not.
14
+ *
15
+ * The token is opaque on purpose. Callers compare it for equality and nothing
16
+ * else, which is why no clock arithmetic appears here: Linux reports ticks since
17
+ * boot and BSD reports a formatted date, and neither needs converting to be
18
+ * compared with itself. A platform that will not answer returns null, meaning
19
+ * "unverifiable" — callers fall back to a plain liveness check, which is where
20
+ * they were before this existed.
21
+ *
22
+ * Two rules keep comparison honest, and both exist because getting them wrong
23
+ * reports a *false* mismatch, which prunes a live row and hands the identity
24
+ * walk the same wrong answer this module was written to stop:
25
+ *
26
+ * 1. One machine uses one probe. The probe is chosen by capability and locked
27
+ * in, never fallen back from, so a transient read failure cannot answer in
28
+ * the other probe's dialect and read as a recycled pid.
29
+ * 2. A probe's output must not depend on who asked. The `ps` text is formatted
30
+ * through the caller's locale and timezone, so both are pinned; without that,
31
+ * two callers reading the same live process disagree.
32
+ */
33
+
34
+ import { spawnSync } from "node:child_process";
35
+ import { existsSync, readFileSync } from "node:fs";
36
+ import { coordEnv } from "../../../lib/env.ts";
37
+
38
+ /** Which probe answers "when did this process start?" on this machine. */
39
+ export type StartTokenProbe = "procfs" | "ps";
40
+
41
+ /**
42
+ * The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
43
+ * output moves with `TZ` and `LC_TIME`. A hook running under `LC_ALL=C` (git
44
+ * sets it routinely) and a shell running under the user's locale would render
45
+ * the same start instant two different ways and call each other liars. Pinning
46
+ * both makes the rendering a property of the process, not of its reader.
47
+ */
48
+ const PS_ENV_PINS = { TZ: "UTC", LC_ALL: "C" } as const;
49
+
50
+ let cachedProbe: StartTokenProbe | undefined;
51
+ let cachedBootId: string | null | undefined;
52
+
53
+ /**
54
+ * Which probe this machine uses, decided once.
55
+ *
56
+ * `HARNERY_PID_PROBE` forces the choice. That exists so the `ps` path can be
57
+ * exercised on a procfs machine — it is the same code on either OS, and running
58
+ * it only on hardware we happen to own is how a branch rots.
59
+ */
60
+ export function startTokenProbe(): StartTokenProbe {
61
+ const override = coordEnv("PID_PROBE");
62
+ if (override === "procfs" || override === "ps") return override;
63
+ if (cachedProbe === undefined) {
64
+ cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
65
+ }
66
+ return cachedProbe;
67
+ }
68
+
69
+ /**
70
+ * A short, stable identifier for the current boot, or null where there is none.
71
+ *
72
+ * Linux counts a process's start in ticks *since boot*, so the number alone
73
+ * repeats every time the machine restarts. Pid-map rows live in the working
74
+ * tree and outlive reboots, which is the whole exposure: a stale row for pid
75
+ * 1234 at 45.23s after boot matches a fresh process that happens to land on the
76
+ * same pid at the same moment of the next boot, and that false match is exactly
77
+ * the bug the token exists to prevent. Scoping the count to its boot removes
78
+ * the coincidence. BSD needs no equivalent, since its token is an absolute
79
+ * date.
80
+ */
81
+ function bootId(): string | null {
82
+ if (cachedBootId !== undefined) return cachedBootId;
83
+ try {
84
+ const raw = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim().replace(/-/g, "");
85
+ cachedBootId = /^[0-9a-f]{8,}$/.test(raw) ? raw.slice(0, 8) : null;
86
+ } catch {
87
+ cachedBootId = null;
88
+ }
89
+ return cachedBootId;
90
+ }
91
+
92
+ /** Forget the cached probe and boot id. Tests only. */
93
+ export function resetStartTokenCaches(): void {
94
+ cachedProbe = undefined;
95
+ cachedBootId = undefined;
96
+ }
97
+
98
+ /**
99
+ * `l[<boot>.]<ticks since boot>` from `/proc/<pid>/stat`, with no subprocess.
100
+ *
101
+ * The tick count is field 22, counted from the end of the comm field rather
102
+ * than by splitting the whole line, because comm is the executable name in
103
+ * parentheses and may itself contain spaces and parens. The boot segment is
104
+ * dropped rather than faked when the kernel will not name its boot, which
105
+ * yields the pre-boot-id shape that `tokensAgree` still accepts.
106
+ */
107
+ export function procfsStartToken(pid: number): string | null {
108
+ let stat: string;
109
+ try {
110
+ stat = readFileSync(`/proc/${pid}/stat`, "utf8");
111
+ } catch {
112
+ return null; // dead pid, or no procfs — either way, nothing to say
113
+ }
114
+ const afterComm = stat.slice(stat.lastIndexOf(") ") + 2);
115
+ // Fields after comm, 0-based: 0 is state (field 3), so starttime (field 22)
116
+ // sits at 19.
117
+ const ticks = afterComm.split(" ")[19];
118
+ if (!ticks || !/^\d+$/.test(ticks)) return null;
119
+ const boot = bootId();
120
+ return boot ? `l${boot}.${ticks}` : `l${ticks}`;
121
+ }
122
+
123
+ /**
124
+ * `p<start date>` from one `ps -o lstart=`, for machines without procfs.
125
+ *
126
+ * Second resolution is ample: telling two runs of one pid apart would need the
127
+ * entire pid space to recycle inside a second. The runner is injectable so the
128
+ * parsing and the env pinning can be tested without a process to look at.
129
+ */
130
+ export function psStartToken(
131
+ pid: number,
132
+ run: (pid: number) => { status: number | null; stdout: string } = defaultPsRun,
133
+ ): string | null {
134
+ let out: { status: number | null; stdout: string };
135
+ try {
136
+ out = run(pid);
137
+ } catch {
138
+ return null; // no ps on this machine
139
+ }
140
+ if (out.status !== 0) return null;
141
+ const lstart = (out.stdout ?? "").split("\n")[0]?.trim().replace(/\s+/g, " ");
142
+ return lstart ? `p${lstart}` : null;
143
+ }
144
+
145
+ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
146
+ const out = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
147
+ encoding: "utf8",
148
+ timeout: 2000,
149
+ env: { ...process.env, ...PS_ENV_PINS },
150
+ });
151
+ return { status: out.status, stdout: out.stdout ?? "" };
152
+ }
153
+
154
+ /**
155
+ * An opaque token identifying *this run* of `pid`, or null when the platform
156
+ * will not say.
157
+ */
158
+ export function processStartToken(pid: number): string | null {
159
+ if (!Number.isFinite(pid) || pid <= 0) return null;
160
+ return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
161
+ }
162
+
163
+ /**
164
+ * Do two tokens describe the same run?
165
+ *
166
+ * Equality, plus one concession: a row written before tokens carried a boot
167
+ * segment recorded the tick count alone, so it is compared on the part it
168
+ * actually recorded. Refusing that would call every pre-upgrade row a recycled
169
+ * pid and prune a working machine's live rows the first time the new code ran.
170
+ * Such a row loses its reboot protection until the next write rewrites it,
171
+ * which is a strictly better position than the one it came from.
172
+ */
173
+ export function tokensAgree(recorded: string, current: string): boolean {
174
+ if (recorded === current) return true;
175
+ // Only the Linux form has a boot segment to disagree about, and only when
176
+ // exactly one side carries one is this a shape difference rather than two
177
+ // genuinely different start instants.
178
+ if (recorded[0] !== "l" || current[0] !== "l") return false;
179
+ if (recorded.includes(".") === current.includes(".")) return false;
180
+ return ticksOf(recorded) === ticksOf(current);
181
+ }
182
+
183
+ function ticksOf(token: string): string {
184
+ const dot = token.indexOf(".");
185
+ return dot === -1 ? token.slice(1) : token.slice(dot + 1);
186
+ }
187
+
188
+ /**
189
+ * Does `pid` still name the process a row recorded?
190
+ *
191
+ * Three answers, and the difference between the last two matters:
192
+ *
193
+ * - `"match"` — recorded token, current token, same run. The row is trustworthy.
194
+ * - `"mismatch"` — both present and different. The pid was recycled. This is
195
+ * the case a liveness check cannot see, because a recycled pid is alive.
196
+ * - `"unverified"` — no recorded token (a row written before tokens existed) or
197
+ * no current one (an unsupported platform). Callers treat this the way they
198
+ * treated every row previously: trust it if the pid is alive.
199
+ */
200
+ export function checkPidToken(
201
+ pid: number,
202
+ recorded: string | undefined,
203
+ ): "match" | "mismatch" | "unverified" {
204
+ if (!recorded) return "unverified";
205
+ const current = processStartToken(pid);
206
+ if (!current) return "unverified";
207
+ return tokensAgree(recorded, current) ? "match" : "mismatch";
208
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A heuristic parser that extracts file paths likely to be mutated by a
3
3
  * shell command. Used by
4
- * the cursor + codex harness adapters to emit SHELL_CLAIM_CANDIDATE log lines
4
+ * the cursor + codex adapter adapters to emit SHELL_CLAIM_CANDIDATE log lines
5
5
  * (warn-only, never blocks).
6
6
  *
7
7
  * Two cases: