harnery 0.8.0 → 0.10.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 (234) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +8 -0
  4. package/dist/commands/agents.d.ts +24 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +244 -13
  7. package/dist/commands/backup.d.ts +5 -4
  8. package/dist/commands/backup.d.ts.map +1 -1
  9. package/dist/commands/backup.js +15 -14
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +27 -1
  12. package/dist/commands/claude-desktop.d.ts +19 -0
  13. package/dist/commands/claude-desktop.d.ts.map +1 -0
  14. package/dist/commands/claude-desktop.js +168 -0
  15. package/dist/commands/context.d.ts.map +1 -1
  16. package/dist/commands/context.js +149 -1
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +37 -0
  19. package/dist/commands/eml.d.ts +32 -0
  20. package/dist/commands/eml.d.ts.map +1 -1
  21. package/dist/commands/eml.js +16 -3
  22. package/dist/commands/grep.d.ts +35 -2
  23. package/dist/commands/grep.d.ts.map +1 -1
  24. package/dist/commands/grep.js +427 -139
  25. package/dist/commands/harness.d.ts +11 -0
  26. package/dist/commands/harness.d.ts.map +1 -0
  27. package/dist/commands/harness.js +118 -0
  28. package/dist/commands/init.d.ts +15 -5
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +125 -14
  31. package/dist/commands/presence.d.ts +9 -4
  32. package/dist/commands/presence.d.ts.map +1 -1
  33. package/dist/commands/presence.js +88 -5
  34. package/dist/commands/relay.d.ts +9 -0
  35. package/dist/commands/relay.d.ts.map +1 -0
  36. package/dist/commands/relay.js +143 -0
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +5 -0
  39. package/dist/commands/workflow.d.ts +4 -0
  40. package/dist/commands/workflow.d.ts.map +1 -0
  41. package/dist/commands/workflow.js +82 -0
  42. package/dist/core/agents/canonical-emit.d.ts +15 -0
  43. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  44. package/dist/core/agents/canonical-emit.js +41 -3
  45. package/dist/core/agents/cli.js +14 -5
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +44 -3
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +26 -1
  50. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  51. package/dist/core/agents/rules/claim-conflict.js +26 -1
  52. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  53. package/dist/core/agents/rules/commit-conflict.js +4 -37
  54. package/dist/core/agents/rules/stop-hook.d.ts +8 -0
  55. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  56. package/dist/core/agents/rules/stop-hook.js +8 -0
  57. package/dist/core/agents/session-events.d.ts.map +1 -1
  58. package/dist/core/agents/session-events.js +16 -43
  59. package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
  60. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  61. package/dist/core/agents/state/heartbeat-projector.js +24 -4
  62. package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
  63. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  64. package/dist/core/agents/state/heartbeat-writer.js +24 -7
  65. package/dist/core/agents/state/names.d.ts +38 -3
  66. package/dist/core/agents/state/names.d.ts.map +1 -1
  67. package/dist/core/agents/state/names.js +46 -7
  68. package/dist/core/agents/state/pidmap.d.ts +5 -1
  69. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  70. package/dist/core/agents/state/pidmap.js +58 -2
  71. package/dist/core/agents/state/stale-sweep.d.ts +3 -2
  72. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  73. package/dist/core/agents/state/stale-sweep.js +6 -6
  74. package/dist/core/config.d.ts +77 -11
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +210 -25
  77. package/dist/core/context/index.d.ts +144 -0
  78. package/dist/core/context/index.d.ts.map +1 -0
  79. package/dist/core/context/index.js +380 -0
  80. package/dist/core/harnesses/bench.d.ts +29 -0
  81. package/dist/core/harnesses/bench.d.ts.map +1 -0
  82. package/dist/core/harnesses/bench.js +151 -0
  83. package/dist/core/harnesses/index.d.ts +9 -0
  84. package/dist/core/harnesses/index.d.ts.map +1 -0
  85. package/dist/core/harnesses/index.js +4 -0
  86. package/dist/core/harnesses/profiles.d.ts +62 -0
  87. package/dist/core/harnesses/profiles.d.ts.map +1 -0
  88. package/dist/core/harnesses/profiles.js +115 -0
  89. package/dist/core/harnesses/registry.d.ts +15 -0
  90. package/dist/core/harnesses/registry.d.ts.map +1 -0
  91. package/dist/core/harnesses/registry.js +113 -0
  92. package/dist/core/harnesses/types.d.ts +64 -0
  93. package/dist/core/harnesses/types.d.ts.map +1 -0
  94. package/dist/core/harnesses/types.js +20 -0
  95. package/dist/core/hooks/cli.js +309 -66
  96. package/dist/core/hooks/events/schema.d.ts +42 -1
  97. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  98. package/dist/core/hooks/harness/events.d.ts +7 -0
  99. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  100. package/dist/core/hooks/harness/events.js +4 -0
  101. package/dist/core/hooks/harness/parse.d.ts +1 -1
  102. package/dist/core/hooks/harness/parse.d.ts.map +1 -1
  103. package/dist/core/hooks/harness/parse.js +4 -0
  104. package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
  105. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/coord-root.js +28 -6
  107. package/dist/core/presence/blob.d.ts +40 -0
  108. package/dist/core/presence/blob.d.ts.map +1 -0
  109. package/dist/core/presence/blob.js +91 -0
  110. package/dist/core/presence/git.d.ts +68 -0
  111. package/dist/core/presence/git.d.ts.map +1 -0
  112. package/dist/core/presence/git.js +173 -0
  113. package/dist/core/presence/index.d.ts +74 -0
  114. package/dist/core/presence/index.d.ts.map +1 -0
  115. package/dist/core/presence/index.js +224 -0
  116. package/dist/core/presence/relay-client.d.ts +39 -0
  117. package/dist/core/presence/relay-client.d.ts.map +1 -0
  118. package/dist/core/presence/relay-client.js +295 -0
  119. package/dist/core/presence/relay-protocol.d.ts +94 -0
  120. package/dist/core/presence/relay-protocol.d.ts.map +1 -0
  121. package/dist/core/presence/relay-protocol.js +169 -0
  122. package/dist/core/workflow/billing.d.ts +48 -0
  123. package/dist/core/workflow/billing.d.ts.map +1 -0
  124. package/dist/core/workflow/billing.js +108 -0
  125. package/dist/core/workflow/child-env.d.ts +30 -0
  126. package/dist/core/workflow/child-env.d.ts.map +1 -0
  127. package/dist/core/workflow/child-env.js +43 -0
  128. package/dist/core/workflow/engine.d.ts +21 -0
  129. package/dist/core/workflow/engine.d.ts.map +1 -0
  130. package/dist/core/workflow/engine.js +349 -0
  131. package/dist/core/workflow/harnesses.d.ts +17 -0
  132. package/dist/core/workflow/harnesses.d.ts.map +1 -0
  133. package/dist/core/workflow/harnesses.js +21 -0
  134. package/dist/core/workflow/spawn-claude.d.ts +25 -0
  135. package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
  136. package/dist/core/workflow/spawn-claude.js +103 -0
  137. package/dist/core/workflow/spawn-codex.d.ts +22 -0
  138. package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
  139. package/dist/core/workflow/spawn-codex.js +88 -0
  140. package/dist/core/workflow/spawn-cursor.d.ts +29 -0
  141. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
  142. package/dist/core/workflow/spawn-cursor.js +91 -0
  143. package/dist/core/workflow/types.d.ts +152 -0
  144. package/dist/core/workflow/types.d.ts.map +1 -0
  145. package/dist/core/workflow/types.js +9 -0
  146. package/dist/core/workflow/validate.d.ts +15 -0
  147. package/dist/core/workflow/validate.d.ts.map +1 -0
  148. package/dist/core/workflow/validate.js +70 -0
  149. package/dist/lib/browser/client.d.ts +16 -0
  150. package/dist/lib/browser/client.d.ts.map +1 -1
  151. package/dist/lib/browser/client.js +22 -0
  152. package/dist/lib/browser/index.d.ts +1 -0
  153. package/dist/lib/browser/index.d.ts.map +1 -1
  154. package/dist/lib/browser/index.js +1 -0
  155. package/dist/lib/browser/launch-args.d.ts +21 -0
  156. package/dist/lib/browser/launch-args.d.ts.map +1 -0
  157. package/dist/lib/browser/launch-args.js +32 -0
  158. package/dist/lib/claude-desktop.d.ts +105 -0
  159. package/dist/lib/claude-desktop.d.ts.map +1 -0
  160. package/dist/lib/claude-desktop.js +217 -0
  161. package/dist/lib/docs-lint.d.ts.map +1 -1
  162. package/dist/lib/docs-lint.js +5 -2
  163. package/dist/lib/identities/assume.d.ts +51 -0
  164. package/dist/lib/identities/assume.d.ts.map +1 -0
  165. package/dist/lib/identities/assume.js +274 -0
  166. package/dist/lib/identities/index.d.ts +7 -7
  167. package/dist/lib/identities/index.d.ts.map +1 -1
  168. package/dist/lib/identities/index.js +24 -24
  169. package/dist/lib/instructions/templates.d.ts.map +1 -1
  170. package/dist/lib/instructions/templates.js +15 -2
  171. package/package.json +12 -1
  172. package/schemas/config.schema.json +88 -19
  173. package/src/commander.ts +8 -0
  174. package/src/commands/agents.ts +280 -14
  175. package/src/commands/backup.ts +17 -16
  176. package/src/commands/browse.ts +32 -0
  177. package/src/commands/claude-desktop.ts +215 -0
  178. package/src/commands/context.ts +174 -1
  179. package/src/commands/doctor.ts +44 -0
  180. package/src/commands/eml.ts +22 -4
  181. package/src/commands/grep.ts +535 -142
  182. package/src/commands/harness.ts +147 -0
  183. package/src/commands/init.ts +138 -17
  184. package/src/commands/presence.ts +111 -5
  185. package/src/commands/relay.ts +160 -0
  186. package/src/commands/sync.ts +4 -0
  187. package/src/commands/workflow.ts +123 -0
  188. package/src/core/agents/canonical-emit.ts +41 -3
  189. package/src/core/agents/cli.ts +15 -6
  190. package/src/core/agents/render/prompt-context.ts +48 -3
  191. package/src/core/agents/render/session-context.ts +27 -1
  192. package/src/core/agents/rules/claim-conflict.ts +26 -1
  193. package/src/core/agents/rules/commit-conflict.ts +4 -33
  194. package/src/core/agents/rules/stop-hook.ts +17 -0
  195. package/src/core/agents/session-events.ts +15 -39
  196. package/src/core/agents/state/heartbeat-projector.ts +23 -2
  197. package/src/core/agents/state/heartbeat-writer.ts +33 -7
  198. package/src/core/agents/state/names.ts +68 -9
  199. package/src/core/agents/state/pidmap.ts +59 -2
  200. package/src/core/agents/state/stale-sweep.ts +6 -11
  201. package/src/core/config.ts +271 -24
  202. package/src/core/context/index.ts +575 -0
  203. package/src/core/harnesses/bench.ts +214 -0
  204. package/src/core/harnesses/index.ts +31 -0
  205. package/src/core/harnesses/profiles.ts +141 -0
  206. package/src/core/harnesses/registry.ts +140 -0
  207. package/src/core/harnesses/types.ts +90 -0
  208. package/src/core/hooks/cli.ts +361 -65
  209. package/src/core/hooks/events/schema.ts +72 -0
  210. package/src/core/hooks/harness/events.ts +11 -0
  211. package/src/core/hooks/harness/parse.ts +7 -1
  212. package/src/core/hooks/resolve/coord-root.ts +28 -6
  213. package/src/core/presence/blob.ts +125 -0
  214. package/src/core/presence/git.ts +191 -0
  215. package/src/core/presence/index.ts +274 -0
  216. package/src/core/presence/relay-client.ts +322 -0
  217. package/src/core/presence/relay-protocol.ts +246 -0
  218. package/src/core/workflow/billing.ts +152 -0
  219. package/src/core/workflow/child-env.ts +47 -0
  220. package/src/core/workflow/engine.ts +403 -0
  221. package/src/core/workflow/harnesses.ts +33 -0
  222. package/src/core/workflow/spawn-claude.ts +118 -0
  223. package/src/core/workflow/spawn-codex.ts +93 -0
  224. package/src/core/workflow/spawn-cursor.ts +108 -0
  225. package/src/core/workflow/types.ts +160 -0
  226. package/src/core/workflow/validate.ts +75 -0
  227. package/src/lib/browser/client.ts +37 -0
  228. package/src/lib/browser/index.ts +1 -0
  229. package/src/lib/browser/launch-args.ts +33 -0
  230. package/src/lib/claude-desktop.ts +293 -0
  231. package/src/lib/docs-lint.ts +5 -2
  232. package/src/lib/identities/assume.ts +363 -0
  233. package/src/lib/identities/index.ts +28 -24
  234. package/src/lib/instructions/templates.ts +16 -2
@@ -22,8 +22,8 @@
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { existsSync, readdirSync, readFileSync } from "node:fs";
24
24
  import { join } from "node:path";
25
- import { coordEnv } from "../../../lib/env.js";
26
- const DEFAULT_FRESHNESS_SECS = 600;
25
+ import { coordFreshnessSeconds } from "../../config.js";
26
+ import { instanceHasLivePid } from "../state/pidmap.js";
27
27
  export function evaluateCommit(coordRoot, req) {
28
28
  if (req.staged_paths.length === 0) {
29
29
  return {
@@ -36,7 +36,7 @@ export function evaluateCommit(coordRoot, req) {
36
36
  };
37
37
  }
38
38
  const peers = readActivePeers(coordRoot);
39
- const cutoffMs = Date.now() - freshnessSecs() * 1000;
39
+ const cutoffMs = Date.now() - coordFreshnessSeconds(coordRoot) * 1000;
40
40
  const stagedSet = new Set(req.staged_paths);
41
41
  const gitlinkSet = new Set(req.staged_gitlinks ?? []);
42
42
  const conflicts = [];
@@ -166,33 +166,7 @@ function isHolderSelfAttributed(coordRoot, holderId, stagedSet, peers) {
166
166
  return true;
167
167
  }
168
168
  function holderHasLiveForeignPid(coordRoot, holderId) {
169
- const dir = join(coordRoot, ".harnery", "pid-map");
170
- if (!existsSync(dir))
171
- return false;
172
- for (const f of readdirSync(dir)) {
173
- let row = "";
174
- try {
175
- row = readFileSync(join(dir, f), "utf8").trim();
176
- }
177
- catch {
178
- continue;
179
- }
180
- const owner = row.split("\t")[0]?.trim() ?? "";
181
- if (owner !== holderId)
182
- continue;
183
- // Process still alive?
184
- const pid = Number.parseInt(f, 10);
185
- if (!Number.isFinite(pid))
186
- continue;
187
- try {
188
- process.kill(pid, 0); // signal 0 = liveness probe
189
- return true; // live foreign pid found
190
- }
191
- catch {
192
- // ESRCH (no such process): pid-map entry is stale, skip
193
- }
194
- }
195
- return false;
169
+ return instanceHasLivePid(coordRoot, holderId);
196
170
  }
197
171
  function isPathCleanInHead(coordRoot, relPath) {
198
172
  // Path is tracked + diff-clean against HEAD = "already committed, holder
@@ -230,13 +204,6 @@ function readActivePeers(coordRoot) {
230
204
  }
231
205
  return out;
232
206
  }
233
- function freshnessSecs() {
234
- const raw = coordEnv("AGENT_FRESHNESS");
235
- if (!raw)
236
- return DEFAULT_FRESHNESS_SECS;
237
- const n = Number.parseInt(raw, 10);
238
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_FRESHNESS_SECS;
239
- }
240
207
  function shortName(peer) {
241
208
  if (peer.name && peer.name.length > 0)
242
209
  return `agent-${peer.name}`;
@@ -34,6 +34,14 @@ export interface StopHookRequest {
34
34
  };
35
35
  /** Bypass switch: operator escape hatch identical to HARNERY_AGENT_COORD_BYPASS_STOP. */
36
36
  bypass?: boolean;
37
+ /** Headless child spawned by `harn workflow` (HARNERY_WORKFLOW_CHILD=1).
38
+ * The end-of-turn ritual exists to surface status to a HUMAN reader; a
39
+ * workflow child reports to the engine's journal instead, so the ritual is
40
+ * meaningless there — worse, blocking burns the child's turn budget on
41
+ * re-prompts (observed as error_max_turns in the Phase 1 spike). Exempting
42
+ * here, rather than disabling the child's hooks wholesale, keeps heartbeat +
43
+ * event capture on: the child stays visible to peers and the coord layer. */
44
+ workflow_child?: boolean;
37
45
  }
38
46
  /**
39
47
  * Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
@@ -1 +1 @@
1
- {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAcF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CA2FvF"}
1
+ {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAcF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CAoGvF"}
@@ -33,6 +33,14 @@ export function evaluateStopHook(coordRoot, req) {
33
33
  reason: "HARNERY_AGENT_COORD_BYPASS_STOP=1",
34
34
  };
35
35
  }
36
+ if (req.workflow_child) {
37
+ return {
38
+ allow: true,
39
+ exit_code: 0,
40
+ rule: "stop-hook.workflow_child",
41
+ reason: "HARNERY_WORKFLOW_CHILD=1: headless workflow child; ritual not applicable",
42
+ };
43
+ }
36
44
  let events;
37
45
  try {
38
46
  events = readRecentEvents(coordRoot, RECENT_EVENT_WINDOW_LINES);
@@ -1 +1 @@
1
- {"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH;;iFAEiF;AACjF,MAAM,MAAM,gBAAgB,GACxB,eAAe,GACf,QAAQ,GACR,aAAa,GACb,aAAa,GACb,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,cAAc,GACd,aAAa,GACb,WAAW,CAAC;AAEhB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAwB1C;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8BjE;AAsGD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,IAAI,CAEN;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,SAAO,GAAG,MAAM,CAGxD"}
1
+ {"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH;;iFAEiF;AACjF,MAAM,MAAM,gBAAgB,GACxB,eAAe,GACf,QAAQ,GACR,aAAa,GACb,aAAa,GACb,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,cAAc,GACd,aAAa,GACb,WAAW,CAAC;AAEhB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAa1C;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE;AAsGD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,IAAI,CAEN;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,SAAO,GAAG,MAAM,CAGxD"}
@@ -8,10 +8,10 @@
8
8
  * middleware callers need no edits.
9
9
  */
10
10
  import { randomBytes } from "node:crypto";
11
- import { existsSync, readFileSync, statSync } from "node:fs";
11
+ import { existsSync, readFileSync } from "node:fs";
12
12
  import { dirname, resolve } from "node:path";
13
13
  // Kept dependency-light: vendored verbatim into a downstream consumer, so no coordEnv import.
14
- import { normalizeHarness } from "./canonical-emit.js";
14
+ import { normalizeHarness, resolveEmitRoot } from "./canonical-emit.js";
15
15
  import { emit } from "./events/emit.js";
16
16
  /**
17
17
  * Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
@@ -22,27 +22,15 @@ export function sessionEventsPath() {
22
22
  const explicit = process.env.HARNERY_OUTPUT_SESSION_EVENTS;
23
23
  if (explicit)
24
24
  return explicit;
25
- // Walk up looking for an EXISTING .harnery/ directory; only match dirs
26
- // (submodule .git is a file/gitlink, and CLAUDE.md / AGENTS.md can
27
- // proliferate per-subdirectory in many monorepos, so neither is a safe
28
- // anchor). Don't reintroduce CLAUDE.md / .git as anchors, which creates
25
+ // Superproject-aware root resolution (git first, cwd walk fallback) via
26
+ // resolveEmitRoot. A plain cwd walk here mis-anchored to a NESTED
27
+ // `.harnery/` when the shell sat inside a submodule carrying its own
28
+ // (e.g. an embedded harnery checkout), silently splitting the event
29
+ // stream. Don't reintroduce CLAUDE.md / .git as anchors, which creates
29
30
  // stray .harnery/ dirs under submodule subtrees.
30
- let dir = process.cwd();
31
- for (let i = 0; i < 10; i++) {
32
- const candidate = resolve(dir, ".harnery");
33
- try {
34
- if (existsSync(candidate) && statSync(candidate).isDirectory()) {
35
- return resolve(candidate, "session-events.ndjson");
36
- }
37
- }
38
- catch {
39
- /* keep walking */
40
- }
41
- const parent = dirname(dir);
42
- if (parent === dir)
43
- break;
44
- dir = parent;
45
- }
31
+ const root = resolveEmitRoot();
32
+ if (root)
33
+ return resolve(root, ".harnery", "session-events.ndjson");
46
34
  return resolve(process.env.HOME || "/tmp", ".harnery", "session-events.ndjson");
47
35
  }
48
36
  /** Random 8-char hex id for grouping output lines under a single command. */
@@ -60,28 +48,13 @@ export function newCmdId() {
60
48
  export function readLastIntent(instanceId) {
61
49
  if (!instanceId)
62
50
  return null;
63
- // Walk up from cwd looking for .harnery/, same resolution as
64
- // sessionEventsPath() (only match existing directories).
65
- let dir = process.cwd();
66
- let agentsDir = null;
67
- for (let i = 0; i < 10; i++) {
68
- const candidate = resolve(dir, ".harnery");
69
- try {
70
- if (existsSync(candidate) && statSync(candidate).isDirectory()) {
71
- agentsDir = candidate;
72
- break;
73
- }
74
- }
75
- catch {
76
- /* keep walking */
77
- }
78
- const parent = dirname(dir);
79
- if (parent === dir)
80
- break;
81
- dir = parent;
82
- }
83
- if (!agentsDir)
51
+ // Same superproject-aware root resolution as sessionEventsPath(): the
52
+ // intent stamp is written by the PreToolUse hook into the SUPERPROJECT's
53
+ // .harnery/, so a nested-`.harnery/` cwd must not redirect the read.
54
+ const root = resolveEmitRoot();
55
+ if (!root)
84
56
  return null;
57
+ const agentsDir = resolve(root, ".harnery");
85
58
  const intentPath = resolve(agentsDir, `.last-intent.${instanceId}`);
86
59
  if (!existsSync(intentPath))
87
60
  return null;
@@ -20,6 +20,8 @@ export interface V2Heartbeat {
20
20
  platform?: string;
21
21
  subagent_call_id?: string;
22
22
  parent_session_id?: string;
23
+ /** Set iff this owner is a `workflow run` child (joins to the run journal). */
24
+ workflow_run_id?: string;
23
25
  started_at?: string;
24
26
  last_heartbeat: string;
25
27
  last_tool?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,OAAO,EAAE;QACP,cAAc,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoD9D;AAsWD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3E"}
1
+ {"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,OAAO,EAAE;QACP,cAAc,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoD9D;AAyXD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3E"}
@@ -92,8 +92,8 @@ function seed(ev, coordRoot) {
92
92
  if (resolved) {
93
93
  hb.name = resolved.name;
94
94
  hb.kind = resolved.kind;
95
- if (resolved.kind === "subagent")
96
- hb.agent_id = ev.instance_id;
95
+ hb.agent_id =
96
+ resolved.agent_id ?? (resolved.kind === "subagent" ? ev.instance_id : undefined);
97
97
  }
98
98
  }
99
99
  catch {
@@ -138,6 +138,9 @@ function apply(hb, ev, coordRoot) {
138
138
  const parentSession = pickStr(d, "parent_session_id");
139
139
  if (parentSession)
140
140
  hb.parent_session_id = parentSession;
141
+ const workflowRunId = pickStr(d, "workflow_run_id");
142
+ if (workflowRunId)
143
+ hb.workflow_run_id = workflowRunId;
141
144
  if (!hb.files_touched)
142
145
  hb.files_touched = [];
143
146
  }
@@ -202,10 +205,17 @@ function apply(hb, ev, coordRoot) {
202
205
  if (toolName === "Edit" || toolName === "Write" || toolName === "NotebookEdit") {
203
206
  const target = extractFilePath(d);
204
207
  if (target) {
208
+ // Canonicalize to repo-relative before storing: the claim guard
209
+ // writes canonical paths directly, so an absolute entry here would
210
+ // double-count the same file (inflated "N files" display) and
211
+ // defeat exact-match pruning on commit.
212
+ const canonical = target.startsWith(`${coordRoot}/`)
213
+ ? target.slice(coordRoot.length + 1)
214
+ : target;
205
215
  if (!hb.files_touched)
206
216
  hb.files_touched = [];
207
- if (!hb.files_touched.includes(target))
208
- hb.files_touched.push(target);
217
+ if (!hb.files_touched.includes(canonical))
218
+ hb.files_touched.push(canonical);
209
219
  }
210
220
  }
211
221
  break;
@@ -229,6 +239,15 @@ function apply(hb, ev, coordRoot) {
229
239
  case "state.status_checked":
230
240
  hb.last_status_at = ev.ts;
231
241
  break;
242
+ case "identity.assumed": {
243
+ const name = pickStr(d, "name");
244
+ const agentId = pickStr(d, "agent_id");
245
+ if (name)
246
+ hb.name = name;
247
+ if (agentId)
248
+ hb.agent_id = agentId;
249
+ break;
250
+ }
232
251
  case "state.presence_change": {
233
252
  const to = pickStr(d, "to");
234
253
  if (to === "mobile" || to === "office")
@@ -391,6 +410,7 @@ function writeHeartbeat(coordRoot, instanceId, hb) {
391
410
  setIfDefined(merged, "kind", hb.kind);
392
411
  setIfDefined(merged, "agent_id", hb.agent_id);
393
412
  setIfDefined(merged, "subagent_call_id", hb.subagent_call_id);
413
+ setIfDefined(merged, "workflow_run_id", hb.workflow_run_id);
394
414
  setIfDefined(merged, "model", hb.model);
395
415
  setIfDefined(merged, "platform", hb.platform);
396
416
  setIfDefined(merged, "started_at", hb.started_at);
@@ -42,17 +42,31 @@ export declare function setTask(coordRoot: string, instanceId: string, task: str
42
42
  export declare function stampStatusCheck(coordRoot: string, instanceId: string): Heartbeat | null;
43
43
  export declare function setTurnSummary(coordRoot: string, instanceId: string, summary: string): Heartbeat | null;
44
44
  export declare function releaseClaim(coordRoot: string, instanceId: string, path: string): Heartbeat | null;
45
+ /** A heartbeat that actually dropped a path during a group unclaim. */
46
+ export interface GroupUnclaimHit {
47
+ instance_id: string;
48
+ session_id?: string;
49
+ platform?: string;
50
+ }
45
51
  /**
46
52
  * Session-group-wide unclaim. Walks every heartbeat sharing `groupId`
47
53
  * (parent's session_id == group_id;
48
54
  * subagents inherit it) and removes the path from each one's files_touched.
49
- * Idempotent: heartbeats that don't hold the path are untouched.
55
+ * Idempotent: heartbeats that don't hold the path are untouched. Returns the
56
+ * heartbeats that actually dropped the path so the caller can emit the
57
+ * durable `claim.release` events — a file-only prune is silently reverted by
58
+ * the next projector replay.
59
+ *
60
+ * files_touched can hold either absolute-under-coordRoot or canonical
61
+ * repo-relative entries (legacy projections stored the raw tool_input path),
62
+ * so both sides are normalized before comparing — an exact-string match
63
+ * silently no-ops on the mixed-form case and the claim never releases.
50
64
  *
51
65
  * This is the Option B fix for post-commit's pid-map attribution hole: a
52
66
  * subagent-held claim that doesn't live on the parent's heartbeat still gets
53
67
  * pruned because the walk covers the whole group.
54
68
  */
55
- export declare function groupUnclaim(coordRoot: string, groupId: string, path: string): void;
69
+ export declare function groupUnclaim(coordRoot: string, groupId: string, path: string): GroupUnclaimHit[];
56
70
  export declare function killHeartbeat(coordRoot: string, instanceId: string): boolean;
57
71
  export declare function healPidmap(coordRoot: string, instanceId: string, pid: number): void;
58
72
  export declare function healHeartbeat(coordRoot: string, instanceId: string, sessionId?: string, model?: string, harness?: string): Heartbeat | null;
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA6DH,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAiBD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQrF;AAeD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAS7F;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKxF;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,SAAS,GAAG,IAAI,CAMlB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAYlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA4BnF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAS5E;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAuBnF;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,GAAG,IAAI,CAqDlB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,SAAS,GAAG,IAAI,CAOlB"}
1
+ {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA6DH,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAiBD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQrF;AAeD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAS7F;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKxF;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,SAAS,GAAG,IAAI,CAMlB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAYlB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE,CAsChG;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAS5E;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAuBnF;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,GAAG,IAAI,CAsDlB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,SAAS,GAAG,IAAI,CAOlB"}
@@ -129,18 +129,29 @@ export function releaseClaim(coordRoot, instanceId, path) {
129
129
  * Session-group-wide unclaim. Walks every heartbeat sharing `groupId`
130
130
  * (parent's session_id == group_id;
131
131
  * subagents inherit it) and removes the path from each one's files_touched.
132
- * Idempotent: heartbeats that don't hold the path are untouched.
132
+ * Idempotent: heartbeats that don't hold the path are untouched. Returns the
133
+ * heartbeats that actually dropped the path so the caller can emit the
134
+ * durable `claim.release` events — a file-only prune is silently reverted by
135
+ * the next projector replay.
136
+ *
137
+ * files_touched can hold either absolute-under-coordRoot or canonical
138
+ * repo-relative entries (legacy projections stored the raw tool_input path),
139
+ * so both sides are normalized before comparing — an exact-string match
140
+ * silently no-ops on the mixed-form case and the claim never releases.
133
141
  *
134
142
  * This is the Option B fix for post-commit's pid-map attribution hole: a
135
143
  * subagent-held claim that doesn't live on the parent's heartbeat still gets
136
144
  * pruned because the walk covers the whole group.
137
145
  */
138
146
  export function groupUnclaim(coordRoot, groupId, path) {
147
+ const hits = [];
139
148
  if (!groupId || !path)
140
- return;
149
+ return hits;
141
150
  const activeDir = join(coordRoot, ".harnery", "active");
142
151
  if (!existsSync(activeDir))
143
- return;
152
+ return hits;
153
+ const norm = (p) => p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
154
+ const target = norm(path);
144
155
  for (const f of readdirSync(activeDir)) {
145
156
  if (!f.endsWith(".json"))
146
157
  continue;
@@ -156,7 +167,7 @@ export function groupUnclaim(coordRoot, groupId, path) {
156
167
  if (peerSession !== groupId)
157
168
  continue;
158
169
  const files = body.files_touched ?? [];
159
- const next = files.filter((p) => p !== path);
170
+ const next = files.filter((p) => norm(p) !== target);
160
171
  if (next.length === files.length)
161
172
  continue;
162
173
  body.files_touched = next;
@@ -164,11 +175,17 @@ export function groupUnclaim(coordRoot, groupId, path) {
164
175
  const tmp = `${hbPath}.tmp.${process.pid}`;
165
176
  writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
166
177
  renameSync(tmp, hbPath);
178
+ hits.push({
179
+ instance_id: body.instance_id ?? f.replace(/\.json$/, ""),
180
+ session_id: body.session_id,
181
+ platform: body.platform,
182
+ });
167
183
  }
168
184
  catch {
169
185
  /* silent */
170
186
  }
171
187
  }
188
+ return hits;
172
189
  }
173
190
  export function killHeartbeat(coordRoot, instanceId) {
174
191
  const path = heartbeatPath(coordRoot, instanceId);
@@ -228,9 +245,9 @@ export function healHeartbeat(coordRoot, instanceId, sessionId, model, harness)
228
245
  if (resolved) {
229
246
  name = resolved.name;
230
247
  kind = resolved.kind;
231
- // subagent kind: agent_id == instance_id. Else (session/transient): no id stamp.
232
- if (resolved.kind === "subagent")
233
- agentId = instanceId;
248
+ // An explicitly assumed session carries its durable persona UUID in
249
+ // name-history. Native subagents continue to use instance_id.
250
+ agentId = resolved.agent_id ?? (resolved.kind === "subagent" ? instanceId : "");
234
251
  }
235
252
  }
236
253
  catch {
@@ -8,14 +8,30 @@
8
8
  * Durable persistence: `.harnery/.name-history` (JSONL, one row per assignment)
9
9
  * + `.harnery/.name-counter` (current counter, atomic temp+rename).
10
10
  *
11
- * Recreation rule (mirrors v1):
12
- * 1. Own instance_id in name-history → (original name, original kind)
13
- * 2. session_id in name-history (owner != session) → (parent's name, "transient")
11
+ * Recreation rule:
12
+ * 1. Own instance_id in name-history → latest (name, kind, agent_id)
13
+ * 2. session_id in name-history (owner != session) → latest parent's
14
+ * (name, agent_id) with kind="transient"
15
+ *
16
+ * Multiple rows for one instance are intentional. `identity assume` appends a
17
+ * new binding instead of rewriting history; readers therefore resolve from the
18
+ * end of the file. Older one-row histories retain their original behavior.
14
19
  * 3. Else: new assignment, consume a counter slot.
15
20
  */
16
21
  /** All 260 names. */
17
22
  export declare const COORD_NAMES: readonly ["Anna", "Bob", "Carlos", "Dalia", "Erika", "Fred", "Gemma", "Hank", "Imelda", "Jerry", "Kirk", "Lee", "Milton", "Nate", "Oscar", "Patty", "Quentin", "Rafael", "Sara", "Tony", "Ulrik", "Valerie", "Whitney", "Xander", "Yara", "Zara", "Adelaide", "Bertha", "Carmen", "Dorian", "Edna", "Francine", "Greta", "Helene", "Ines", "Joyce", "Karen", "Lorenzo", "Mindy", "Nadine", "Odette", "Paulette", "Quinn", "Rebekah", "Sean", "Tammy", "Ursula", "Vinson", "Wallace", "Xanthe", "Yusuf", "Zoe", "Alex", "Beatrice", "Cyrus", "Delia", "Ernesto", "Florence", "Gordon", "Hester", "Isaias", "Jenna", "Klaus", "Larry", "Maxine", "Nicholas", "Otto", "Peter", "Quill", "Renee", "Stella", "Theresa", "Umberto", "Virgil", "Willow", "Xavier", "Yolanda", "Zane", "Anita", "Bruno", "Cora", "Damon", "Edith", "Felix", "Gloria", "Holly", "Iris", "Jasper", "Kaia", "Linda", "Marco", "Nora", "Olive", "Petra", "Querida", "Roman", "Sebastian", "Tobias", "Una", "Vera", "Walter", "Xena", "Yusra", "Zelda", "Aaron", "Bonnie", "Caleb", "Daphne", "Elias", "Fiona", "Galileo", "Hugo", "Ian", "Jude", "Knox", "Leo", "Maya", "Nash", "Owen", "Paxton", "Quetzal", "Rosa", "Sage", "Talia", "Uri", "Vincent", "Wesley", "Ximena", "Yvette", "Zephyr", "Astrid", "Beau", "Celeste", "Dexter", "Ezra", "Fitz", "Genevieve", "Hadley", "Imogen", "Joaquin", "Kira", "Lila", "Mason", "Nila", "Olga", "Pearl", "Quincy", "Royce", "Sterling", "Trent", "Ulysses", "Violet", "Wynne", "Xerxes", "Yael", "Zinnia", "Amelia", "Bianca", "Cody", "Dahlia", "Esme", "Fern", "Greer", "Harriet", "Ivan", "Juno", "Kendrick", "Luther", "Margot", "Nigel", "Orion", "Phillip", "Quenby", "Reagan", "Sienna", "Tessa", "Uma", "Vesper", "Watson", "Xiomara", "Yuri", "Zoltan", "Andre", "Boris", "Cassidy", "Davis", "Evander", "Forrest", "Gibson", "Henry", "Irene", "Justus", "Kasper", "Lainey", "Miles", "Naomi", "Ophelia", "Phoebe", "Quark", "Rylie", "Saul", "Truman", "Ulrich", "Voss", "Winifred", "Xuxa", "Yvonne", "Zia", "Aria", "Brenda", "Crispin", "Dolores", "Estelle", "Felicity", "Goldie", "Hazel", "Ira", "Jovi", "Kestrel", "Lyric", "Mavis", "Nico", "Otis", "Percy", "Querubin", "Rhett", "Scout", "Tatum", "Unity", "Vivian", "Wyatt", "Xan", "Yancy", "Zev", "Atticus", "Barnaby", "Calliope", "Drake", "Eustace", "Foster", "Granger", "Hollis", "Indira", "Jericho", "Kaleb", "Logan", "Magnus", "Noor", "Oakley", "Posy", "Quito", "Rosalind", "Sloane", "Theron", "Upton", "Vance", "Wendell", "Xola", "Yann", "Zora"];
18
23
  export type NameKind = "session" | "subagent" | "transient";
24
+ export interface NameHistoryRow {
25
+ instance_id: string;
26
+ name: string;
27
+ kind: NameKind;
28
+ ts: string;
29
+ /** Durable persona UUID. Present after `agents identity assume`. */
30
+ agent_id?: string;
31
+ /** Audit marker distinguishing an explicit role adoption from pool assignment. */
32
+ source?: "pool" | "identity.assume";
33
+ previous_name?: string;
34
+ }
19
35
  /**
20
36
  * Resolve (name, kind) for an existing owner without consuming a counter
21
37
  * slot. Returns null if no history match found.
@@ -26,7 +42,26 @@ export type NameKind = "session" | "subagent" | "transient";
26
42
  export declare function resolveName(coordRoot: string, instanceId: string, sessionId?: string): {
27
43
  name: string;
28
44
  kind: NameKind;
45
+ agent_id?: string;
29
46
  } | null;
47
+ export interface NameAssumptionResult {
48
+ changed: boolean;
49
+ previous: {
50
+ name: string;
51
+ kind: NameKind;
52
+ agent_id?: string;
53
+ } | null;
54
+ current: {
55
+ name: string;
56
+ kind: NameKind;
57
+ agent_id: string;
58
+ };
59
+ }
60
+ /**
61
+ * Append an explicit instance → durable-persona binding. Latest-row-wins makes
62
+ * this auditable and retry-safe without mutating prior assignment history.
63
+ */
64
+ export declare function recordNameAssumption(coordRoot: string, instanceId: string, name: string, agentId: string, kind?: NameKind): NameAssumptionResult;
30
65
  /**
31
66
  * Assign a name to <instanceId> with the given <kind>. Counter-consuming when
32
67
  * the owner is new. Idempotent: returns existing name on resume.
@@ -1 +1 @@
1
- {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/names.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,qBAAqB;AACrB,eAAO,MAAM,WAAW,y5EA+Qd,CAAC;AAMX,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;AA6C5D;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,IAAI,CAkBzC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAqBxF"}
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/names.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,qBAAqB;AACrB,eAAO,MAAM,WAAW,y5EA+Qd,CAAC;AAMX,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsCD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA4B5D;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,QAAoB,GACzB,oBAAoB,CAoBtB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAsBxF"}
@@ -8,9 +8,14 @@
8
8
  * Durable persistence: `.harnery/.name-history` (JSONL, one row per assignment)
9
9
  * + `.harnery/.name-counter` (current counter, atomic temp+rename).
10
10
  *
11
- * Recreation rule (mirrors v1):
12
- * 1. Own instance_id in name-history → (original name, original kind)
13
- * 2. session_id in name-history (owner != session) → (parent's name, "transient")
11
+ * Recreation rule:
12
+ * 1. Own instance_id in name-history → latest (name, kind, agent_id)
13
+ * 2. session_id in name-history (owner != session) → latest parent's
14
+ * (name, agent_id) with kind="transient"
15
+ *
16
+ * Multiple rows for one instance are intentional. `identity assume` appends a
17
+ * new binding instead of rewriting history; readers therefore resolve from the
18
+ * end of the file. Older one-row histories retain their original behavior.
14
19
  * 3. Else: new assignment, consume a counter slot.
15
20
  */
16
21
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -333,20 +338,53 @@ function appendHistory(coordRoot, row) {
333
338
  */
334
339
  export function resolveName(coordRoot, instanceId, sessionId) {
335
340
  const history = readHistory(coordRoot);
336
- for (const row of history) {
341
+ for (let i = history.length - 1; i >= 0; i--) {
342
+ const row = history[i];
337
343
  if (row.instance_id === instanceId) {
338
- return { name: row.name, kind: row.kind };
344
+ return {
345
+ name: row.name,
346
+ kind: row.kind,
347
+ ...(row.agent_id ? { agent_id: row.agent_id } : {}),
348
+ };
339
349
  }
340
350
  }
341
351
  if (sessionId && sessionId !== instanceId) {
342
- for (const row of history) {
352
+ for (let i = history.length - 1; i >= 0; i--) {
353
+ const row = history[i];
343
354
  if (row.instance_id === sessionId) {
344
- return { name: row.name, kind: "transient" };
355
+ return {
356
+ name: row.name,
357
+ kind: "transient",
358
+ ...(row.agent_id ? { agent_id: row.agent_id } : {}),
359
+ };
345
360
  }
346
361
  }
347
362
  }
348
363
  return null;
349
364
  }
365
+ /**
366
+ * Append an explicit instance → durable-persona binding. Latest-row-wins makes
367
+ * this auditable and retry-safe without mutating prior assignment history.
368
+ */
369
+ export function recordNameAssumption(coordRoot, instanceId, name, agentId, kind = "session") {
370
+ const previous = resolveName(coordRoot, instanceId);
371
+ const current = { name, kind, agent_id: agentId };
372
+ if (previous?.name === current.name &&
373
+ previous.kind === current.kind &&
374
+ previous.agent_id === current.agent_id) {
375
+ return { changed: false, previous, current };
376
+ }
377
+ appendHistory(coordRoot, {
378
+ instance_id: instanceId,
379
+ name,
380
+ kind,
381
+ agent_id: agentId,
382
+ source: "identity.assume",
383
+ previous_name: previous?.name,
384
+ ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
385
+ });
386
+ return { changed: true, previous, current };
387
+ }
350
388
  /**
351
389
  * Assign a name to <instanceId> with the given <kind>. Counter-consuming when
352
390
  * the owner is new. Idempotent: returns existing name on resume.
@@ -370,6 +408,7 @@ export function assignName(coordRoot, instanceId, kind) {
370
408
  instance_id: instanceId,
371
409
  name,
372
410
  kind,
411
+ source: "pool",
373
412
  ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
374
413
  });
375
414
  return name;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pid-map writer.
2
+ * Pid-map writer + liveness helpers.
3
3
  *
4
4
  * Per-harness pid-map at `.harnery/pid-map/<pid>` containing
5
5
  * `<instance_id>\t<platform>`. `harn agents whoami` walks ppid up 20 hops looking
@@ -8,4 +8,8 @@
8
8
  * Atomic temp+rename. Idempotent: re-writing the same row is a no-op.
9
9
  */
10
10
  export declare function writePidmapRow(coordRoot: string, pid: number, instanceId: string, platform: string): void;
11
+ /** True when any pid-map row for `instanceId` still belongs to a live process. */
12
+ export declare function instanceHasLivePid(coordRoot: string, instanceId: string): boolean;
13
+ /** Drop every pid-map row owned by `instanceId`. Returns how many files were removed. */
14
+ export declare function removePidmapRowsForInstance(coordRoot: string, instanceId: string): number;
11
15
  //# sourceMappingURL=pidmap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pidmap.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/pidmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,IAAI,CAYN"}
1
+ {"version":3,"file":"pidmap.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/pidmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,IAAI,CAYN;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAsBjF;AAED,yFAAyF;AACzF,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAqBzF"}