harnery 0.31.6 → 0.33.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 (264) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +5 -1
  4. package/dist/commands/agents.d.ts +0 -9
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +280 -94
  7. package/dist/commands/browse-session.d.ts +21 -0
  8. package/dist/commands/browse-session.d.ts.map +1 -0
  9. package/dist/commands/browse-session.js +157 -0
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +205 -37
  12. package/dist/commands/checkpoint.js +1 -1
  13. package/dist/commands/claude-desktop.d.ts +2 -2
  14. package/dist/commands/claude-desktop.d.ts.map +1 -1
  15. package/dist/commands/claude-desktop.js +74 -3
  16. package/dist/commands/deinit.d.ts.map +1 -1
  17. package/dist/commands/deinit.js +4 -0
  18. package/dist/commands/docs.d.ts.map +1 -1
  19. package/dist/commands/docs.js +40 -0
  20. package/dist/commands/doctor.d.ts +4 -2
  21. package/dist/commands/doctor.d.ts.map +1 -1
  22. package/dist/commands/doctor.js +141 -19
  23. package/dist/commands/init.d.ts +2 -2
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +26 -5
  26. package/dist/commands/instructions.d.ts +4 -0
  27. package/dist/commands/instructions.d.ts.map +1 -0
  28. package/dist/commands/instructions.js +100 -0
  29. package/dist/commands/presence.d.ts.map +1 -1
  30. package/dist/commands/presence.js +4 -1
  31. package/dist/commands/tunnel.d.ts.map +1 -1
  32. package/dist/commands/tunnel.js +2 -3
  33. package/dist/core/agents/canonical-emit.d.ts +2 -3
  34. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  35. package/dist/core/agents/canonical-emit.js +6 -5
  36. package/dist/core/agents/cli.js +128 -89
  37. package/dist/core/agents/coord-client.d.ts +18 -3
  38. package/dist/core/agents/coord-client.d.ts.map +1 -1
  39. package/dist/core/agents/coord-client.js +5 -4
  40. package/dist/core/agents/finalization.d.ts +68 -0
  41. package/dist/core/agents/finalization.d.ts.map +1 -0
  42. package/dist/core/agents/finalization.js +443 -0
  43. package/dist/core/agents/git-hook.d.ts +51 -0
  44. package/dist/core/agents/git-hook.d.ts.map +1 -0
  45. package/dist/core/agents/git-hook.js +118 -0
  46. package/dist/core/agents/index.d.ts +1 -0
  47. package/dist/core/agents/index.d.ts.map +1 -1
  48. package/dist/core/agents/index.js +1 -0
  49. package/dist/core/agents/render/prompt-context.d.ts +12 -5
  50. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/prompt-context.js +70 -16
  52. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  53. package/dist/core/agents/render/session-context.js +23 -4
  54. package/dist/core/agents/rules/claim-conflict.js +3 -3
  55. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  56. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  57. package/dist/core/agents/rules/commit-conflict.js +21 -5
  58. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  59. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  60. package/dist/core/agents/rules/stop-hook.js +59 -23
  61. package/dist/core/agents/session-events.d.ts +8 -16
  62. package/dist/core/agents/session-events.d.ts.map +1 -1
  63. package/dist/core/agents/session-events.js +12 -26
  64. package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
  65. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  66. package/dist/core/agents/state/heartbeat-projector.js +98 -1
  67. package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
  68. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  69. package/dist/core/agents/state/heartbeat-writer.js +86 -17
  70. package/dist/core/agents/state/names.d.ts +22 -1
  71. package/dist/core/agents/state/names.d.ts.map +1 -1
  72. package/dist/core/agents/state/names.js +40 -2
  73. package/dist/core/agents/state/session-state.d.ts +44 -0
  74. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  75. package/dist/core/agents/state/session-state.js +81 -0
  76. package/dist/core/config.d.ts +30 -2
  77. package/dist/core/config.d.ts.map +1 -1
  78. package/dist/core/config.js +74 -11
  79. package/dist/core/governor/planning.d.ts.map +1 -1
  80. package/dist/core/governor/planning.js +12 -13
  81. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  82. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  83. package/dist/core/hooks/adapter/detect.js +4 -14
  84. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  85. package/dist/core/hooks/adapter/events.js +1 -0
  86. package/dist/core/hooks/adapter/output.d.ts +1 -1
  87. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  88. package/dist/core/hooks/adapter/output.js +5 -4
  89. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  90. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  91. package/dist/core/hooks/adapter/parse.js +2 -0
  92. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  93. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/wiring.js +32 -0
  95. package/dist/core/hooks/cli.d.ts +1 -2
  96. package/dist/core/hooks/cli.d.ts.map +1 -1
  97. package/dist/core/hooks/cli.js +258 -32
  98. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  99. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  100. package/dist/core/hooks/codex-authorization.js +199 -0
  101. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  102. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  103. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  104. package/dist/core/hooks/effects/index.d.ts +1 -1
  105. package/dist/core/hooks/effects/index.js +3 -3
  106. package/dist/core/hooks/events/schema.d.ts +57 -1
  107. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  108. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  109. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  110. package/dist/core/hooks/resolve/owner.js +15 -0
  111. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  112. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  113. package/dist/core/hooks/resolve/transcript.js +176 -1
  114. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  115. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  116. package/dist/core/hooks/session-name-presence.js +43 -0
  117. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  118. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  119. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  120. package/dist/core/presence/blob.d.ts +4 -0
  121. package/dist/core/presence/blob.d.ts.map +1 -1
  122. package/dist/core/presence/blob.js +12 -0
  123. package/dist/core/presence/index.js +15 -1
  124. package/dist/core/work/state.d.ts +4 -5
  125. package/dist/core/work/state.d.ts.map +1 -1
  126. package/dist/core/work/state.js +5 -8
  127. package/dist/core/workflow/index.d.ts +1 -1
  128. package/dist/core/workflow/index.d.ts.map +1 -1
  129. package/dist/core/workflow/proof.d.ts +2 -2
  130. package/dist/core/workflow/proof.d.ts.map +1 -1
  131. package/dist/core/workflow/proof.js +2 -2
  132. package/dist/core/workflow/run-state.d.ts +2 -2
  133. package/dist/core/workflow/run-state.d.ts.map +1 -1
  134. package/dist/core/workflow/run-state.js +2 -2
  135. package/dist/core/workflow/types.d.ts +3 -3
  136. package/dist/core/workflow/types.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  138. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  139. package/dist/core/workflow/workspaces/execution.js +5 -0
  140. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  141. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  142. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  143. package/dist/core/workflow/workspaces/local-git.js +1 -2
  144. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  145. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  146. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  147. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  148. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  149. package/dist/core/workflow/workspaces/validate.js +0 -2
  150. package/dist/lib/browser/client.d.ts +118 -1
  151. package/dist/lib/browser/client.d.ts.map +1 -1
  152. package/dist/lib/browser/client.js +435 -6
  153. package/dist/lib/browser/geometry.d.ts.map +1 -1
  154. package/dist/lib/browser/geometry.js +180 -37
  155. package/dist/lib/browser/index.d.ts +5 -1
  156. package/dist/lib/browser/index.d.ts.map +1 -1
  157. package/dist/lib/browser/index.js +5 -1
  158. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  159. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  160. package/dist/lib/browser/netscape-cookies.js +31 -0
  161. package/dist/lib/browser/proxy.d.ts +24 -0
  162. package/dist/lib/browser/proxy.d.ts.map +1 -0
  163. package/dist/lib/browser/proxy.js +84 -0
  164. package/dist/lib/browser/runts.d.ts +6 -0
  165. package/dist/lib/browser/runts.d.ts.map +1 -1
  166. package/dist/lib/browser/runts.js +21 -4
  167. package/dist/lib/browser/session-control.d.ts +112 -0
  168. package/dist/lib/browser/session-control.d.ts.map +1 -0
  169. package/dist/lib/browser/session-control.js +670 -0
  170. package/dist/lib/claude-desktop.d.ts +47 -0
  171. package/dist/lib/claude-desktop.d.ts.map +1 -1
  172. package/dist/lib/claude-desktop.js +169 -1
  173. package/dist/lib/docs-links.d.ts +108 -0
  174. package/dist/lib/docs-links.d.ts.map +1 -0
  175. package/dist/lib/docs-links.js +555 -0
  176. package/dist/lib/exec.d.ts +1 -1
  177. package/dist/lib/exec.js +1 -1
  178. package/dist/lib/identities/assume.d.ts +4 -2
  179. package/dist/lib/identities/assume.d.ts.map +1 -1
  180. package/dist/lib/identities/assume.js +15 -2
  181. package/dist/lib/instructions/bundle.d.ts +33 -0
  182. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  183. package/dist/lib/instructions/bundle.js +149 -0
  184. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  185. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  186. package/dist/lib/instructions/git-hooks.js +238 -0
  187. package/dist/lib/instructions/splice.d.ts +10 -4
  188. package/dist/lib/instructions/splice.d.ts.map +1 -1
  189. package/dist/lib/instructions/splice.js +19 -11
  190. package/package.json +2 -2
  191. package/schemas/config.schema.json +32 -0
  192. package/src/commander.ts +5 -1
  193. package/src/commands/agents.ts +341 -113
  194. package/src/commands/browse-session.ts +245 -0
  195. package/src/commands/browse.ts +287 -44
  196. package/src/commands/checkpoint.ts +1 -1
  197. package/src/commands/claude-desktop.ts +106 -5
  198. package/src/commands/deinit.ts +7 -1
  199. package/src/commands/docs.ts +56 -0
  200. package/src/commands/doctor.ts +155 -21
  201. package/src/commands/init.ts +28 -9
  202. package/src/commands/instructions.ts +129 -0
  203. package/src/commands/presence.ts +7 -1
  204. package/src/commands/tunnel.ts +2 -5
  205. package/src/core/agents/canonical-emit.ts +7 -6
  206. package/src/core/agents/cli.ts +143 -89
  207. package/src/core/agents/coord-client.ts +21 -5
  208. package/src/core/agents/finalization.ts +595 -0
  209. package/src/core/agents/git-hook.ts +126 -0
  210. package/src/core/agents/index.ts +1 -0
  211. package/src/core/agents/render/prompt-context.ts +96 -16
  212. package/src/core/agents/render/session-context.ts +29 -4
  213. package/src/core/agents/rules/claim-conflict.ts +3 -3
  214. package/src/core/agents/rules/commit-conflict.ts +35 -8
  215. package/src/core/agents/rules/stop-hook.ts +71 -23
  216. package/src/core/agents/session-events.ts +14 -41
  217. package/src/core/agents/state/heartbeat-projector.ts +108 -1
  218. package/src/core/agents/state/heartbeat-writer.ts +129 -16
  219. package/src/core/agents/state/names.ts +54 -2
  220. package/src/core/agents/state/session-state.ts +131 -0
  221. package/src/core/config.ts +92 -12
  222. package/src/core/governor/planning.ts +17 -12
  223. package/src/core/hooks/adapter/detect.ts +4 -13
  224. package/src/core/hooks/adapter/events.ts +1 -0
  225. package/src/core/hooks/adapter/output.ts +9 -4
  226. package/src/core/hooks/adapter/parse.ts +3 -0
  227. package/src/core/hooks/adapter/wiring.ts +43 -0
  228. package/src/core/hooks/cli.ts +296 -27
  229. package/src/core/hooks/codex-authorization.ts +253 -0
  230. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  231. package/src/core/hooks/effects/index.ts +5 -5
  232. package/src/core/hooks/events/schema.ts +71 -0
  233. package/src/core/hooks/resolve/owner.ts +22 -1
  234. package/src/core/hooks/resolve/transcript.ts +164 -1
  235. package/src/core/hooks/session-name-presence.ts +52 -0
  236. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  237. package/src/core/presence/blob.ts +18 -0
  238. package/src/core/presence/index.ts +18 -1
  239. package/src/core/work/state.ts +8 -12
  240. package/src/core/workflow/engine.ts +2 -2
  241. package/src/core/workflow/index.ts +0 -1
  242. package/src/core/workflow/proof.ts +4 -4
  243. package/src/core/workflow/run-state.ts +5 -5
  244. package/src/core/workflow/types.ts +3 -3
  245. package/src/core/workflow/workspaces/execution.ts +11 -4
  246. package/src/core/workflow/workspaces/index.ts +0 -3
  247. package/src/core/workflow/workspaces/local-git.ts +1 -2
  248. package/src/core/workflow/workspaces/paths.ts +2 -2
  249. package/src/core/workflow/workspaces/types.ts +0 -9
  250. package/src/core/workflow/workspaces/validate.ts +1 -5
  251. package/src/lib/browser/client.ts +528 -6
  252. package/src/lib/browser/geometry.ts +197 -37
  253. package/src/lib/browser/index.ts +39 -0
  254. package/src/lib/browser/netscape-cookies.ts +39 -0
  255. package/src/lib/browser/proxy.ts +105 -0
  256. package/src/lib/browser/runts.ts +27 -3
  257. package/src/lib/browser/session-control.ts +892 -0
  258. package/src/lib/claude-desktop.ts +232 -1
  259. package/src/lib/docs-links.ts +674 -0
  260. package/src/lib/exec.ts +1 -1
  261. package/src/lib/identities/assume.ts +22 -1
  262. package/src/lib/instructions/bundle.ts +199 -0
  263. package/src/lib/instructions/git-hooks.ts +259 -0
  264. package/src/lib/instructions/splice.ts +41 -11
@@ -30,6 +30,12 @@ import type { Command } from "commander";
30
30
  import type { EmitContext, HarneryProgramContext } from "../commander.ts";
31
31
  import { coordBinPath } from "../core/agents/coord-bin.ts";
32
32
  import { readStreamTailBounded } from "../core/agents/events/consume.ts";
33
+ import {
34
+ checkGitFinalization,
35
+ formatGitFinalizationFailure,
36
+ type GitFinalizationResult,
37
+ readSessionWriteClaims,
38
+ } from "../core/agents/finalization.ts";
33
39
  import {
34
40
  emitCanonical,
35
41
  type Heartbeat,
@@ -40,6 +46,15 @@ import {
40
46
  resolveOwnerBySessionEnv,
41
47
  resolveOwnerWithSource,
42
48
  } from "../core/agents/index.ts";
49
+ import {
50
+ buildLifecycleSuggestedName,
51
+ buildSuggestedName,
52
+ } from "../core/agents/state/heartbeat-writer.ts";
53
+ import {
54
+ type AgentActivity,
55
+ foldSessionState,
56
+ type TaskState,
57
+ } from "../core/agents/state/session-state.ts";
43
58
  import { coordFreshnessSeconds, resolveBinName } from "../core/config.ts";
44
59
  import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
45
60
  import { registerContextCommand } from "./context.ts";
@@ -175,6 +190,12 @@ interface Row {
175
190
  last_tool?: string | null;
176
191
  last_tool_target?: string | null;
177
192
  task?: string | null;
193
+ activity: AgentActivity;
194
+ activity_updated_at?: string | null;
195
+ activity_source?: string | null;
196
+ task_state: TaskState;
197
+ task_state_updated_at?: string | null;
198
+ task_state_reason?: string | null;
178
199
  turn_summary?: string | null;
179
200
  turn_summary_updated_at?: string | null;
180
201
  platform?: string | null;
@@ -183,6 +204,19 @@ interface Row {
183
204
  machine?: string | null;
184
205
  }
185
206
 
207
+ function activityOf(hb: Pick<Heartbeat, "activity">): AgentActivity {
208
+ return hb.activity ?? "unknown";
209
+ }
210
+
211
+ function taskStateOf(hb: Pick<Heartbeat, "task_state">): TaskState {
212
+ return hb.task_state ?? "active";
213
+ }
214
+
215
+ function lifecycleLabel(hb: Pick<Heartbeat, "task_state" | "task_state_reason">): string {
216
+ const state = taskStateOf(hb);
217
+ return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
218
+ }
219
+
186
220
  let emit: EmitContext;
187
221
 
188
222
  export function registerAgentsCommand(
@@ -221,13 +255,17 @@ export function registerAgentsCommand(
221
255
  .command("status")
222
256
  .description("End-of-turn status box (name + session age + files held + peer count)")
223
257
  .option("--json", "JSON output instead of the box")
258
+ .option(
259
+ "--end-turn",
260
+ "Treat this as the turn's closing status: issue the box only when this session's held paths are committed and their repositories are pushed",
261
+ )
224
262
  .option(
225
263
  "--session-id <id>",
226
264
  "Lookup heartbeat by session_id directly, bypassing the ppid walk. " +
227
265
  "Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
228
266
  "The Stop hook payload includes session_id; pass it through.",
229
267
  )
230
- .action((opts: { json?: boolean; sessionId?: string }) => {
268
+ .action((opts: { endTurn?: boolean; json?: boolean; sessionId?: string }) => {
231
269
  runStatus(opts);
232
270
  });
233
271
 
@@ -306,6 +344,18 @@ export function registerAgentsCommand(
306
344
  runSetTask(text.join(" "), opts);
307
345
  });
308
346
 
347
+ cmd
348
+ .command("lifecycle <state>")
349
+ .description("Declare this session's task lifecycle: active, blocked, or done")
350
+ .option("--reason <text>", "Explain the lifecycle declaration")
351
+ .option(
352
+ "--session-id <id>",
353
+ "Target the heartbeat with this session_id directly, bypassing the ppid walk.",
354
+ )
355
+ .action((state: string, opts: { reason?: string; sessionId?: string }) => {
356
+ runLifecycle(state, opts);
357
+ });
358
+
309
359
  cmd
310
360
  .command("release-claim <path>")
311
361
  .description(
@@ -343,7 +393,7 @@ export function registerAgentsCommand(
343
393
  )
344
394
  .option(
345
395
  "--timeout <dur>",
346
- "Give up after this duration; suffix s/m/h/d (e.g. 30s, 5m, 1h). Bare integer = minutes. Default 60m.",
396
+ "Give up after this duration; suffix s/m/h/d is required (e.g. 30s, 5m, 1h). Default 60m.",
347
397
  "60m",
348
398
  )
349
399
  .option("--poll-secs <n>", "Poll interval in seconds (default 5)", "5")
@@ -381,8 +431,8 @@ export function registerAgentsCommand(
381
431
  cmd
382
432
  .command("health")
383
433
  .description(
384
- "One-screen coord-layer health rollup: heal events, schema validity, " +
385
- "commit-guard activity, council activity, anomalies. Designed for " +
434
+ "One-screen coord-layer health rollup: heal events, council activity, " +
435
+ "zombie detection, and anomalies. Designed for " +
386
436
  "daily glance + dashboard ingestion. Reads .harnery/.",
387
437
  )
388
438
  .option("--since <window>", "Window (Nh | Nd). Default: 24h.", "24h")
@@ -397,7 +447,7 @@ export function registerAgentsCommand(
397
447
  "Adapter wiring probe: ppid chain, comm names, pid-map anchor, sample payload paths. " +
398
448
  "With --replay-samples, also replays every checked-in sample payload against the live " +
399
449
  "adapter in an isolated sandbox to catch adapter / payload-shape drift. " +
400
- "Complements heal-events (drift telemetry). Id: claude_code | cursor.",
450
+ "Complements heal-events (drift telemetry). Id: claude-code | cursor.",
401
451
  )
402
452
  .option("--json", "JSON envelope output")
403
453
  .option(
@@ -552,12 +602,23 @@ function registerIdentityCommands(parent: Command): void {
552
602
  "--session-id <id>",
553
603
  "Bind the heartbeat with this session_id directly, bypassing the ppid walk.",
554
604
  )
555
- .action((target: string, opts: { json?: boolean; sessionId?: string }) => {
556
- runIdentityAssume(target, opts);
557
- });
605
+ .option(
606
+ "--force-ancestor",
607
+ "Allow assuming a persona this session's fork lineage descends from " +
608
+ "(default: refused, because a fork's inherited transcript asserting that " +
609
+ "name is not a role handoff).",
610
+ )
611
+ .action(
612
+ (target: string, opts: { json?: boolean; sessionId?: string; forceAncestor?: boolean }) => {
613
+ runIdentityAssume(target, opts);
614
+ },
615
+ );
558
616
  }
559
617
 
560
- function runIdentityAssume(target: string, opts: { json?: boolean; sessionId?: string }): void {
618
+ function runIdentityAssume(
619
+ target: string,
620
+ opts: { json?: boolean; sessionId?: string; forceAncestor?: boolean },
621
+ ): void {
561
622
  if (opts.json) emit.config({ format: "json" });
562
623
  const root = monorepoRoot();
563
624
  if (!root) {
@@ -578,7 +639,9 @@ function runIdentityAssume(target: string, opts: { json?: boolean; sessionId?: s
578
639
  process.exit(1);
579
640
  }
580
641
  try {
581
- const result = assumeIdentity(root, owner, target);
642
+ const result = assumeIdentity(root, owner, target, {
643
+ forceAncestor: opts.forceAncestor ?? false,
644
+ });
582
645
  emit.data({
583
646
  ...result,
584
647
  display_name: displayAgentName(result.name),
@@ -877,9 +940,15 @@ function runWhoami(opts: { json?: boolean }): void {
877
940
  last_tool: hb.last_tool ?? null,
878
941
  last_tool_target: hb.last_tool_target ?? null,
879
942
  task: hb.task ?? null,
943
+ activity: activityOf(hb),
944
+ activity_updated_at: hb.activity_updated_at ?? null,
945
+ activity_source: hb.activity_source ?? null,
946
+ task_state: taskStateOf(hb),
947
+ task_state_updated_at: hb.task_state_updated_at ?? null,
948
+ task_state_reason: hb.task_state_reason ?? null,
880
949
  turn_summary: hb.turn_summary ?? null,
881
950
  turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
882
- platform: hb.platform ?? "claude_code",
951
+ platform: hb.platform ?? "claude-code",
883
952
  };
884
953
 
885
954
  emit.data({ ...row, resolution_source: resolved.source, note: SUBAGENT_NOTE });
@@ -975,9 +1044,15 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
975
1044
  last_tool: h.last_tool ?? null,
976
1045
  last_tool_target: h.last_tool_target ?? null,
977
1046
  task: h.task ?? null,
1047
+ activity: activityOf(h),
1048
+ activity_updated_at: h.activity_updated_at ?? null,
1049
+ activity_source: h.activity_source ?? null,
1050
+ task_state: taskStateOf(h),
1051
+ task_state_updated_at: h.task_state_updated_at ?? null,
1052
+ task_state_reason: h.task_state_reason ?? null,
978
1053
  turn_summary: h.turn_summary ?? null,
979
1054
  turn_summary_updated_at: h.turn_summary_updated_at ?? null,
980
- platform: h.platform ?? "claude_code",
1055
+ platform: h.platform ?? "claude-code",
981
1056
  });
982
1057
  continue;
983
1058
  }
@@ -1000,9 +1075,15 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
1000
1075
  last_tool: h.last_tool ?? null,
1001
1076
  last_tool_target: h.last_tool_target ?? null,
1002
1077
  task: h.task ?? null,
1078
+ activity: activityOf(h),
1079
+ activity_updated_at: h.activity_updated_at ?? null,
1080
+ activity_source: h.activity_source ?? null,
1081
+ task_state: taskStateOf(h),
1082
+ task_state_updated_at: h.task_state_updated_at ?? null,
1083
+ task_state_reason: h.task_state_reason ?? null,
1003
1084
  turn_summary: h.turn_summary ?? null,
1004
1085
  turn_summary_updated_at: h.turn_summary_updated_at ?? null,
1005
- platform: h.platform ?? "claude_code",
1086
+ platform: h.platform ?? "claude-code",
1006
1087
  });
1007
1088
  }
1008
1089
 
@@ -1028,9 +1109,15 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
1028
1109
  last_tool: a.last_tool ?? null,
1029
1110
  last_tool_target: null,
1030
1111
  task: a.task ?? null,
1112
+ activity: a.activity ?? "unknown",
1113
+ activity_updated_at: null,
1114
+ activity_source: null,
1115
+ task_state: a.task_state ?? "active",
1116
+ task_state_updated_at: null,
1117
+ task_state_reason: a.task_state_reason ?? null,
1031
1118
  turn_summary: a.turn_summary ?? null,
1032
1119
  turn_summary_updated_at: null,
1033
- platform: a.platform ?? "claude_code",
1120
+ platform: a.platform ?? "claude-code",
1034
1121
  machine: rm.machine,
1035
1122
  });
1036
1123
  }
@@ -1295,6 +1382,12 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
1295
1382
  age_secs: ageSecs,
1296
1383
  last_heartbeat_secs_ago: heartbeatAgeSecs,
1297
1384
  task: hb.task ?? null,
1385
+ activity: activityOf(hb),
1386
+ activity_updated_at: hb.activity_updated_at ?? null,
1387
+ activity_source: hb.activity_source ?? null,
1388
+ task_state: taskStateOf(hb),
1389
+ task_state_updated_at: hb.task_state_updated_at ?? null,
1390
+ task_state_reason: hb.task_state_reason ?? null,
1298
1391
  turn_summary: hb.turn_summary ?? null,
1299
1392
  turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
1300
1393
  title: report?.title ?? null,
@@ -1326,6 +1419,10 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
1326
1419
  ` session ${formatAge(ageSecs)} old · kind=${data.kind} · session_id=${data.session_id.slice(0, 8)}…`,
1327
1420
  );
1328
1421
  lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
1422
+ lines.push(` activity: ${data.activity}`);
1423
+ lines.push(
1424
+ ` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`,
1425
+ );
1329
1426
  if (data.last_tool) {
1330
1427
  const target = data.last_tool_target ? ` ${truncate(data.last_tool_target, 80)}` : "";
1331
1428
  lines.push(` last activity: ${data.last_tool}${target}`);
@@ -1495,15 +1592,18 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1495
1592
  process.exit(1);
1496
1593
  }
1497
1594
 
1498
- // Snapshot BEFORE the mutation: a heartbeat that has never carried a task
1499
- // has no `task_updated_at` stamp (setTask always stamps it, even on a clear),
1500
- // so its absence is the "focus never declared this session" signal — i.e. the
1501
- // first set-task of the session. That's the natural moment to also hand the
1502
- // operator a session name built from the same declaration (one source of
1503
- // truth: the focus you declare names the session). Distinct from task===""
1504
- // (a deliberate clear), which carries a stamp and does NOT re-trigger.
1595
+ // Snapshot BEFORE the mutation: the writer stamps `suggested_session_name`
1596
+ // on the session's first NON-EMPTY declaration (one source of truth: the
1597
+ // focus you declare names the session). A bare clear never consumes the
1598
+ // naming window, and subagent/workflow kinds are never named — see
1599
+ // heartbeat-writer.setTask. This call is the naming call exactly when the
1600
+ // stamp appears across the mutation.
1505
1601
  const priorHb = readHeartbeat(myOwner);
1506
- const firstOfSession = !priorHb?.task_updated_at;
1602
+ const normalizedTask = task.length > 0 ? task : undefined;
1603
+ const lifecycleWarning =
1604
+ priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
1605
+ ? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
1606
+ : null;
1507
1607
 
1508
1608
  // Heartbeat mutation goes through agent-coord (atomic temp+rename).
1509
1609
  const helper = agentCoordOrExit(root);
@@ -1520,44 +1620,187 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1520
1620
  }
1521
1621
 
1522
1622
  const hb = readHeartbeat(myOwner);
1623
+
1624
+ // The naming call: the mutation just stamped `suggested_session_name` for
1625
+ // the first time. `first_of_session: true` means exactly "this call produced
1626
+ // the session name" — it is never true with a null name (an empty first
1627
+ // declaration or a `--session-id` relay both keep the window open instead).
1628
+ const suggestedName =
1629
+ !priorHb?.suggested_session_name && hb?.suggested_session_name
1630
+ ? hb.suggested_session_name
1631
+ : null;
1632
+ const firstOfSession = suggestedName !== null;
1633
+
1523
1634
  emitCanonical({
1524
1635
  type: "state.task_set",
1525
1636
  owner: myOwner,
1526
1637
  session: hb?.session_id ?? myOwner,
1527
1638
  adapter: normalizeAdapter(hb?.platform),
1528
- data: { task, cleared: !task || task.length === 0 },
1639
+ data: {
1640
+ task,
1641
+ cleared: !task || task.length === 0,
1642
+ first_of_session: firstOfSession,
1643
+ suggested_session_name: suggestedName,
1644
+ },
1529
1645
  });
1530
1646
 
1531
- // On the first focus declaration of a session, surface a suggested session
1532
- // name for the operator to set as their adapter tab title. Read the result
1533
- // (`suggested_session_name`) and reproduce it in a fenced code block so the
1534
- // UI's Copy button hands over the exact string. null on every later call.
1535
- const built =
1536
- firstOfSession && !opts?.sessionId ? buildSuggestedName(hb?.name ?? "unknown", [task]) : null;
1537
-
1538
1647
  emit.data({
1539
1648
  instance_id: myOwner,
1540
1649
  name: hb?.name ?? null,
1541
1650
  task: hb?.task ?? null,
1542
1651
  cleared: !task || task.length === 0,
1543
1652
  first_of_session: firstOfSession,
1544
- suggested_session_name: built?.suggestedName ?? null,
1653
+ suggested_session_name: suggestedName,
1654
+ ...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
1655
+ // Right-time instruction: the UserPromptSubmit nudge fires before the name
1656
+ // exists; this result is the moment the model holds the string.
1657
+ ...(suggestedName
1658
+ ? {
1659
+ note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
1660
+ }
1661
+ : {}),
1545
1662
  });
1546
1663
  }
1547
1664
 
1548
- /**
1549
- * Build the copy-pasteable session name from the coord identity + the agent's
1550
- * description parts. Pure (no coord-state reads) so it's unit-testable; collapses
1551
- * internal whitespace and trims. Returns null when the description is empty.
1552
- */
1553
- export function buildSuggestedName(
1554
- agentName: string,
1555
- descriptionParts: string[],
1556
- ): { suggestedName: string; description: string } | null {
1557
- const description = descriptionParts.join(" ").replace(/\s+/g, " ").trim();
1558
- if (!description) return null;
1559
- const name = agentName?.trim() || "unknown";
1560
- return { suggestedName: `Agent ${name} - ${description}`, description };
1665
+ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: string }): void {
1666
+ const state = rawState.trim().toLowerCase();
1667
+ if (state !== "active" && state !== "blocked" && state !== "done") {
1668
+ emit.error({
1669
+ code: "invalid_lifecycle_state",
1670
+ message: "lifecycle state must be one of: active, blocked, done",
1671
+ });
1672
+ process.exitCode = 1;
1673
+ return;
1674
+ }
1675
+
1676
+ const root = monorepoRoot();
1677
+ if (!root) {
1678
+ emit.error({
1679
+ code: "not_in_repo",
1680
+ message: "not in an agent session; coord_root() returned null",
1681
+ });
1682
+ process.exitCode = 1;
1683
+ return;
1684
+ }
1685
+ if (!opts.sessionId) ensureCursorSession(root);
1686
+ const myOwner = opts.sessionId ?? resolveOwner();
1687
+ if (!myOwner) {
1688
+ emit.error({
1689
+ code: "no_pidmap_entry",
1690
+ message:
1691
+ "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
1692
+ });
1693
+ process.exitCode = 1;
1694
+ return;
1695
+ }
1696
+
1697
+ const hb = readHeartbeat(myOwner);
1698
+ if (!hb) {
1699
+ emit.error({ code: "no_heartbeat", message: noHeartbeatMessage(myOwner) });
1700
+ process.exitCode = 1;
1701
+ return;
1702
+ }
1703
+ if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
1704
+ emit.error({
1705
+ code: "lifecycle_not_human_facing",
1706
+ message: "task lifecycle declarations are limited to human-facing sessions",
1707
+ });
1708
+ process.exitCode = 1;
1709
+ return;
1710
+ }
1711
+
1712
+ const reason = opts.reason?.trim() || undefined;
1713
+ if (state === "blocked" && !reason) {
1714
+ emit.error({
1715
+ code: "blocked_reason_required",
1716
+ message: "lifecycle blocked requires --reason <text>",
1717
+ });
1718
+ process.exitCode = 1;
1719
+ return;
1720
+ }
1721
+ if (state === "done" && !hb.task) {
1722
+ emit.error({
1723
+ code: "task_required_for_done",
1724
+ message: "declare a current task with agents set-task before marking it done",
1725
+ });
1726
+ process.exitCode = 1;
1727
+ return;
1728
+ }
1729
+
1730
+ const priorState: TaskState = hb.task_state ?? "active";
1731
+ const priorReason = hb.task_state_reason || undefined;
1732
+ if (priorState === state && priorReason === reason) {
1733
+ emit.data({
1734
+ instance_id: myOwner,
1735
+ task_state: state,
1736
+ reason: reason ?? null,
1737
+ changed: false,
1738
+ name_reminted: false,
1739
+ git_finalization_checked: false,
1740
+ });
1741
+ return;
1742
+ }
1743
+
1744
+ let finalization: GitFinalizationResult | null = null;
1745
+ if (state === "done") {
1746
+ const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1747
+ const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1748
+ finalization = checkGitFinalization(root, touchedPaths, {
1749
+ claimHistoryComplete: history.complete,
1750
+ });
1751
+ if (!finalization.ok) {
1752
+ emit.error({
1753
+ code: "git_not_finalized",
1754
+ message: formatGitFinalizationFailure(finalization, resolveBinName(root)).replace(
1755
+ "the status box was not issued",
1756
+ "lifecycle was not changed",
1757
+ ),
1758
+ });
1759
+ process.exitCode = 1;
1760
+ return;
1761
+ }
1762
+ }
1763
+
1764
+ const suggestedName = buildLifecycleSuggestedName(hb.name ?? "unknown", hb.task, state);
1765
+ const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
1766
+ const emitted = emitCanonical({
1767
+ type: "state.task_state",
1768
+ owner: myOwner,
1769
+ session: hb.session_id ?? myOwner,
1770
+ adapter: normalizeAdapter(hb.platform),
1771
+ data: {
1772
+ state,
1773
+ reason: reason ?? null,
1774
+ prior_state: priorState,
1775
+ name_reminted: nameReminted,
1776
+ git_finalization_checked: finalization !== null,
1777
+ ...(nameReminted ? { suggested_session_name: suggestedName } : {}),
1778
+ },
1779
+ });
1780
+ if (!emitted) {
1781
+ emit.error({
1782
+ code: "lifecycle_event_failed",
1783
+ message: "the lifecycle event could not be recorded; heartbeat state was left unchanged",
1784
+ });
1785
+ process.exitCode = 1;
1786
+ return;
1787
+ }
1788
+
1789
+ emit.data({
1790
+ instance_id: myOwner,
1791
+ task_state: state,
1792
+ prior_state: priorState,
1793
+ reason: reason ?? null,
1794
+ changed: true,
1795
+ name_reminted: nameReminted,
1796
+ suggested_session_name: nameReminted ? suggestedName : null,
1797
+ git_finalization_checked: finalization !== null,
1798
+ ...(nameReminted
1799
+ ? {
1800
+ note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
1801
+ }
1802
+ : {}),
1803
+ });
1561
1804
  }
1562
1805
 
1563
1806
  function runSuggestName(
@@ -1624,7 +1867,7 @@ function runSuggestName(
1624
1867
  process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
1625
1868
  }
1626
1869
 
1627
- function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1870
+ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string }): void {
1628
1871
  const root = monorepoRoot();
1629
1872
  if (!root) {
1630
1873
  emit.error({
@@ -1656,20 +1899,23 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1656
1899
  process.exit(1);
1657
1900
  }
1658
1901
 
1659
- // Stamp .last_status_at = NOW. The verdict path reads state.status_checked
1660
- // canonical events (emitted below), but the legacy heartbeat field is still
1661
- // populated for back-compat with consumers reading v1 directly. The stamp
1662
- // goes through agent-coord (atomic write).
1663
- try {
1664
- const helper = agentCoordOrExit(root);
1665
- spawnSync(helper, ["stamp-status-call", myOwner], {
1666
- encoding: "utf8",
1667
- timeout: 2000,
1668
- ...coordHelperOpts(root),
1902
+ let finalization: GitFinalizationResult | null = null;
1903
+ if (opts.endTurn) {
1904
+ const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
1905
+ const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
1906
+ finalization = checkGitFinalization(root, touchedPaths, {
1907
+ claimHistoryComplete: history.complete,
1669
1908
  });
1670
- } catch {
1671
- /* non-fatal */
1909
+ if (!finalization.ok) {
1910
+ emit.error({
1911
+ code: "git_not_finalized",
1912
+ message: formatGitFinalizationFailure(finalization, resolveBinName(root)),
1913
+ });
1914
+ process.exitCode = 1;
1915
+ return;
1916
+ }
1672
1917
  }
1918
+
1673
1919
  emitCanonical({
1674
1920
  type: "state.status_checked",
1675
1921
  owner: myOwner,
@@ -1677,6 +1923,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1677
1923
  adapter: normalizeAdapter(hb.platform),
1678
1924
  data: {
1679
1925
  format: opts.json ? "json" : "box",
1926
+ git_finalization_checked: opts.endTurn === true,
1680
1927
  agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
1681
1928
  included_self: true,
1682
1929
  },
@@ -1763,6 +2010,12 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1763
2010
  instance_id: hb.instance_id,
1764
2011
  kind: normalizeKind(hb.kind),
1765
2012
  session_age_secs: ageSecs,
2013
+ activity: activityOf(hb),
2014
+ activity_updated_at: hb.activity_updated_at ?? null,
2015
+ activity_source: hb.activity_source ?? null,
2016
+ task_state: taskStateOf(hb),
2017
+ task_state_updated_at: hb.task_state_updated_at ?? null,
2018
+ task_state_reason: hb.task_state_reason ?? null,
1766
2019
  files_held: filesHeld,
1767
2020
  peers_live: livePeers.length,
1768
2021
  peers_stale: peersStale,
@@ -1776,6 +2029,9 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1776
2029
  agents: m.agents.map((a) => ({
1777
2030
  name: a.name || "unnamed",
1778
2031
  task: a.task ?? null,
2032
+ activity: a.activity ?? "unknown",
2033
+ task_state: a.task_state ?? "active",
2034
+ task_state_reason: a.task_state_reason ?? null,
1779
2035
  files: a.files_touched?.length ?? 0,
1780
2036
  })),
1781
2037
  })),
@@ -1784,6 +2040,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1784
2040
  context_window: ctxUsage?.window ?? null,
1785
2041
  timestamp_iso: new Date().toISOString(),
1786
2042
  timestamp_local: timeStr,
2043
+ ...(finalization ? { finalization } : {}),
1787
2044
  };
1788
2045
 
1789
2046
  if (opts.json) {
@@ -1794,6 +2051,8 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1794
2051
 
1795
2052
  const rows: Array<[string, string]> = [
1796
2053
  ["session", formatAge(ageSecs)],
2054
+ ["activity", activityOf(hb)],
2055
+ ["lifecycle", lifecycleLabel(hb)],
1797
2056
  ["context", ctxStr],
1798
2057
  ["files", filesStr],
1799
2058
  ["peers", peersStr],
@@ -2154,13 +2413,12 @@ function readCanonicalEventsInWindow(root: string, cutoffMs: number): CanonicalE
2154
2413
  return out;
2155
2414
  }
2156
2415
 
2157
- /** adapter ("claude-code") → legacy platform label ("claude_code") so the
2158
- * existing formatPlatformLabel rendering keeps working unchanged. */
2416
+ /** Normalize adapter event data into the heartbeat platform value. */
2159
2417
  function adapterToPlatform(adapter: string | undefined): string {
2160
- if (adapter === "claude-code") return "claude_code";
2418
+ if (adapter === "claude-code") return "claude-code";
2161
2419
  if (adapter === "cursor") return "cursor";
2162
2420
  if (adapter === "codex") return "codex";
2163
- return "claude_code";
2421
+ return "claude-code";
2164
2422
  }
2165
2423
 
2166
2424
  /** Project a canonical health.* event into the HealEvent shape the aggregators
@@ -2387,14 +2645,6 @@ interface HealthReport {
2387
2645
  by_platform: Record<string, number>;
2388
2646
  top_agents: Array<{ agent: string; count: number }>;
2389
2647
  };
2390
- schema_invalid: { count: number; samples: string[] };
2391
- commit_guards: {
2392
- blocked: number;
2393
- bypassed: number;
2394
- suppressed: number;
2395
- edit_blocked: number;
2396
- shell_candidates: number;
2397
- };
2398
2648
  councils: {
2399
2649
  active: number;
2400
2650
  archived_in_window: number;
@@ -2554,6 +2804,12 @@ function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
2554
2804
  case "state.task_set":
2555
2805
  detail = d.cleared ? "(cleared)" : clip(s("task"));
2556
2806
  break;
2807
+ case "state.task_state":
2808
+ detail = `${s("prior_state") || "active"} → ${s("state") || "active"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
2809
+ break;
2810
+ case "interaction.input_requested":
2811
+ detail = s("request_kind") || s("tool_name") || "operator input requested";
2812
+ break;
2557
2813
  case "state.status_checked":
2558
2814
  detail = "status box rendered";
2559
2815
  break;
@@ -2674,6 +2930,7 @@ function runTrace(
2674
2930
  }
2675
2931
 
2676
2932
  const events = byId.get(resolvedId) ?? [];
2933
+ const state = foldSessionState(events, { instance_id: resolvedId });
2677
2934
  const lines = events
2678
2935
  .map((ev) => traceLine(ev, !!opts.allTools))
2679
2936
  .filter((l): l is TraceEntry => l !== null)
@@ -2688,6 +2945,12 @@ function runTrace(
2688
2945
  instance_id: resolvedId,
2689
2946
  other_instances: candidateIds.filter((id) => id !== resolvedId),
2690
2947
  total_events: events.length,
2948
+ activity: state.activity,
2949
+ activity_updated_at: state.activity_updated_at ?? null,
2950
+ activity_source: state.activity_source ?? null,
2951
+ task_state: state.task_state,
2952
+ task_state_updated_at: state.task_state_updated_at ?? null,
2953
+ task_state_reason: state.task_state_reason ?? null,
2691
2954
  shown: shown.length,
2692
2955
  entries: shown,
2693
2956
  };
@@ -2699,6 +2962,9 @@ function runTrace(
2699
2962
  emit.data(result);
2700
2963
  const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
2701
2964
  process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
2965
+ process.stdout.write(
2966
+ ` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
2967
+ ); // lint-ok-emission: human trace view
2702
2968
  if (shown.length === 0) {
2703
2969
  process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
2704
2970
  return;
@@ -2736,18 +3002,7 @@ function runHealth(opts: { since: string; json?: boolean }): void {
2736
3002
 
2737
3003
  // Coordination telemetry reads from the canonical events.ndjson stream.
2738
3004
  // Heals come from health.*; council window-activity from council.*. The
2739
- // commit-guard + schema-invalid counters below have NO canonical equivalent
2740
- // yet. Their future home is the decision.* events (defined in schema.ts, not
2741
- // yet wired). Until then they report 0 (fields kept for output-shape
2742
- // compatibility).
2743
3005
  const heal: HealEvent[] = [];
2744
- const schemaInvalid = 0;
2745
- const schemaSamples: string[] = [];
2746
- const commitBlocked = 0;
2747
- const commitBypassed = 0;
2748
- const commitSuppressed = 0;
2749
- const editBlocked = 0;
2750
- const shellCandidates = 0;
2751
3006
  let councilAdvanced = 0;
2752
3007
  let councilClosed = 0;
2753
3008
  let councilArchived = 0;
@@ -2865,12 +3120,6 @@ function runHealth(opts: { since: string; json?: boolean }): void {
2865
3120
 
2866
3121
  // Anomaly detection.
2867
3122
  const anomalies: string[] = [];
2868
- if (schemaInvalid > 0) {
2869
- const sampleList = schemaSamples.slice(0, 3).join(", ");
2870
- anomalies.push(
2871
- `HEARTBEAT_SCHEMA_INVALID fired ${schemaInvalid}x; heartbeat shape failed validation${sampleList ? ` (samples: ${sampleList})` : ""}`,
2872
- );
2873
- }
2874
3123
  for (const { agent, count } of healTopAgents) {
2875
3124
  if (count >= 5) {
2876
3125
  anomalies.push(
@@ -2937,14 +3186,6 @@ function runHealth(opts: { since: string; json?: boolean }): void {
2937
3186
  by_platform: healByPlatform,
2938
3187
  top_agents: healTopAgents,
2939
3188
  },
2940
- schema_invalid: { count: schemaInvalid, samples: schemaSamples },
2941
- commit_guards: {
2942
- blocked: commitBlocked,
2943
- bypassed: commitBypassed,
2944
- suppressed: commitSuppressed,
2945
- edit_blocked: editBlocked,
2946
- shell_candidates: shellCandidates,
2947
- },
2948
3189
  councils: {
2949
3190
  active: activeCouncils,
2950
3191
  archived_in_window: councilArchived,
@@ -2988,17 +3229,6 @@ function renderHealthBox(report: HealthReport): void {
2988
3229
  const topHealer = report.heal_events.top_agents[0];
2989
3230
  const topHealerStr = topHealer ? `${topHealer.agent} x${topHealer.count}` : "(none)";
2990
3231
 
2991
- const guardParts: string[] = [];
2992
- if (report.commit_guards.blocked > 0) guardParts.push(`blocked ${report.commit_guards.blocked}`);
2993
- if (report.commit_guards.bypassed > 0)
2994
- guardParts.push(`bypassed ${report.commit_guards.bypassed}`);
2995
- if (report.commit_guards.suppressed > 0)
2996
- guardParts.push(`suppressed ${report.commit_guards.suppressed}`);
2997
- if (report.commit_guards.edit_blocked > 0)
2998
- guardParts.push(`edit-blocked ${report.commit_guards.edit_blocked}`);
2999
- if (report.commit_guards.shell_candidates > 0)
3000
- guardParts.push(`shell ${report.commit_guards.shell_candidates}`);
3001
-
3002
3232
  const councilParts: string[] = [`${report.councils.active} active`];
3003
3233
  if (report.councils.advanced_in_window > 0)
3004
3234
  councilParts.push(`${report.councils.advanced_in_window} advanced`);
@@ -3035,8 +3265,6 @@ function renderHealthBox(report: HealthReport): void {
3035
3265
  ? "0"
3036
3266
  : `${report.zombies.count} (${report.zombies.samples.join(", ")})`,
3037
3267
  ],
3038
- ["schema invalid", String(report.schema_invalid.count)],
3039
- ["commit guards", guardParts.length ? guardParts.join(", ") : "0"],
3040
3268
  ["councils", councilParts.join(", ")],
3041
3269
  ["anomalies", report.anomalies.length === 0 ? "(clean)" : `${report.anomalies.length} flagged`],
3042
3270
  ];
@@ -3070,10 +3298,10 @@ function runAdapterProbe(
3070
3298
  if (opts.json) emit.config({ format: "json" });
3071
3299
 
3072
3300
  const adapter = id.trim();
3073
- if (adapter !== "claude_code" && adapter !== "cursor") {
3301
+ if (adapter !== "claude-code" && adapter !== "cursor") {
3074
3302
  emit.error({
3075
3303
  code: "bad_adapter",
3076
- message: "adapter id must be claude_code or cursor",
3304
+ message: "adapter id must be claude-code or cursor",
3077
3305
  });
3078
3306
  process.exit(1);
3079
3307
  }
@@ -3293,7 +3521,7 @@ function replayAdapterSamples(
3293
3521
  stopFailure: "stop-failure",
3294
3522
  StopFailure: "stop-failure",
3295
3523
  };
3296
- const adapterFlag = adapter === "claude_code" ? "claude-code" : adapter;
3524
+ const adapterFlag = adapter;
3297
3525
 
3298
3526
  const sandbox = mkdtempSync(join(tmpdir(), "harn-adapter-probe-"));
3299
3527
  const results: SampleReplayResult[] = [];
@@ -3430,13 +3658,13 @@ function collectPath(value: string, prev: string[]): string[] {
3430
3658
  return [...prev, value];
3431
3659
  }
3432
3660
 
3433
- /** Parse "30", "30s", "5m", "1h", "2d" → ms. Bare integer defaults to minutes (back-compat). */
3661
+ /** Parse "30s", "5m", "1h", "2d" into milliseconds. */
3434
3662
  function parseDurationToMs(input: string): number | null {
3435
- const match = input.trim().match(/^(\d+)([smhd]?)$/i);
3663
+ const match = input.trim().match(/^(\d+)([smhd])$/i);
3436
3664
  if (!match) return null;
3437
3665
  const n = Number.parseInt(match[1], 10);
3438
3666
  if (!Number.isFinite(n) || n <= 0) return null;
3439
- const unit = (match[2] || "m").toLowerCase();
3667
+ const unit = match[2].toLowerCase();
3440
3668
  const mult: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
3441
3669
  return n * mult[unit];
3442
3670
  }