harnery 0.31.6 → 0.32.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 (218) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +0 -9
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +70 -95
  6. package/dist/commands/browse-session.d.ts +21 -0
  7. package/dist/commands/browse-session.d.ts.map +1 -0
  8. package/dist/commands/browse-session.js +157 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +205 -37
  11. package/dist/commands/checkpoint.js +1 -1
  12. package/dist/commands/deinit.d.ts.map +1 -1
  13. package/dist/commands/deinit.js +4 -0
  14. package/dist/commands/docs.d.ts.map +1 -1
  15. package/dist/commands/docs.js +40 -0
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +100 -17
  18. package/dist/commands/init.d.ts +1 -2
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +16 -5
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +2 -3
  23. package/dist/core/agents/canonical-emit.d.ts +1 -2
  24. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  25. package/dist/core/agents/canonical-emit.js +1 -2
  26. package/dist/core/agents/cli.js +123 -88
  27. package/dist/core/agents/coord-client.d.ts +7 -3
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +5 -4
  30. package/dist/core/agents/finalization.d.ts +68 -0
  31. package/dist/core/agents/finalization.d.ts.map +1 -0
  32. package/dist/core/agents/finalization.js +443 -0
  33. package/dist/core/agents/git-hook.d.ts +51 -0
  34. package/dist/core/agents/git-hook.d.ts.map +1 -0
  35. package/dist/core/agents/git-hook.js +118 -0
  36. package/dist/core/agents/render/prompt-context.d.ts +6 -5
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +26 -13
  39. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/session-context.js +15 -2
  41. package/dist/core/agents/rules/claim-conflict.js +3 -3
  42. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +21 -5
  45. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  46. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  47. package/dist/core/agents/rules/stop-hook.js +59 -23
  48. package/dist/core/agents/session-events.d.ts +8 -16
  49. package/dist/core/agents/session-events.d.ts.map +1 -1
  50. package/dist/core/agents/session-events.js +12 -26
  51. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
  52. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  53. package/dist/core/agents/state/heartbeat-projector.js +44 -1
  54. package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
  55. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/heartbeat-writer.js +54 -17
  57. package/dist/core/agents/state/names.d.ts +22 -1
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +40 -2
  60. package/dist/core/config.d.ts +30 -2
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +74 -11
  63. package/dist/core/governor/planning.d.ts.map +1 -1
  64. package/dist/core/governor/planning.js +12 -13
  65. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  66. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  67. package/dist/core/hooks/adapter/detect.js +4 -14
  68. package/dist/core/hooks/adapter/output.d.ts +1 -1
  69. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  70. package/dist/core/hooks/adapter/output.js +5 -4
  71. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +32 -0
  74. package/dist/core/hooks/cli.d.ts +1 -2
  75. package/dist/core/hooks/cli.d.ts.map +1 -1
  76. package/dist/core/hooks/cli.js +169 -27
  77. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  78. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  79. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  80. package/dist/core/hooks/effects/index.d.ts +1 -1
  81. package/dist/core/hooks/effects/index.js +3 -3
  82. package/dist/core/hooks/events/schema.d.ts +27 -0
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  85. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  86. package/dist/core/hooks/resolve/owner.js +15 -0
  87. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  88. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/transcript.js +176 -1
  90. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  91. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  92. package/dist/core/hooks/session-name-presence.js +43 -0
  93. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  94. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  95. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  96. package/dist/core/work/state.d.ts +4 -5
  97. package/dist/core/work/state.d.ts.map +1 -1
  98. package/dist/core/work/state.js +5 -8
  99. package/dist/core/workflow/index.d.ts +1 -1
  100. package/dist/core/workflow/index.d.ts.map +1 -1
  101. package/dist/core/workflow/proof.d.ts +2 -2
  102. package/dist/core/workflow/proof.d.ts.map +1 -1
  103. package/dist/core/workflow/proof.js +2 -2
  104. package/dist/core/workflow/run-state.d.ts +2 -2
  105. package/dist/core/workflow/run-state.d.ts.map +1 -1
  106. package/dist/core/workflow/run-state.js +2 -2
  107. package/dist/core/workflow/types.d.ts +3 -3
  108. package/dist/core/workflow/types.d.ts.map +1 -1
  109. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  110. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  111. package/dist/core/workflow/workspaces/execution.js +5 -0
  112. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  113. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  115. package/dist/core/workflow/workspaces/local-git.js +1 -2
  116. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  117. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  118. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  119. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  120. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  121. package/dist/core/workflow/workspaces/validate.js +0 -2
  122. package/dist/lib/browser/client.d.ts +118 -1
  123. package/dist/lib/browser/client.d.ts.map +1 -1
  124. package/dist/lib/browser/client.js +435 -6
  125. package/dist/lib/browser/geometry.d.ts.map +1 -1
  126. package/dist/lib/browser/geometry.js +180 -37
  127. package/dist/lib/browser/index.d.ts +5 -1
  128. package/dist/lib/browser/index.d.ts.map +1 -1
  129. package/dist/lib/browser/index.js +5 -1
  130. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  131. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  132. package/dist/lib/browser/netscape-cookies.js +31 -0
  133. package/dist/lib/browser/proxy.d.ts +24 -0
  134. package/dist/lib/browser/proxy.d.ts.map +1 -0
  135. package/dist/lib/browser/proxy.js +84 -0
  136. package/dist/lib/browser/runts.d.ts +6 -0
  137. package/dist/lib/browser/runts.d.ts.map +1 -1
  138. package/dist/lib/browser/runts.js +21 -4
  139. package/dist/lib/browser/session-control.d.ts +112 -0
  140. package/dist/lib/browser/session-control.d.ts.map +1 -0
  141. package/dist/lib/browser/session-control.js +670 -0
  142. package/dist/lib/docs-links.d.ts +108 -0
  143. package/dist/lib/docs-links.d.ts.map +1 -0
  144. package/dist/lib/docs-links.js +555 -0
  145. package/dist/lib/exec.d.ts +1 -1
  146. package/dist/lib/exec.js +1 -1
  147. package/dist/lib/identities/assume.d.ts +4 -2
  148. package/dist/lib/identities/assume.d.ts.map +1 -1
  149. package/dist/lib/identities/assume.js +15 -2
  150. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  151. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  152. package/dist/lib/instructions/git-hooks.js +238 -0
  153. package/dist/lib/instructions/splice.d.ts +10 -4
  154. package/dist/lib/instructions/splice.d.ts.map +1 -1
  155. package/dist/lib/instructions/splice.js +19 -11
  156. package/package.json +1 -1
  157. package/schemas/config.schema.json +32 -0
  158. package/src/commander.ts +2 -0
  159. package/src/commands/agents.ts +96 -115
  160. package/src/commands/browse-session.ts +245 -0
  161. package/src/commands/browse.ts +287 -44
  162. package/src/commands/checkpoint.ts +1 -1
  163. package/src/commands/deinit.ts +7 -1
  164. package/src/commands/docs.ts +56 -0
  165. package/src/commands/doctor.ts +100 -18
  166. package/src/commands/init.ts +17 -9
  167. package/src/commands/tunnel.ts +2 -5
  168. package/src/core/agents/canonical-emit.ts +1 -2
  169. package/src/core/agents/cli.ts +137 -88
  170. package/src/core/agents/coord-client.ts +10 -5
  171. package/src/core/agents/finalization.ts +595 -0
  172. package/src/core/agents/git-hook.ts +126 -0
  173. package/src/core/agents/render/prompt-context.ts +28 -13
  174. package/src/core/agents/render/session-context.ts +15 -2
  175. package/src/core/agents/rules/claim-conflict.ts +3 -3
  176. package/src/core/agents/rules/commit-conflict.ts +35 -8
  177. package/src/core/agents/rules/stop-hook.ts +71 -23
  178. package/src/core/agents/session-events.ts +14 -41
  179. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  180. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  181. package/src/core/agents/state/names.ts +54 -2
  182. package/src/core/config.ts +92 -12
  183. package/src/core/governor/planning.ts +17 -12
  184. package/src/core/hooks/adapter/detect.ts +4 -13
  185. package/src/core/hooks/adapter/output.ts +9 -4
  186. package/src/core/hooks/adapter/wiring.ts +43 -0
  187. package/src/core/hooks/cli.ts +201 -23
  188. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  189. package/src/core/hooks/effects/index.ts +5 -5
  190. package/src/core/hooks/events/schema.ts +27 -0
  191. package/src/core/hooks/resolve/owner.ts +22 -1
  192. package/src/core/hooks/resolve/transcript.ts +164 -1
  193. package/src/core/hooks/session-name-presence.ts +52 -0
  194. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  195. package/src/core/work/state.ts +8 -12
  196. package/src/core/workflow/engine.ts +2 -2
  197. package/src/core/workflow/index.ts +0 -1
  198. package/src/core/workflow/proof.ts +4 -4
  199. package/src/core/workflow/run-state.ts +5 -5
  200. package/src/core/workflow/types.ts +3 -3
  201. package/src/core/workflow/workspaces/execution.ts +11 -4
  202. package/src/core/workflow/workspaces/index.ts +0 -3
  203. package/src/core/workflow/workspaces/local-git.ts +1 -2
  204. package/src/core/workflow/workspaces/paths.ts +2 -2
  205. package/src/core/workflow/workspaces/types.ts +0 -9
  206. package/src/core/workflow/workspaces/validate.ts +1 -5
  207. package/src/lib/browser/client.ts +528 -6
  208. package/src/lib/browser/geometry.ts +197 -37
  209. package/src/lib/browser/index.ts +39 -0
  210. package/src/lib/browser/netscape-cookies.ts +39 -0
  211. package/src/lib/browser/proxy.ts +105 -0
  212. package/src/lib/browser/runts.ts +27 -3
  213. package/src/lib/browser/session-control.ts +892 -0
  214. package/src/lib/docs-links.ts +674 -0
  215. package/src/lib/exec.ts +1 -1
  216. package/src/lib/identities/assume.ts +22 -1
  217. package/src/lib/instructions/git-hooks.ts +259 -0
  218. package/src/lib/instructions/splice.ts +41 -11
@@ -1,21 +1,17 @@
1
1
  /**
2
2
  * Command/narration event emitter for the coordination layer.
3
3
  *
4
- * `writeSessionEvent` emits command + narration events straight to the
4
+ * `writeSessionEvent` emits command and narration events straight to the
5
5
  * **canonical** `.harnery/events.ndjson` (alongside the hook events), which the
6
- * `/live` web viewer reads. The exported surface (`writeSessionEvent`,
7
- * `newCmdId`, `clampField`, `readLastIntent`) is stable so the session-tee
8
- * middleware callers need no edits.
6
+ * `/live` web viewer reads.
9
7
  */
10
- /** Event types accepted by `writeSessionEvent`. Only the command stream +
11
- * narration are emitted canonically; the coord/state types are accepted for
12
- * call-site compatibility but are no-ops (the agents CLI emits those itself). */
13
- export type SessionEventType = "command_start" | "output" | "command_end" | "end_of_turn" | "hook_event" | "set_task" | "file_claim" | "file_release" | "peer_change" | "narration";
8
+ /** Event types accepted by `writeSessionEvent`. */
9
+ export type SessionEventType = "command_start" | "output" | "command_end" | "narration";
14
10
  /**
15
11
  * Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
16
12
  * containerized reader can pick it up through a single bind mount.
17
13
  */
18
- export declare function sessionEventsPath(): string;
14
+ export declare function canonicalEventsPath(): string;
19
15
  /** Random 8-char hex id for grouping output lines under a single command. */
20
16
  export declare function newCmdId(): string;
21
17
  /**
@@ -28,14 +24,10 @@ export declare function newCmdId(): string;
28
24
  */
29
25
  export declare function readLastIntent(instanceId?: string): string | null;
30
26
  /**
31
- * Emit a session event. Command + narration events
32
- * are written to the canonical `.harnery/events.ndjson`; the coord/state types
33
- * are accepted for call-site compatibility but are no-ops here (the agents CLI
34
- * emits those itself). Best-effort, never throws into the caller; a command
35
- * must never break or slow on telemetry. The `agentName` arg is retained for
36
- * the stable call signature (canonical events key on instance_id, not name).
27
+ * Emit a command or narration event to `.harnery/events.ndjson`. Best-effort,
28
+ * never throws into the caller; telemetry must not break or slow a command.
37
29
  */
38
- export declare function writeSessionEvent(type: SessionEventType, _agentName: string, fields?: Record<string, unknown>): void;
30
+ export declare function writeSessionEvent(type: SessionEventType, fields?: Record<string, unknown>): void;
39
31
  /** Trim long values to keep individual events small. */
40
32
  export declare function clampField(v: string, max?: number): string;
41
33
  //# sourceMappingURL=session-events.d.ts.map
@@ -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,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"}
1
+ {"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,mDAAmD;AACnD,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,QAAQ,GAAG,aAAa,GAAG,WAAW,CAAC;AAExF;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAa5C;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE;AA8FD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,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,11 +1,9 @@
1
1
  /**
2
2
  * Command/narration event emitter for the coordination layer.
3
3
  *
4
- * `writeSessionEvent` emits command + narration events straight to the
4
+ * `writeSessionEvent` emits command and narration events straight to the
5
5
  * **canonical** `.harnery/events.ndjson` (alongside the hook events), which the
6
- * `/live` web viewer reads. The exported surface (`writeSessionEvent`,
7
- * `newCmdId`, `clampField`, `readLastIntent`) is stable so the session-tee
8
- * middleware callers need no edits.
6
+ * `/live` web viewer reads.
9
7
  */
10
8
  import { randomBytes } from "node:crypto";
11
9
  import { existsSync, readFileSync } from "node:fs";
@@ -17,9 +15,9 @@ import { emit } from "./events/emit.js";
17
15
  * Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
18
16
  * containerized reader can pick it up through a single bind mount.
19
17
  */
20
- export function sessionEventsPath() {
18
+ export function canonicalEventsPath() {
21
19
  // Explicit override (tests + non-monorepo invocations).
22
- const explicit = process.env.HARNERY_OUTPUT_SESSION_EVENTS;
20
+ const explicit = process.env.HARNERY_EVENTS_PATH;
23
21
  if (explicit)
24
22
  return explicit;
25
23
  // Superproject-aware root resolution (git first, cwd walk fallback) via
@@ -48,7 +46,7 @@ export function newCmdId() {
48
46
  export function readLastIntent(instanceId) {
49
47
  if (!instanceId)
50
48
  return null;
51
- // Same superproject-aware root resolution as sessionEventsPath(): the
49
+ // Same superproject-aware root resolution as canonicalEventsPath(): the
52
50
  // intent stamp is written by the PreToolUse hook into the SUPERPROJECT's
53
51
  // .harnery/, so a nested-`.harnery/` cwd must not redirect the read.
54
52
  const root = resolveEmitRoot();
@@ -68,14 +66,6 @@ export function readLastIntent(instanceId) {
68
66
  return null;
69
67
  }
70
68
  }
71
- /**
72
- * Dual-write: mirror command/narration session-events
73
- * into the canonical `.harnery/events.ndjson` stream so the legacy
74
- * session-events.ndjson writer + its web consumers can be retired.
75
- * Only the command-stream + narration types migrate; the coord/state types
76
- * (`set_task`, `file_claim`, `peer_change`, …) are already emitted canonically
77
- * by the agents CLI, so re-emitting them here would double-count.
78
- */
79
69
  const CANONICAL_TYPE = {
80
70
  command_start: "command.start",
81
71
  output: "command.output",
@@ -110,7 +100,7 @@ function enrichFromHeartbeat(coordRoot, instanceId) {
110
100
  return null;
111
101
  }
112
102
  }
113
- /** Project the flat legacy `fields` into the canonical event's `data` shape.
103
+ /** Project the flat middleware `fields` into the canonical event's `data` shape.
114
104
  * Unknown types never reach here, guarded by CANONICAL_TYPE. */
115
105
  function canonicalData(type, fields) {
116
106
  switch (type) {
@@ -133,7 +123,7 @@ function canonicalData(type, fields) {
133
123
  }
134
124
  /** Emit a command/narration event to the canonical stream. Swallows every
135
125
  * error and skips when identity can't be resolved: telemetry must never break
136
- * (or slow down) a command. Non-command types return early. */
126
+ * or slow down a command. */
137
127
  function emitCanonicalCommand(type, fields) {
138
128
  const eventType = CANONICAL_TYPE[type];
139
129
  if (!eventType)
@@ -142,8 +132,8 @@ function emitCanonicalCommand(type, fields) {
142
132
  if (!instanceId)
143
133
  return;
144
134
  try {
145
- // coordRoot = the dir containing `.harnery/`; sessionEventsPath() anchors it.
146
- const coordRoot = dirname(dirname(sessionEventsPath()));
135
+ // coordRoot = the dir containing `.harnery/`; canonicalEventsPath() anchors it.
136
+ const coordRoot = dirname(dirname(canonicalEventsPath()));
147
137
  const enrich = enrichFromHeartbeat(coordRoot, instanceId);
148
138
  if (!enrich)
149
139
  return;
@@ -160,14 +150,10 @@ function emitCanonicalCommand(type, fields) {
160
150
  }
161
151
  }
162
152
  /**
163
- * Emit a session event. Command + narration events
164
- * are written to the canonical `.harnery/events.ndjson`; the coord/state types
165
- * are accepted for call-site compatibility but are no-ops here (the agents CLI
166
- * emits those itself). Best-effort, never throws into the caller; a command
167
- * must never break or slow on telemetry. The `agentName` arg is retained for
168
- * the stable call signature (canonical events key on instance_id, not name).
153
+ * Emit a command or narration event to `.harnery/events.ndjson`. Best-effort,
154
+ * never throws into the caller; telemetry must not break or slow a command.
169
155
  */
170
- export function writeSessionEvent(type, _agentName, fields = {}) {
156
+ export function writeSessionEvent(type, fields = {}) {
171
157
  emitCanonicalCommand(type, fields);
172
158
  }
173
159
  /** Trim long values to keep individual events small. */
@@ -30,6 +30,9 @@ export interface V2Heartbeat {
30
30
  last_tool_at?: string;
31
31
  task?: string;
32
32
  task_updated_at?: string;
33
+ suggested_session_name?: string;
34
+ session_name_seen_at?: string;
35
+ session_name_seen_for?: string;
33
36
  last_status_at?: string;
34
37
  presence?: "mobile" | "office";
35
38
  last_intent?: 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,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,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;AA6YD,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,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,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,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,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;AAsbD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3E"}
@@ -197,6 +197,18 @@ function apply(hb, ev, coordRoot) {
197
197
  case "turn.stop":
198
198
  hb.last_turn_stop_at = ev.ts;
199
199
  hb.last_turn_status_box_present = pickBool(d, "status_box_present");
200
+ // Rebuild fidelity for the naming ritual: the live stamp is written by
201
+ // the Stop hook (stampSessionNameSeen); reproduce it from the event so a
202
+ // full projector rebuild doesn't re-open a satisfied naming window.
203
+ if (d.session_name_present === true) {
204
+ hb.session_name_seen_at ??= ev.ts;
205
+ // Attribute the sighting to the name the scan actually covered. Reading
206
+ // the name current at this point in the replay instead re-attributed an
207
+ // old sighting to a re-minted name, which faked "already seen" for a
208
+ // name no reply had shown. An event without the field predates it, so
209
+ // leave the attribution unset and let the next stop re-scan.
210
+ hb.session_name_seen_for = pickStr(d, "session_name_present_for");
211
+ }
200
212
  {
201
213
  const summary = pickStr(d, "turn_summary");
202
214
  if (summary) {
@@ -254,6 +266,11 @@ function apply(hb, ev, coordRoot) {
254
266
  hb.task = task;
255
267
  }
256
268
  hb.task_updated_at = ev.ts;
269
+ // Rebuild fidelity: the naming call carries the name it produced.
270
+ const suggested = pickStr(d, "suggested_session_name");
271
+ if (suggested && !hb.suggested_session_name) {
272
+ hb.suggested_session_name = suggested;
273
+ }
257
274
  break;
258
275
  }
259
276
  case "state.status_checked":
@@ -274,6 +291,20 @@ function apply(hb, ev, coordRoot) {
274
291
  hb.presence = to;
275
292
  break;
276
293
  }
294
+ case "claim.acquire": {
295
+ const path = pickStr(d, "path");
296
+ const mode = pickStr(d, "mode");
297
+ if (path && mode === "write") {
298
+ const canonical = path.startsWith(`${coordRoot}/`)
299
+ ? path.slice(coordRoot.length + 1)
300
+ : path;
301
+ if (!hb.files_touched)
302
+ hb.files_touched = [];
303
+ if (!hb.files_touched.includes(canonical))
304
+ hb.files_touched.push(canonical);
305
+ }
306
+ break;
307
+ }
277
308
  case "claim.release": {
278
309
  const path = pickStr(d, "path");
279
310
  if (path && hb.files_touched) {
@@ -292,7 +323,7 @@ function apply(hb, ev, coordRoot) {
292
323
  }
293
324
  function adapterToPlatform(adapter) {
294
325
  if (adapter === "claude-code")
295
- return "claude_code";
326
+ return "claude-code";
296
327
  if (adapter === "cursor")
297
328
  return "cursor";
298
329
  if (adapter === "codex")
@@ -446,6 +477,18 @@ function writeHeartbeat(coordRoot, instanceId, hb) {
446
477
  setIfDefined(merged, "last_tool_at", hb.last_tool_at);
447
478
  setIfDefined(merged, "task", hb.task);
448
479
  setIfDefined(merged, "task_updated_at", hb.task_updated_at);
480
+ setIfDefined(merged, "suggested_session_name", hb.suggested_session_name);
481
+ setIfDefined(merged, "session_name_seen_at", hb.session_name_seen_at);
482
+ if (hb.session_name_seen_for) {
483
+ merged.session_name_seen_for = hb.session_name_seen_for;
484
+ }
485
+ else {
486
+ // Unlike most projected fields, an absent attribution is meaningful: a
487
+ // legacy sighting cannot prove which suggested name it covered. Remove a
488
+ // stale value inherited through the additive merge so the next stop
489
+ // scans the current name instead of treating it as already satisfied.
490
+ delete merged.session_name_seen_for;
491
+ }
449
492
  setIfDefined(merged, "last_status_at", hb.last_status_at);
450
493
  setIfDefined(merged, "turn_summary", hb.turn_summary);
451
494
  setIfDefined(merged, "turn_summary_updated_at", hb.turn_summary_updated_at);
@@ -27,6 +27,17 @@ export interface Heartbeat {
27
27
  files_touched: string[];
28
28
  task?: string;
29
29
  task_updated_at?: string | null;
30
+ /** Session name built on the first non-empty set-task (never rebuilt). Its
31
+ * presence is the "this session has been named" signal the prompt-context
32
+ * nudge and the Stop-hook naming rule key on. */
33
+ suggested_session_name?: string;
34
+ /** Stamped by turn.stop once the suggested name is seen in assistant reply
35
+ * text, ending the per-turn transcript scan. */
36
+ session_name_seen_at?: string;
37
+ /** WHICH name that sighting was for. The scan is skipped only while this
38
+ * matches the current suggested name, so a re-minted name is detectable
39
+ * again rather than being suppressed by the earlier sighting. */
40
+ session_name_seen_for?: string;
30
41
  last_status_at?: string;
31
42
  turn_summary?: string | null;
32
43
  turn_summary_updated_at?: string | null;
@@ -35,11 +46,26 @@ export interface Heartbeat {
35
46
  last_tool_at?: string;
36
47
  current_turn_id?: string;
37
48
  parent_instance_id?: string;
49
+ workflow_run_id?: string;
38
50
  [extra: string]: unknown;
39
51
  }
40
52
  export declare function readHeartbeat(coordRoot: string, instanceId: string): Heartbeat | null;
41
53
  export declare function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null;
42
- export declare function stampStatusCheck(coordRoot: string, instanceId: string): Heartbeat | null;
54
+ /**
55
+ * Build the copy-pasteable session name from the coord identity + the agent's
56
+ * description parts. Pure (no coord-state reads) so it's unit-testable; collapses
57
+ * internal whitespace and trims. Returns null when the description is empty.
58
+ */
59
+ export declare function buildSuggestedName(agentName: string, descriptionParts: string[]): {
60
+ suggestedName: string;
61
+ description: string;
62
+ } | null;
63
+ /** Stamp the sighting once the suggested name has been observed in assistant
64
+ * reply text; later turns skip the transcript scan. Records WHICH name was seen,
65
+ * because the skip is only valid for that one: if a later set-task mints a
66
+ * different suggested name, a bare "already seen" stamp would suppress the scan
67
+ * forever and leave the Stop-hook naming rule permanently unsatisfiable. */
68
+ export declare function stampSessionNameSeen(coordRoot: string, instanceId: string, name?: string): Heartbeat | null;
43
69
  export declare function setTurnSummary(coordRoot: string, instanceId: string, summary: string): Heartbeat | null;
44
70
  export declare function releaseClaim(coordRoot: string, instanceId: string, path: string): Heartbeat | null;
45
71
  /** A heartbeat that actually dropped a path during a group unclaim. */
@@ -57,9 +83,8 @@ export interface GroupUnclaimHit {
57
83
  * durable `claim.release` events — a file-only prune is silently reverted by
58
84
  * the next projector replay.
59
85
  *
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
86
+ * Tool payloads and release calls can supply either absolute-under-coordRoot or
87
+ * canonical repo-relative entries, so both sides are normalized before comparing. An exact-string match
63
88
  * silently no-ops on the mixed-form case and the claim never releases.
64
89
  *
65
90
  * This is the Option B fix for post-commit's pid-map attribution hole: a
@@ -69,7 +94,9 @@ export interface GroupUnclaimHit {
69
94
  export declare function groupUnclaim(coordRoot: string, groupId: string, path: string): GroupUnclaimHit[];
70
95
  export declare function killHeartbeat(coordRoot: string, instanceId: string): boolean;
71
96
  export declare function healPidmap(coordRoot: string, instanceId: string, pid: number): void;
72
- export declare function healHeartbeat(coordRoot: string, instanceId: string, sessionId?: string, model?: string, adapter?: string): Heartbeat | null;
97
+ export declare function healHeartbeat(coordRoot: string, instanceId: string, sessionId?: string, model?: string, adapter?: string, opts?: {
98
+ forkedFrom?: string;
99
+ }): Heartbeat | null;
73
100
  /**
74
101
  * Register a workflow child in the coordination layer from the ENGINE side.
75
102
  *
@@ -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,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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IACJ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,SAAS,CAwBX;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;AA4DH,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;;qDAEiD;IACjD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;oDACgD;IAChD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;qEAEiE;IACjE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,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,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,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,CAmB7F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EAAE,GACzB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvD;AAED;;;;4EAI4E;AAC5E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,SAAS,GAAG,IAAI,CAMlB;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;;;;;;;;;;;;;;;;GAgBG;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,EAChB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,SAAS,GAAG,IAAI,CAsElB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IACJ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,SAAS,CAwBX;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"}
@@ -24,16 +24,15 @@ function adapterOf(platform) {
24
24
  return "codex";
25
25
  return "claude-code";
26
26
  }
27
- /** Inverse of adapterOf: maps the canonical adapter ("claude-code"/"cursor"/
28
- * "codex") to the legacy underscore platform label stored on the heartbeat.
29
- * Mirrors heartbeat-projector.adapterToPlatform so a healed heartbeat carries
30
- * the same platform value projection would have written. */
27
+ /** Preserve the canonical adapter id in heartbeat state. Mirrors
28
+ * heartbeat-projector.adapterToPlatform so a healed heartbeat carries the same
29
+ * platform value projection would have written. */
31
30
  function adapterToPlatform(adapter) {
32
31
  if (adapter === "cursor")
33
32
  return "cursor";
34
33
  if (adapter === "codex")
35
34
  return "codex";
36
- return "claude_code";
35
+ return "claude-code";
37
36
  }
38
37
  /**
39
38
  * Emit a canonical `health.*` event for an actual self-heal write. This is the
@@ -93,17 +92,44 @@ function mutate(coordRoot, instanceId, fn) {
93
92
  export function setTask(coordRoot, instanceId, task) {
94
93
  return mutate(coordRoot, instanceId, (hb) => {
95
94
  const cleared = !task || task.length === 0;
95
+ // Name the session on its first NON-EMPTY declaration. Keyed on the
96
+ // suggested_session_name stamp, not task_updated_at, so a bare clear as
97
+ // the first call never burns the naming window. Subagents and workflow
98
+ // children have no human-owned tab to rename, so they are never named.
99
+ const humanFacing = hb.kind !== "subagent" && hb.kind !== "transient" && !hb.workflow_run_id;
100
+ const built = !cleared && !hb.suggested_session_name && humanFacing
101
+ ? buildSuggestedName(hb.name ?? "unknown", [task])
102
+ : null;
96
103
  return {
97
104
  ...hb,
98
105
  task: cleared ? undefined : task,
99
106
  task_updated_at: nowIsoSeconds(),
107
+ ...(built ? { suggested_session_name: built.suggestedName } : {}),
100
108
  };
101
109
  });
102
110
  }
103
- export function stampStatusCheck(coordRoot, instanceId) {
111
+ /**
112
+ * Build the copy-pasteable session name from the coord identity + the agent's
113
+ * description parts. Pure (no coord-state reads) so it's unit-testable; collapses
114
+ * internal whitespace and trims. Returns null when the description is empty.
115
+ */
116
+ export function buildSuggestedName(agentName, descriptionParts) {
117
+ const description = descriptionParts.join(" ").replace(/\s+/g, " ").trim();
118
+ if (!description)
119
+ return null;
120
+ const name = agentName?.trim() || "unknown";
121
+ return { suggestedName: `Agent ${name} - ${description}`, description };
122
+ }
123
+ /** Stamp the sighting once the suggested name has been observed in assistant
124
+ * reply text; later turns skip the transcript scan. Records WHICH name was seen,
125
+ * because the skip is only valid for that one: if a later set-task mints a
126
+ * different suggested name, a bare "already seen" stamp would suppress the scan
127
+ * forever and leave the Stop-hook naming rule permanently unsatisfiable. */
128
+ export function stampSessionNameSeen(coordRoot, instanceId, name) {
104
129
  return mutate(coordRoot, instanceId, (hb) => ({
105
130
  ...hb,
106
- last_status_at: nowIsoSeconds(),
131
+ session_name_seen_at: hb.session_name_seen_at ?? nowIsoSeconds(),
132
+ ...(name ? { session_name_seen_for: name } : {}),
107
133
  }));
108
134
  }
109
135
  export function setTurnSummary(coordRoot, instanceId, summary) {
@@ -134,9 +160,8 @@ export function releaseClaim(coordRoot, instanceId, path) {
134
160
  * durable `claim.release` events — a file-only prune is silently reverted by
135
161
  * the next projector replay.
136
162
  *
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
163
+ * Tool payloads and release calls can supply either absolute-under-coordRoot or
164
+ * canonical repo-relative entries, so both sides are normalized before comparing. An exact-string match
140
165
  * silently no-ops on the mixed-form case and the claim never releases.
141
166
  *
142
167
  * This is the Option B fix for post-commit's pid-map attribution hole: a
@@ -203,7 +228,7 @@ export function healPidmap(coordRoot, instanceId, pid) {
203
228
  const dir = join(coordRoot, ".harnery", "pid-map");
204
229
  mkdirSync(dir, { recursive: true });
205
230
  const hb = readHeartbeat(coordRoot, instanceId);
206
- const platform = hb?.platform ?? "claude_code";
231
+ const platform = hb?.platform ?? "claude-code";
207
232
  const pmPath = join(dir, String(pid));
208
233
  // Drift guard: only write + emit telemetry when the entry is missing or
209
234
  // points at a different owner. Without this, a per-tool-call heal would
@@ -226,7 +251,7 @@ export function healPidmap(coordRoot, instanceId, pid) {
226
251
  ...(existingOwner ? { prior: existingOwner.slice(0, 8) } : {}),
227
252
  });
228
253
  }
229
- export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter) {
254
+ export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter, opts) {
230
255
  const path = heartbeatPath(coordRoot, instanceId);
231
256
  if (existsSync(path)) {
232
257
  // Already alive, return it.
@@ -240,8 +265,8 @@ export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter)
240
265
  let agentId = "";
241
266
  try {
242
267
  // eslint-disable-next-line @typescript-eslint/no-require-imports
243
- const { resolveName } = require("./names.ts");
244
- const resolved = resolveName(coordRoot, instanceId, sessionId);
268
+ const names = require("./names.ts");
269
+ const resolved = names.resolveName(coordRoot, instanceId, sessionId);
245
270
  if (resolved) {
246
271
  name = resolved.name;
247
272
  kind = resolved.kind;
@@ -249,6 +274,16 @@ export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter)
249
274
  // name-history. Native subagents continue to use instance_id.
250
275
  agentId = resolved.agent_id ?? (resolved.kind === "subagent" ? instanceId : "");
251
276
  }
277
+ else if (!sessionId || sessionId === instanceId) {
278
+ // A main session with NO history at all never fired session.start —
279
+ // the CC fork flow (SessionStart fires under the parent's id before
280
+ // the fork id is minted), or a partially-wired adapter. Mint its pool
281
+ // name here instead of leaving a nameless heartbeat, and stamp the
282
+ // detected fork lineage while we're at it. A sessionId that differs
283
+ // from instanceId is left alone: that shape is a subagent whose parent
284
+ // is also unknown, and guessing "session" would be wrong.
285
+ name = names.assignName(coordRoot, instanceId, "session", opts?.forkedFrom ? { forkedFrom: opts.forkedFrom } : undefined);
286
+ }
252
287
  }
253
288
  catch {
254
289
  /* names module unavailable, fall back to empty */
@@ -264,19 +299,21 @@ export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter)
264
299
  started_at: now,
265
300
  last_heartbeat: now,
266
301
  files_touched: [],
267
- // Default to claude_code only when the caller can't tell us the adapter
302
+ // Default to claude-code only when the caller can't tell us the adapter
268
303
  // (e.g. manual `harn agents heal`). The live tool.pre_use heal threads the
269
304
  // detected adapter so a pruned Cursor/Codex heartbeat is recreated with
270
- // the correct platform instead of being mislabeled claude_code.
305
+ // the correct platform instead of being mislabeled claude-code.
271
306
  platform: adapterToPlatform(adapter),
272
307
  };
273
308
  atomicWrite(path, JSON.stringify(hb, null, 2));
274
309
  // Write-only telemetry: only the actual-recreate branch reaches here (the
275
310
  // already-alive case returned above), so this records exactly the heals that
276
- // happened.
311
+ // happened. Recorded fork lineage rides the event too, so derived readers
312
+ // rebuilding from the ledger converge with .name-history.
277
313
  emitHealthHeal(coordRoot, "health.heartbeat_heal", instanceId, hb, {
278
314
  reason: "missing",
279
315
  kind: "heartbeat",
316
+ ...(opts?.forkedFrom ? { forked_from: opts.forkedFrom } : {}),
280
317
  });
281
318
  return hb;
282
319
  }
@@ -31,6 +31,10 @@ export interface NameHistoryRow {
31
31
  /** Audit marker distinguishing an explicit role adoption from pool assignment. */
32
32
  source?: "pool" | "identity.assume";
33
33
  previous_name?: string;
34
+ /** Instance this session was forked/branched from (recorded fork lineage).
35
+ * Stamped only on the row that first assigns this instance, when the adapter
36
+ * layer detected or supplied a parent conversation. */
37
+ forked_from?: string;
34
38
  }
35
39
  /**
36
40
  * Resolve (name, kind) for an existing owner without consuming a counter
@@ -66,5 +70,22 @@ export declare function recordNameAssumption(coordRoot: string, instanceId: stri
66
70
  * Assign a name to <instanceId> with the given <kind>. Counter-consuming when
67
71
  * the owner is new. Idempotent: returns existing name on resume.
68
72
  */
69
- export declare function assignName(coordRoot: string, instanceId: string, kind: NameKind): string;
73
+ export declare function assignName(coordRoot: string, instanceId: string, kind: NameKind, opts?: {
74
+ forkedFrom?: string;
75
+ }): string;
76
+ /** One step of recorded fork lineage: the latest row for <instanceId> that
77
+ * carries `forked_from` (latest-row-wins, matching resolveName). */
78
+ export declare function readForkParent(coordRoot: string, instanceId: string): {
79
+ instance_id: string;
80
+ name: string | null;
81
+ } | null;
82
+ /**
83
+ * Full recorded fork ancestry for <instanceId>, nearest ancestor first, each
84
+ * with its latest resolved name. Depth-capped and cycle-guarded: lineage is
85
+ * append-only operational data, not something to trust unboundedly.
86
+ */
87
+ export declare function resolveForkAncestry(coordRoot: string, instanceId: string): Array<{
88
+ instance_id: string;
89
+ name: string | null;
90
+ }>;
70
91
  //# sourceMappingURL=names.d.ts.map
@@ -1 +1 @@
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"}
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;IACvB;;2DAEuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;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,CACxB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,MAAM,CA0BR;AAED;oEACoE;AACpE,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAUrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAYrD"}
@@ -389,8 +389,10 @@ export function recordNameAssumption(coordRoot, instanceId, name, agentId, kind
389
389
  * Assign a name to <instanceId> with the given <kind>. Counter-consuming when
390
390
  * the owner is new. Idempotent: returns existing name on resume.
391
391
  */
392
- export function assignName(coordRoot, instanceId, kind) {
393
- // Check 1: existing history row → original name.
392
+ export function assignName(coordRoot, instanceId, kind, opts) {
393
+ // Check 1: existing history row → original name. A resume re-enters here,
394
+ // which also makes fork stamping naturally idempotent: lineage lands only on
395
+ // the row that first assigns the instance.
394
396
  const existing = resolveName(coordRoot, instanceId);
395
397
  if (existing)
396
398
  return existing.name;
@@ -404,12 +406,48 @@ export function assignName(coordRoot, instanceId, kind) {
404
406
  }
405
407
  const name = COORD_NAMES[counter % 260];
406
408
  atomicWrite(cPath, String(counter + 1));
409
+ const forkedFrom = opts?.forkedFrom;
407
410
  appendHistory(coordRoot, {
408
411
  instance_id: instanceId,
409
412
  name,
410
413
  kind,
411
414
  source: "pool",
415
+ ...(forkedFrom && forkedFrom !== instanceId ? { forked_from: forkedFrom } : {}),
412
416
  ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
413
417
  });
414
418
  return name;
415
419
  }
420
+ /** One step of recorded fork lineage: the latest row for <instanceId> that
421
+ * carries `forked_from` (latest-row-wins, matching resolveName). */
422
+ export function readForkParent(coordRoot, instanceId) {
423
+ const history = readHistory(coordRoot);
424
+ for (let i = history.length - 1; i >= 0; i--) {
425
+ const row = history[i];
426
+ if (row.instance_id !== instanceId)
427
+ continue;
428
+ if (!row.forked_from)
429
+ return null;
430
+ const parent = resolveName(coordRoot, row.forked_from);
431
+ return { instance_id: row.forked_from, name: parent?.name ?? null };
432
+ }
433
+ return null;
434
+ }
435
+ /**
436
+ * Full recorded fork ancestry for <instanceId>, nearest ancestor first, each
437
+ * with its latest resolved name. Depth-capped and cycle-guarded: lineage is
438
+ * append-only operational data, not something to trust unboundedly.
439
+ */
440
+ export function resolveForkAncestry(coordRoot, instanceId) {
441
+ const out = [];
442
+ const seen = new Set([instanceId]);
443
+ let cursor = instanceId;
444
+ for (let depth = 0; depth < 20; depth++) {
445
+ const parent = readForkParent(coordRoot, cursor);
446
+ if (!parent || seen.has(parent.instance_id))
447
+ break;
448
+ out.push(parent);
449
+ seen.add(parent.instance_id);
450
+ cursor = parent.instance_id;
451
+ }
452
+ return out;
453
+ }