harnery 0.9.0 → 0.11.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 (211) hide show
  1. package/README.md +1 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +6 -0
  4. package/dist/commands/agents.d.ts +24 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +244 -13
  7. package/dist/commands/backup.d.ts +5 -4
  8. package/dist/commands/backup.d.ts.map +1 -1
  9. package/dist/commands/backup.js +15 -14
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +27 -1
  12. package/dist/commands/claude-desktop.d.ts +19 -0
  13. package/dist/commands/claude-desktop.d.ts.map +1 -0
  14. package/dist/commands/claude-desktop.js +168 -0
  15. package/dist/commands/context.d.ts.map +1 -1
  16. package/dist/commands/context.js +149 -1
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +2 -3
  19. package/dist/commands/eml.d.ts +32 -0
  20. package/dist/commands/eml.d.ts.map +1 -1
  21. package/dist/commands/eml.js +16 -3
  22. package/dist/commands/harness.d.ts +11 -0
  23. package/dist/commands/harness.d.ts.map +1 -0
  24. package/dist/commands/harness.js +118 -0
  25. package/dist/commands/presence.d.ts +9 -4
  26. package/dist/commands/presence.d.ts.map +1 -1
  27. package/dist/commands/presence.js +88 -5
  28. package/dist/commands/relay.d.ts +9 -0
  29. package/dist/commands/relay.d.ts.map +1 -0
  30. package/dist/commands/relay.js +143 -0
  31. package/dist/commands/sync.d.ts.map +1 -1
  32. package/dist/commands/sync.js +5 -0
  33. package/dist/commands/workflow.d.ts.map +1 -1
  34. package/dist/commands/workflow.js +53 -17
  35. package/dist/core/agents/canonical-emit.d.ts +15 -0
  36. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  37. package/dist/core/agents/canonical-emit.js +41 -3
  38. package/dist/core/agents/cli.js +4 -2
  39. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/prompt-context.js +44 -3
  41. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  42. package/dist/core/agents/render/session-context.js +26 -1
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +4 -37
  45. package/dist/core/agents/session-events.d.ts.map +1 -1
  46. package/dist/core/agents/session-events.js +16 -43
  47. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  48. package/dist/core/agents/state/heartbeat-projector.js +11 -2
  49. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  50. package/dist/core/agents/state/heartbeat-writer.js +3 -3
  51. package/dist/core/agents/state/names.d.ts +38 -3
  52. package/dist/core/agents/state/names.d.ts.map +1 -1
  53. package/dist/core/agents/state/names.js +46 -7
  54. package/dist/core/agents/state/pidmap.d.ts +5 -1
  55. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  56. package/dist/core/agents/state/pidmap.js +58 -2
  57. package/dist/core/agents/state/stale-sweep.d.ts +3 -2
  58. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  59. package/dist/core/agents/state/stale-sweep.js +6 -6
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +191 -25
  63. package/dist/core/context/index.d.ts +144 -0
  64. package/dist/core/context/index.d.ts.map +1 -0
  65. package/dist/core/context/index.js +380 -0
  66. package/dist/core/harnesses/bench.d.ts +29 -0
  67. package/dist/core/harnesses/bench.d.ts.map +1 -0
  68. package/dist/core/harnesses/bench.js +151 -0
  69. package/dist/core/harnesses/index.d.ts +9 -0
  70. package/dist/core/harnesses/index.d.ts.map +1 -0
  71. package/dist/core/harnesses/index.js +4 -0
  72. package/dist/core/harnesses/profiles.d.ts +62 -0
  73. package/dist/core/harnesses/profiles.d.ts.map +1 -0
  74. package/dist/core/harnesses/profiles.js +115 -0
  75. package/dist/core/harnesses/registry.d.ts +15 -0
  76. package/dist/core/harnesses/registry.d.ts.map +1 -0
  77. package/dist/core/harnesses/registry.js +113 -0
  78. package/dist/core/harnesses/types.d.ts +64 -0
  79. package/dist/core/harnesses/types.d.ts.map +1 -0
  80. package/dist/core/harnesses/types.js +20 -0
  81. package/dist/core/hooks/cli.js +280 -63
  82. package/dist/core/hooks/events/schema.d.ts +38 -1
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  85. package/dist/core/hooks/harness/events.js +3 -0
  86. package/dist/core/hooks/harness/parse.d.ts +1 -1
  87. package/dist/core/hooks/harness/parse.d.ts.map +1 -1
  88. package/dist/core/hooks/harness/parse.js +4 -0
  89. package/dist/core/presence/blob.d.ts +40 -0
  90. package/dist/core/presence/blob.d.ts.map +1 -0
  91. package/dist/core/presence/blob.js +91 -0
  92. package/dist/core/presence/git.d.ts +68 -0
  93. package/dist/core/presence/git.d.ts.map +1 -0
  94. package/dist/core/presence/git.js +173 -0
  95. package/dist/core/presence/index.d.ts +74 -0
  96. package/dist/core/presence/index.d.ts.map +1 -0
  97. package/dist/core/presence/index.js +224 -0
  98. package/dist/core/presence/relay-client.d.ts +39 -0
  99. package/dist/core/presence/relay-client.d.ts.map +1 -0
  100. package/dist/core/presence/relay-client.js +295 -0
  101. package/dist/core/presence/relay-protocol.d.ts +94 -0
  102. package/dist/core/presence/relay-protocol.d.ts.map +1 -0
  103. package/dist/core/presence/relay-protocol.js +169 -0
  104. package/dist/core/workflow/billing.d.ts +1 -1
  105. package/dist/core/workflow/billing.d.ts.map +1 -1
  106. package/dist/core/workflow/billing.js +7 -1
  107. package/dist/core/workflow/engine.d.ts +5 -0
  108. package/dist/core/workflow/engine.d.ts.map +1 -1
  109. package/dist/core/workflow/engine.js +164 -16
  110. package/dist/core/workflow/harnesses.d.ts +3 -3
  111. package/dist/core/workflow/harnesses.d.ts.map +1 -1
  112. package/dist/core/workflow/harnesses.js +8 -17
  113. package/dist/core/workflow/index.d.ts +5 -0
  114. package/dist/core/workflow/index.d.ts.map +1 -0
  115. package/dist/core/workflow/index.js +3 -0
  116. package/dist/core/workflow/proof.d.ts +42 -0
  117. package/dist/core/workflow/proof.d.ts.map +1 -0
  118. package/dist/core/workflow/proof.js +342 -0
  119. package/dist/core/workflow/spawn-claude.d.ts +4 -1
  120. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  121. package/dist/core/workflow/spawn-claude.js +39 -18
  122. package/dist/core/workflow/spawn-codex.d.ts +4 -1
  123. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  124. package/dist/core/workflow/spawn-codex.js +44 -22
  125. package/dist/core/workflow/spawn-cursor.d.ts +4 -1
  126. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  127. package/dist/core/workflow/spawn-cursor.js +35 -16
  128. package/dist/core/workflow/types.d.ts +156 -2
  129. package/dist/core/workflow/types.d.ts.map +1 -1
  130. package/dist/core/workflow/types.js +1 -1
  131. package/dist/lib/browser/client.d.ts +16 -0
  132. package/dist/lib/browser/client.d.ts.map +1 -1
  133. package/dist/lib/browser/client.js +22 -0
  134. package/dist/lib/browser/index.d.ts +1 -0
  135. package/dist/lib/browser/index.d.ts.map +1 -1
  136. package/dist/lib/browser/index.js +1 -0
  137. package/dist/lib/browser/launch-args.d.ts +21 -0
  138. package/dist/lib/browser/launch-args.d.ts.map +1 -0
  139. package/dist/lib/browser/launch-args.js +32 -0
  140. package/dist/lib/claude-desktop.d.ts +105 -0
  141. package/dist/lib/claude-desktop.d.ts.map +1 -0
  142. package/dist/lib/claude-desktop.js +217 -0
  143. package/dist/lib/docs-lint.d.ts.map +1 -1
  144. package/dist/lib/docs-lint.js +5 -2
  145. package/dist/lib/identities/assume.d.ts +51 -0
  146. package/dist/lib/identities/assume.d.ts.map +1 -0
  147. package/dist/lib/identities/assume.js +274 -0
  148. package/dist/lib/identities/index.d.ts +7 -7
  149. package/dist/lib/identities/index.d.ts.map +1 -1
  150. package/dist/lib/identities/index.js +24 -24
  151. package/dist/lib/instructions/templates.d.ts.map +1 -1
  152. package/dist/lib/instructions/templates.js +15 -2
  153. package/package.json +22 -6
  154. package/schemas/config.schema.json +88 -19
  155. package/src/commander.ts +6 -0
  156. package/src/commands/agents.ts +280 -14
  157. package/src/commands/backup.ts +17 -16
  158. package/src/commands/browse.ts +32 -0
  159. package/src/commands/claude-desktop.ts +215 -0
  160. package/src/commands/context.ts +174 -1
  161. package/src/commands/doctor.ts +2 -3
  162. package/src/commands/eml.ts +22 -4
  163. package/src/commands/harness.ts +147 -0
  164. package/src/commands/presence.ts +111 -5
  165. package/src/commands/relay.ts +160 -0
  166. package/src/commands/sync.ts +4 -0
  167. package/src/commands/workflow.ts +62 -19
  168. package/src/core/agents/canonical-emit.ts +41 -3
  169. package/src/core/agents/cli.ts +4 -2
  170. package/src/core/agents/render/prompt-context.ts +48 -3
  171. package/src/core/agents/render/session-context.ts +27 -1
  172. package/src/core/agents/rules/commit-conflict.ts +4 -33
  173. package/src/core/agents/session-events.ts +15 -39
  174. package/src/core/agents/state/heartbeat-projector.ts +10 -1
  175. package/src/core/agents/state/heartbeat-writer.ts +3 -2
  176. package/src/core/agents/state/names.ts +68 -9
  177. package/src/core/agents/state/pidmap.ts +59 -2
  178. package/src/core/agents/state/stale-sweep.ts +6 -11
  179. package/src/core/config.ts +248 -24
  180. package/src/core/context/index.ts +575 -0
  181. package/src/core/harnesses/bench.ts +214 -0
  182. package/src/core/harnesses/index.ts +31 -0
  183. package/src/core/harnesses/profiles.ts +141 -0
  184. package/src/core/harnesses/registry.ts +140 -0
  185. package/src/core/harnesses/types.ts +90 -0
  186. package/src/core/hooks/cli.ts +331 -62
  187. package/src/core/hooks/events/schema.ts +68 -0
  188. package/src/core/hooks/harness/events.ts +3 -0
  189. package/src/core/hooks/harness/parse.ts +7 -1
  190. package/src/core/presence/blob.ts +125 -0
  191. package/src/core/presence/git.ts +191 -0
  192. package/src/core/presence/index.ts +274 -0
  193. package/src/core/presence/relay-client.ts +322 -0
  194. package/src/core/presence/relay-protocol.ts +246 -0
  195. package/src/core/workflow/billing.ts +8 -2
  196. package/src/core/workflow/engine.ts +182 -16
  197. package/src/core/workflow/harnesses.ts +14 -19
  198. package/src/core/workflow/index.ts +43 -0
  199. package/src/core/workflow/proof.ts +434 -0
  200. package/src/core/workflow/spawn-claude.ts +39 -20
  201. package/src/core/workflow/spawn-codex.ts +47 -28
  202. package/src/core/workflow/spawn-cursor.ts +37 -18
  203. package/src/core/workflow/types.ts +176 -2
  204. package/src/lib/browser/client.ts +37 -0
  205. package/src/lib/browser/index.ts +1 -0
  206. package/src/lib/browser/launch-args.ts +33 -0
  207. package/src/lib/claude-desktop.ts +293 -0
  208. package/src/lib/docs-lint.ts +5 -2
  209. package/src/lib/identities/assume.ts +363 -0
  210. package/src/lib/identities/index.ts +28 -24
  211. package/src/lib/instructions/templates.ts +16 -2
package/README.md CHANGED
@@ -20,6 +20,7 @@
20
20
  `harnery` keeps multiple AI coding agents from stepping on each other in a shared checkout. It was extracted from years of running several Claude Code / Cursor / Codex sessions at once against the same monorepo:
21
21
 
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
+ - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
23
24
  - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages.
24
25
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
25
26
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
@@ -1 +1 @@
1
- {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkCpC,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEnD,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACjE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,KAAK,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACpE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,WAAW,EAAE,WAkCzB,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CA8C3E"}
1
+ {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
package/dist/commander.js CHANGED
@@ -22,6 +22,7 @@ import { registerBackupCommand } from "./commands/backup.js";
22
22
  import { registerBrowseCommand } from "./commands/browse.js";
23
23
  import { registerBrowseAiCommand } from "./commands/browse-ai.js";
24
24
  import { registerCallersCommand } from "./commands/callers.js";
25
+ import { registerClaudeDesktopCommand } from "./commands/claude-desktop.js";
25
26
  import { registerCompletionCommand } from "./commands/completion.js";
26
27
  import { registerConfigGetCommand } from "./commands/config-get.js";
27
28
  import { registerContextCommand } from "./commands/context.js";
@@ -37,10 +38,12 @@ import { registerEnvCommand } from "./commands/env.js";
37
38
  import { registerFetchCommand } from "./commands/fetch.js";
38
39
  import { registerFileHistoryCommand } from "./commands/file-history.js";
39
40
  import { registerGrepCommand } from "./commands/grep.js";
41
+ import { registerHarnessCommand } from "./commands/harness.js";
40
42
  import { registerInitCommand } from "./commands/init.js";
41
43
  import { registerOutlineCommand } from "./commands/outline.js";
42
44
  import { registerPresenceCommand } from "./commands/presence.js";
43
45
  import { registerReadCommand } from "./commands/read.js";
46
+ import { registerRelayCommand } from "./commands/relay.js";
44
47
  import { registerScratchCommand } from "./commands/scratch.js";
45
48
  import { registerSessionCommand } from "./commands/session.js";
46
49
  import { registerSyncCommand } from "./commands/sync.js";
@@ -96,6 +99,7 @@ export function createHarneryProgram(opts = {}) {
96
99
  registerEmlCommand(program, emit);
97
100
  registerEnvCommand(program, emit, opts.context);
98
101
  registerPresenceCommand(program, emit);
102
+ registerRelayCommand(program, emit);
99
103
  registerConfigGetCommand(program, emit);
100
104
  registerFileHistoryCommand(program, emit);
101
105
  registerOutlineCommand(program, emit);
@@ -104,6 +108,7 @@ export function createHarneryProgram(opts = {}) {
104
108
  registerCallersCommand(program, emit, opts.context);
105
109
  registerEditBatchCommand(program, emit);
106
110
  registerGrepCommand(program, emit, opts.context);
111
+ registerHarnessCommand(program, emit);
107
112
  registerCookiesCommand(program, emit);
108
113
  registerFetchCommand(program, emit, opts.context);
109
114
  registerReadCommand(program, emit);
@@ -122,6 +127,7 @@ export function createHarneryProgram(opts = {}) {
122
127
  registerInitCommand(program, emit, opts.binName);
123
128
  registerDeinitCommand(program, emit, opts.binName);
124
129
  registerBackupCommand(program, emit);
130
+ registerClaudeDesktopCommand(program, emit);
125
131
  registerSyncCommand(program, emit);
126
132
  registerWorkflowCommand(program, emit);
127
133
  if (include("web"))
@@ -14,5 +14,29 @@
14
14
  */
15
15
  import type { Command } from "commander";
16
16
  import type { EmitContext } from "../commander.js";
17
+ /**
18
+ * Spawn options for agent-coord child processes: pin the coord root the
19
+ * command already resolved (git-superproject-aware via `monorepoRoot()`), so
20
+ * the helper can't re-resolve a DIFFERENT root by walking up from a drifted
21
+ * shell cwd. The concrete failure this prevents: a shell cd'd into an
22
+ * embedded harnery checkout (which carries its own committed `.harnery/`)
23
+ * made agent-coord resolve that nested root and miss the session's real
24
+ * heartbeat — `set-task: no heartbeat at .harnery/active/<id>.json` while
25
+ * `status` (which resolves in-process) worked fine. Mirrors the hooks side's
26
+ * `childEnv()`; every agent-coord spawn must carry this.
27
+ */
28
+ export declare function coordHelperOpts(root: string): {
29
+ cwd: string;
30
+ env: NodeJS.ProcessEnv;
31
+ };
17
32
  export declare function registerAgentsCommand(program: Command, emitParam: EmitContext): void;
33
+ /**
34
+ * Build the copy-pasteable session name from the coord identity + the agent's
35
+ * description parts. Pure (no coord-state reads) so it's unit-testable; collapses
36
+ * internal whitespace and trims. Returns null when the description is empty.
37
+ */
38
+ export declare function buildSuggestedName(agentName: string, descriptionParts: string[]): {
39
+ suggestedName: string;
40
+ description: string;
41
+ } | null;
18
42
  //# sourceMappingURL=agents.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAwFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA4OpF"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAyEnD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,CAErF;AAgCD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA6PpF;AA0iCD;;;;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"}
@@ -18,7 +18,8 @@ import { homedir, tmpdir } from "node:os";
18
18
  import { basename, join, resolve } from "node:path";
19
19
  import { readStreamTailBounded } from "../core/agents/events/consume.js";
20
20
  import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, } from "../core/agents/index.js";
21
- import { resolveBinName } from "../core/config.js";
21
+ import { coordFreshnessSeconds, resolveBinName } from "../core/config.js";
22
+ import { readRemoteMachines } from "../core/presence/index.js";
22
23
  /** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
23
24
  * under V8's ~512MB max string length so a `readFileSync` of the whole file can
24
25
  * never throw; covers ample recent history for a diagnostic scan. */
@@ -26,11 +27,34 @@ const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
26
27
  import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
27
28
  import { appendEntry, resolveOwnerByName } from "../core/scratch/index.js";
28
29
  import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
30
+ import { assumeIdentity, IdentityAssumeError } from "../lib/identities/assume.js";
29
31
  import { displayName as displayAgentName, ensureIdentity, listIdentities, lookupById as lookupIdentityById, lookupByName as lookupIdentityByName, } from "../lib/identities/index.js";
30
- const FRESHNESS_SECS = 600; // 10-minute heartbeat-freshness window.
32
+ /**
33
+ * Heartbeat-freshness window (seconds). Reads `coord.freshness_seconds` /
34
+ * `HARNERY_AGENT_COORD_FRESHNESS` via the shared accessor (defaults to 600 =
35
+ * 10 min). A function, not a const, so a config edit takes effect without a
36
+ * process restart (mtime-cached).
37
+ */
38
+ function freshnessCutoffSecs() {
39
+ return coordFreshnessSeconds();
40
+ }
31
41
  const SUBAGENT_NOTE = "Bash identity is process-level in v1; if you're running inside a subagent, " +
32
42
  "this resolves to the parent group's name, not the subagent's. A subagent-aware " +
33
43
  "bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
44
+ /**
45
+ * Spawn options for agent-coord child processes: pin the coord root the
46
+ * command already resolved (git-superproject-aware via `monorepoRoot()`), so
47
+ * the helper can't re-resolve a DIFFERENT root by walking up from a drifted
48
+ * shell cwd. The concrete failure this prevents: a shell cd'd into an
49
+ * embedded harnery checkout (which carries its own committed `.harnery/`)
50
+ * made agent-coord resolve that nested root and miss the session's real
51
+ * heartbeat — `set-task: no heartbeat at .harnery/active/<id>.json` while
52
+ * `status` (which resolves in-process) worked fine. Mirrors the hooks side's
53
+ * `childEnv()`; every agent-coord spawn must carry this.
54
+ */
55
+ export function coordHelperOpts(root) {
56
+ return { cwd: root, env: { ...process.env, HARNERY_COORD_ROOT_OVERRIDE: root } };
57
+ }
34
58
  function formatPlatformLabel(platform) {
35
59
  if (platform === "cursor")
36
60
  return "Cursor";
@@ -70,6 +94,17 @@ export function registerAgentsCommand(program, emitParam) {
70
94
  .action((opts) => {
71
95
  runStatus(opts);
72
96
  });
97
+ cmd
98
+ .command("suggest-name [description...]")
99
+ .description('Reprint a session name ("Agent <you> - <description>") for the operator to set as their ' +
100
+ "harness tab title. With no arg, derives it from your current task. The primary naming path " +
101
+ "is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
102
+ "or re-suggest after a topic pivot. Read-only.")
103
+ .option("--json", "JSON output instead of the bare name")
104
+ .option("--session-id <id>", "Lookup heartbeat by session_id directly, bypassing the ppid walk.")
105
+ .action((description, opts) => {
106
+ runSuggestName(description, opts);
107
+ });
73
108
  cmd
74
109
  .command("watch")
75
110
  .description("Stream peer state changes in real time (file watcher on .harnery/active/). " +
@@ -279,6 +314,59 @@ function registerIdentityCommands(parent) {
279
314
  meta: { action: "identity-ensure" },
280
315
  });
281
316
  });
317
+ identity
318
+ .command("assume <name-or-id>")
319
+ .description("Bind this live session to a durable persona. Reclaims an abandoned " +
320
+ "local namesake (no live process); refuses when another live process " +
321
+ "or known remote session still holds the name. Updates name history, " +
322
+ "the event ledger, and the heartbeat.")
323
+ .option("--json", "JSON envelope output")
324
+ .option("--session-id <id>", "Bind the heartbeat with this session_id directly, bypassing the ppid walk.")
325
+ .action((target, opts) => {
326
+ runIdentityAssume(target, opts);
327
+ });
328
+ }
329
+ function runIdentityAssume(target, opts) {
330
+ if (opts.json)
331
+ emit.config({ format: "json" });
332
+ const root = monorepoRoot();
333
+ if (!root) {
334
+ emit.error({
335
+ code: "not_in_repo",
336
+ message: "not in an agent session; coord_root() returned null",
337
+ });
338
+ process.exit(1);
339
+ }
340
+ if (!opts.sessionId)
341
+ ensureCursorSession(root);
342
+ const owner = opts.sessionId ?? resolveOwner();
343
+ if (!owner) {
344
+ emit.error({
345
+ code: "no_pidmap_entry",
346
+ message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
347
+ });
348
+ process.exit(1);
349
+ }
350
+ try {
351
+ const result = assumeIdentity(root, owner, target);
352
+ emit.data({
353
+ ...result,
354
+ display_name: displayAgentName(result.name),
355
+ action: "identity-assume",
356
+ });
357
+ }
358
+ catch (error) {
359
+ if (error instanceof IdentityAssumeError) {
360
+ emit.error({ code: error.code, message: error.message });
361
+ }
362
+ else {
363
+ emit.error({
364
+ code: "identity_assume_failed",
365
+ message: error instanceof Error ? error.message : String(error),
366
+ });
367
+ }
368
+ process.exit(1);
369
+ }
282
370
  }
283
371
  function registerCouncilCommands(parent) {
284
372
  const council = parent
@@ -454,6 +542,7 @@ function runWhoami(opts) {
454
542
  }
455
543
  const row = {
456
544
  name: hb.name || "unknown",
545
+ agent_id: hb.agent_id || null,
457
546
  instance_id: hb.instance_id,
458
547
  session_id: hb.session_id,
459
548
  kind: normalizeKind(hb.kind),
@@ -512,7 +601,7 @@ function runList(opts) {
512
601
  }
513
602
  // Apply staleness filter unless --stale.
514
603
  const nowSec = Math.floor(Date.now() / 1000);
515
- const cutoff = nowSec - FRESHNESS_SECS;
604
+ const cutoff = nowSec - freshnessCutoffSecs();
516
605
  const live = opts.stale
517
606
  ? heartbeats
518
607
  : heartbeats.filter((h) => {
@@ -542,6 +631,7 @@ function runList(opts) {
542
631
  // Orphan transient case
543
632
  rows.push({
544
633
  name: h.name || "unknown",
634
+ agent_id: h.agent_id || null,
545
635
  instance_id: h.instance_id,
546
636
  session_id: h.session_id,
547
637
  kind: "transient",
@@ -566,6 +656,7 @@ function runList(opts) {
566
656
  }
567
657
  rows.push({
568
658
  name: h.name || "unknown",
659
+ agent_id: h.agent_id || null,
569
660
  instance_id: h.instance_id,
570
661
  session_id: h.session_id,
571
662
  kind: normalizeKind(h.kind),
@@ -585,6 +676,30 @@ function runList(opts) {
585
676
  // (legacy zombies), and an unguarded .localeCompare throws, which is exactly
586
677
  // what made `harn agents list --all --stale` crash.
587
678
  rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
679
+ // Cross-machine presence (ADR 0016): append sessions on OTHER machines from
680
+ // the locally-fetched presence refs. Advisory rows (relation=remote) — they
681
+ // don't participate in local claim blocking in v1.
682
+ for (const rm of readRemoteMachines(root)) {
683
+ for (const a of rm.agents) {
684
+ rows.push({
685
+ name: a.name || "unknown",
686
+ instance_id: a.instance_id,
687
+ session_id: a.session_id ?? a.instance_id,
688
+ kind: normalizeKind(a.kind),
689
+ relation: "remote",
690
+ started_at: a.started_at ?? "",
691
+ last_heartbeat: a.last_heartbeat ?? "",
692
+ files_touched: [...(a.files_touched ?? [])].sort(),
693
+ last_tool: a.last_tool ?? null,
694
+ last_tool_target: null,
695
+ task: a.task ?? null,
696
+ turn_summary: a.turn_summary ?? null,
697
+ turn_summary_updated_at: null,
698
+ platform: a.platform ?? "claude_code",
699
+ machine: rm.machine,
700
+ });
701
+ }
702
+ }
588
703
  // Emit. JSON format gets {rows, note}; TTY gets the rows with note as a footnote.
589
704
  emit.data({ rows, note: SUBAGENT_NOTE });
590
705
  if (process.stdout.isTTY && !opts.json) {
@@ -691,7 +806,7 @@ async function runWatch(pollMs) {
691
806
  function listActiveHeartbeats(activeDir) {
692
807
  const out = [];
693
808
  const nowSec = Math.floor(Date.now() / 1000);
694
- const cutoff = nowSec - FRESHNESS_SECS;
809
+ const cutoff = nowSec - freshnessCutoffSecs();
695
810
  for (const file of readdirSync(activeDir)) {
696
811
  if (!file.endsWith(".json"))
697
812
  continue;
@@ -735,7 +850,7 @@ async function runShow(name, opts) {
735
850
  // Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
736
851
  const matches = [];
737
852
  const nowSec = Math.floor(Date.now() / 1000);
738
- const cutoff = nowSec - FRESHNESS_SECS;
853
+ const cutoff = nowSec - freshnessCutoffSecs();
739
854
  for (const file of readdirSync(activeDir)) {
740
855
  if (!file.endsWith(".json"))
741
856
  continue;
@@ -885,6 +1000,12 @@ function shouldBootstrapCursorSession() {
885
1000
  function ensureCursorSession(root) {
886
1001
  if (!shouldBootstrapCursorSession())
887
1002
  return;
1003
+ // Explicit owner override means the caller already knows who they are.
1004
+ // Bootstrapping sessionStart inherits that env into the child hook, which
1005
+ // re-projects a fresh heartbeat onto the override owner and can wipe an
1006
+ // assumed persona agent_id (seen under Cursor integration fixtures).
1007
+ if (process.env.HARNERY_AGENT_COORD_OWNER?.trim())
1008
+ return;
888
1009
  if (resolveOwnerBySessionEnv(root))
889
1010
  return;
890
1011
  const sessionId = cursorEnvSessionId();
@@ -941,6 +1062,7 @@ function runReleaseClaim(path) {
941
1062
  const helper = resolve(root, "harnery", "bin", "agent-coord");
942
1063
  const result = spawnSync(helper, ["release-claim", myOwner, canonical], {
943
1064
  encoding: "utf8",
1065
+ ...coordHelperOpts(root),
944
1066
  });
945
1067
  if (result.status !== 0) {
946
1068
  emit.error({
@@ -973,10 +1095,20 @@ function runSetTask(task, opts) {
973
1095
  });
974
1096
  process.exit(1);
975
1097
  }
1098
+ // Snapshot BEFORE the mutation: a heartbeat that has never carried a task
1099
+ // has no `task_updated_at` stamp (setTask always stamps it, even on a clear),
1100
+ // so its absence is the "focus never declared this session" signal — i.e. the
1101
+ // first set-task of the session. That's the natural moment to also hand the
1102
+ // operator a session name built from the same declaration (one source of
1103
+ // truth: the focus you declare names the session). Distinct from task===""
1104
+ // (a deliberate clear), which carries a stamp and does NOT re-trigger.
1105
+ const priorHb = readHeartbeat(myOwner);
1106
+ const firstOfSession = !priorHb?.task_updated_at;
976
1107
  // Heartbeat mutation goes through agent-coord (atomic temp+rename).
977
1108
  const helper = resolve(root, "harnery", "bin", "agent-coord");
978
1109
  const result = spawnSync(helper, ["set-task", myOwner, task], {
979
1110
  encoding: "utf8",
1111
+ ...coordHelperOpts(root),
980
1112
  });
981
1113
  if (result.status !== 0) {
982
1114
  emit.error({
@@ -993,13 +1125,87 @@ function runSetTask(task, opts) {
993
1125
  harness: normalizeHarness(hb?.platform),
994
1126
  data: { task, cleared: !task || task.length === 0 },
995
1127
  });
1128
+ // On the first focus declaration of a session, surface a suggested session
1129
+ // name for the operator to set as their harness tab title. Read the result
1130
+ // (`suggested_session_name`) and reproduce it in a fenced code block so the
1131
+ // UI's Copy button hands over the exact string. null on every later call.
1132
+ const built = firstOfSession && !opts?.sessionId ? buildSuggestedName(hb?.name ?? "unknown", [task]) : null;
996
1133
  emit.data({
997
1134
  instance_id: myOwner,
998
1135
  name: hb?.name ?? null,
999
1136
  task: hb?.task ?? null,
1000
1137
  cleared: !task || task.length === 0,
1138
+ first_of_session: firstOfSession,
1139
+ suggested_session_name: built?.suggestedName ?? null,
1001
1140
  });
1002
1141
  }
1142
+ /**
1143
+ * Build the copy-pasteable session name from the coord identity + the agent's
1144
+ * description parts. Pure (no coord-state reads) so it's unit-testable; collapses
1145
+ * internal whitespace and trims. Returns null when the description is empty.
1146
+ */
1147
+ export function buildSuggestedName(agentName, descriptionParts) {
1148
+ const description = descriptionParts.join(" ").replace(/\s+/g, " ").trim();
1149
+ if (!description)
1150
+ return null;
1151
+ const name = agentName?.trim() || "unknown";
1152
+ return { suggestedName: `Agent ${name} - ${description}`, description };
1153
+ }
1154
+ function runSuggestName(descriptionParts, opts) {
1155
+ const root = monorepoRoot();
1156
+ if (!root) {
1157
+ emit.error({
1158
+ code: "not_in_repo",
1159
+ message: "not in an agent session; coord_root() returned null",
1160
+ });
1161
+ process.exit(1);
1162
+ }
1163
+ // Identity: prefer explicit --session-id (ppid-walk-free escape hatch, mirrors
1164
+ // `status`/`set-task`), fall back to the ppid walk for interactive shell use.
1165
+ if (!opts.sessionId)
1166
+ ensureCursorSession(root);
1167
+ const myOwner = opts.sessionId ?? resolveOwner();
1168
+ if (!myOwner) {
1169
+ emit.error({
1170
+ code: "no_pidmap_entry",
1171
+ message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
1172
+ });
1173
+ process.exit(1);
1174
+ }
1175
+ const hb = readHeartbeat(myOwner);
1176
+ const agentName = hb?.name || "unknown";
1177
+ // Description resolution: an explicit arg wins; with no arg, fall back to the
1178
+ // agent's current declared task, so a bare `suggest-name` reprints the running
1179
+ // session's name (handy after a topic pivot). Read-only — mutates no state;
1180
+ // the primary naming path is set-task's first-of-session suggestion.
1181
+ const parts = descriptionParts.length > 0 ? descriptionParts : hb?.task ? [hb.task] : [];
1182
+ const built = buildSuggestedName(agentName, parts);
1183
+ if (!built) {
1184
+ emit.error({
1185
+ code: "no_description",
1186
+ message: 'pass a 2-5 word topic (e.g. suggest-name "Auth Refactor"), or declare one first with set-task',
1187
+ });
1188
+ process.exit(1);
1189
+ }
1190
+ const { suggestedName, description } = built;
1191
+ const displayName = `agent-${agentName}`;
1192
+ if (opts.json) {
1193
+ emit.config({ format: "json" });
1194
+ emit.data({
1195
+ name: displayName,
1196
+ suggested_session_name: suggestedName,
1197
+ agent_name: agentName,
1198
+ description,
1199
+ });
1200
+ return;
1201
+ }
1202
+ // Emit the bare name only — no box. The agent reproduces this exact string in
1203
+ // a fenced code block in its reply, which every chat UI decorates with a
1204
+ // one-click Copy button so the operator can grab it without hand-selecting.
1205
+ // (A drawn box would copy its borders + labels too.) Direct write, not
1206
+ // emit.text: runs via Bash with no TTY, same chat-paste contract as runStatus.
1207
+ process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
1208
+ }
1003
1209
  function runStatus(opts) {
1004
1210
  const root = monorepoRoot();
1005
1211
  if (!root) {
@@ -1038,6 +1244,7 @@ function runStatus(opts) {
1038
1244
  spawnSync(helper, ["stamp-status-call", myOwner], {
1039
1245
  encoding: "utf8",
1040
1246
  timeout: 2000,
1247
+ ...coordHelperOpts(root),
1041
1248
  });
1042
1249
  }
1043
1250
  catch {
@@ -1060,7 +1267,7 @@ function runStatus(opts) {
1060
1267
  : 0;
1061
1268
  const activeDir = resolve(root, ".harnery", "active");
1062
1269
  const nowSec = Math.floor(Date.now() / 1000);
1063
- const cutoff = nowSec - FRESHNESS_SECS;
1270
+ const cutoff = nowSec - freshnessCutoffSecs();
1064
1271
  const livePeers = [];
1065
1272
  let peersStale = 0;
1066
1273
  if (existsSync(activeDir)) {
@@ -1097,7 +1304,9 @@ function runStatus(opts) {
1097
1304
  });
1098
1305
  const filesHeld = hb.files_touched ?? [];
1099
1306
  const filesStr = formatList(filesHeld.map((p) => basename(p)), 4, "0 held");
1100
- const peersStr = formatPeers(livePeers, 4, peersStale);
1307
+ // Cross-machine presence (ADR 0016): sessions on other machines, advisory.
1308
+ const remoteMachines = readRemoteMachines(root);
1309
+ const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
1101
1310
  const ctxUsage = readContextUsage(hb.session_id, hb.platform);
1102
1311
  let ctxStr;
1103
1312
  if (!ctxUsage) {
@@ -1136,6 +1345,15 @@ function runStatus(opts) {
1136
1345
  name: p.name || "unnamed",
1137
1346
  files: p.files_touched?.length ?? 0,
1138
1347
  })),
1348
+ remote_machines: remoteMachines.map((m) => ({
1349
+ machine: m.machine,
1350
+ age_secs: m.age_secs,
1351
+ agents: m.agents.map((a) => ({
1352
+ name: a.name || "unnamed",
1353
+ task: a.task ?? null,
1354
+ files: a.files_touched?.length ?? 0,
1355
+ })),
1356
+ })),
1139
1357
  pending_councils: pendingCouncils,
1140
1358
  context_used: ctxUsage?.used ?? null,
1141
1359
  context_window: ctxUsage?.window ?? null,
@@ -1183,8 +1401,8 @@ function formatList(items, cap, emptyLabel) {
1183
1401
  const shown = items.slice(0, cap).join(", ");
1184
1402
  return `${shown}, +${items.length - cap} more`;
1185
1403
  }
1186
- function formatPeers(peers, cap, staleCount) {
1187
- if (peers.length === 0 && staleCount === 0)
1404
+ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
1405
+ if (peers.length === 0 && staleCount === 0 && remoteMachines.length === 0)
1188
1406
  return "none";
1189
1407
  const labels = peers.map((p) => {
1190
1408
  const name = p.name || "unnamed";
@@ -1203,7 +1421,17 @@ function formatPeers(peers, cap, staleCount) {
1203
1421
  else {
1204
1422
  main = `${labels.slice(0, cap).join(", ")}, +${labels.length - cap} more`;
1205
1423
  }
1206
- return staleCount > 0 ? `${main}; ${staleCount} stale` : main;
1424
+ if (staleCount > 0)
1425
+ main = `${main}; ${staleCount} stale`;
1426
+ // Remote machines (presence transport): `Name @machine` labels, capped.
1427
+ if (remoteMachines.length > 0) {
1428
+ const remote = remoteMachines
1429
+ .flatMap((m) => m.agents.map((a) => `${a.name || "unnamed"} @${m.machine}`))
1430
+ .slice(0, cap);
1431
+ const extra = remoteMachines.reduce((n, m) => n + m.agents.length, 0) - remote.length;
1432
+ main = `${main}; ${remote.join(", ")}${extra > 0 ? `, +${extra} more` : ""}`;
1433
+ }
1434
+ return main;
1207
1435
  }
1208
1436
  function fmtTokens(n) {
1209
1437
  if (n < 1000)
@@ -1825,6 +2053,9 @@ function traceLine(ev, allTools) {
1825
2053
  case "state.status_checked":
1826
2054
  detail = "status box rendered";
1827
2055
  break;
2056
+ case "identity.assumed":
2057
+ detail = `${s("previous_name") || "unknown"} → ${s("name") || "unknown"}${s("agent_id") ? ` · persona=${s("agent_id").slice(0, 8)}…` : ""}`;
2058
+ break;
1828
2059
  case "claim.acquire":
1829
2060
  case "claim.release":
1830
2061
  case "claim.conflict":
@@ -2090,7 +2321,7 @@ function runHealth(opts) {
2090
2321
  activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
2091
2322
  const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
2092
2323
  const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
2093
- if (ageMs > FRESHNESS_SECS * 1000)
2324
+ if (ageMs > freshnessCutoffSecs() * 1000)
2094
2325
  staleHeartbeats++;
2095
2326
  // Zombie heuristics on a parseable heartbeat: no name, or an age so large
2096
2327
  // it can only be a broken/epoch timestamp (a real agent would have healed).
@@ -2126,7 +2357,7 @@ function runHealth(opts) {
2126
2357
  }
2127
2358
  }
2128
2359
  if (staleHeartbeats > 0) {
2129
- anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(FRESHNESS_SECS / 60)}min; heal mechanism may not be firing`);
2360
+ anomalies.push(`${staleHeartbeats} active heartbeat(s) older than ${Math.floor(freshnessCutoffSecs() / 60)}min; heal mechanism may not be firing`);
2130
2361
  }
2131
2362
  const unexpectedSchemas = Object.keys(activeBySchema).filter((k) => k !== "v1");
2132
2363
  if (unexpectedSchemas.length > 0) {
@@ -2806,8 +3037,8 @@ function runHeal(opts) {
2806
3037
  // agent-coord binary at harnery/bin/agent-coord.
2807
3038
  const helper = `${root}/harnery/bin/agent-coord`;
2808
3039
  const proc = spawnSync(helper, helperArgs, {
2809
- cwd: root,
2810
3040
  encoding: "utf8",
3041
+ ...coordHelperOpts(root),
2811
3042
  });
2812
3043
  if (proc.status !== 0) {
2813
3044
  emit.error({
@@ -11,10 +11,11 @@
11
11
  * restore restic restore <id>
12
12
  * prune restic forget --keep-daily 7 --keep-weekly 4 --prune
13
13
  *
14
- * Power users override via env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE)
15
- * or by passing through any restic args after `--`. The wrapper deliberately
16
- * doesn't try to abstract restic; it surfaces it, with opinionated defaults plus
17
- * passthrough.
14
+ * Defaults (repo path, password file, and the keep-daily/weekly/monthly prune
15
+ * policy) come from `.harnery/config.jsonc` `backup.*`, overridable per field by
16
+ * env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE) and CLI flags —
17
+ * see `backupConfig()`. The wrapper deliberately doesn't try to abstract restic;
18
+ * it surfaces it, with opinionated defaults plus passthrough after `--`.
18
19
  */
19
20
  import type { Command } from "commander";
20
21
  import type { EmitContext } from "../commander.js";
@@ -1 +1 @@
1
- {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAkM/E"}
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA4EnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuM/E"}
@@ -11,24 +11,22 @@
11
11
  * restore restic restore <id>
12
12
  * prune restic forget --keep-daily 7 --keep-weekly 4 --prune
13
13
  *
14
- * Power users override via env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE)
15
- * or by passing through any restic args after `--`. The wrapper deliberately
16
- * doesn't try to abstract restic; it surfaces it, with opinionated defaults plus
17
- * passthrough.
14
+ * Defaults (repo path, password file, and the keep-daily/weekly/monthly prune
15
+ * policy) come from `.harnery/config.jsonc` `backup.*`, overridable per field by
16
+ * env vars (HARNERY_RESTIC_REPO, HARNERY_RESTIC_PASSWORD_FILE) and CLI flags —
17
+ * see `backupConfig()`. The wrapper deliberately doesn't try to abstract restic;
18
+ * it surfaces it, with opinionated defaults plus passthrough after `--`.
18
19
  */
19
20
  import { spawnSync } from "node:child_process";
20
21
  import { randomBytes } from "node:crypto";
21
22
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
22
- import os from "node:os";
23
23
  import path from "node:path";
24
+ import { backupConfig } from "../core/config.js";
24
25
  function defaultRepo() {
25
- const home = os.homedir();
26
- return process.env.HARNERY_RESTIC_REPO ?? path.join(home, ".cache", "harnery", "restic-repo");
26
+ return backupConfig().repo;
27
27
  }
28
28
  function defaultPasswordFile() {
29
- const home = os.homedir();
30
- return (process.env.HARNERY_RESTIC_PASSWORD_FILE ??
31
- path.join(home, ".config", "harnery", "restic-password"));
29
+ return backupConfig().passwordFile;
32
30
  }
33
31
  function findHarneryDir() {
34
32
  let dir = process.cwd();
@@ -203,12 +201,15 @@ export function registerBackupCommand(program, emit) {
203
201
  emit.setExitCode(r.exitCode);
204
202
  });
205
203
  // -- prune --
204
+ // Prune-policy defaults come from backup.keep_* in config.jsonc (falling back
205
+ // to 7/4/6); an explicit --keep-* flag still overrides per invocation.
206
+ const prunePolicy = backupConfig();
206
207
  backup
207
208
  .command("prune")
208
- .description("Forget + prune snapshots (default policy: keep 7 daily / 4 weekly / 6 monthly).")
209
- .option("--keep-daily <n>", "Daily snapshots to keep", "7")
210
- .option("--keep-weekly <n>", "Weekly snapshots to keep", "4")
211
- .option("--keep-monthly <n>", "Monthly snapshots to keep", "6")
209
+ .description(`Forget + prune snapshots (default policy: keep ${prunePolicy.keepDaily} daily / ${prunePolicy.keepWeekly} weekly / ${prunePolicy.keepMonthly} monthly).`)
210
+ .option("--keep-daily <n>", "Daily snapshots to keep", String(prunePolicy.keepDaily))
211
+ .option("--keep-weekly <n>", "Weekly snapshots to keep", String(prunePolicy.keepWeekly))
212
+ .option("--keep-monthly <n>", "Monthly snapshots to keep", String(prunePolicy.keepMonthly))
212
213
  .option("--dry-run", "Show what would be pruned without actually pruning")
213
214
  .option("--repo <path>", "Repository path")
214
215
  .option("--password-file <path>", "Password file")
@@ -1 +1 @@
1
- {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiH1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA0KN"}
1
+ {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAmH1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAoLN"}