harnery 0.46.1 → 0.47.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 (176) hide show
  1. package/README.md +24 -0
  2. package/bin/README.md +10 -0
  3. package/bin/agent-coord.mjs +3 -0
  4. package/bin/agent-hook.mjs +3 -0
  5. package/bin/harn.mjs +3 -0
  6. package/bin/run.mjs +43 -0
  7. package/dist/commander.d.ts +18 -6
  8. package/dist/commander.d.ts.map +1 -1
  9. package/dist/commander.js +74 -2
  10. package/dist/commands/agents.d.ts +25 -0
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/agents.js +70 -27
  13. package/dist/commands/autoupdate.d.ts +12 -0
  14. package/dist/commands/autoupdate.d.ts.map +1 -0
  15. package/dist/commands/autoupdate.js +190 -0
  16. package/dist/commands/computer.d.ts +45 -0
  17. package/dist/commands/computer.d.ts.map +1 -0
  18. package/dist/commands/computer.js +509 -0
  19. package/dist/commands/docs.js +3 -1
  20. package/dist/commands/fetch.js +11 -1
  21. package/dist/commands/grep.d.ts +72 -0
  22. package/dist/commands/grep.d.ts.map +1 -1
  23. package/dist/commands/grep.js +200 -47
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +15 -9
  26. package/dist/commands/rm.js +2 -2
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +2 -0
  29. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  30. package/dist/core/agents/canonical-emit.js +2 -3
  31. package/dist/core/agents/coord-bin.d.ts +3 -0
  32. package/dist/core/agents/coord-bin.d.ts.map +1 -1
  33. package/dist/core/agents/coord-bin.js +6 -0
  34. package/dist/core/agents/coord-client.d.ts.map +1 -1
  35. package/dist/core/agents/coord-client.js +6 -2
  36. package/dist/core/agents/render/prompt-context.d.ts +55 -7
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +122 -48
  39. package/dist/core/agents/state/proc-start.d.ts +3 -1
  40. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  41. package/dist/core/agents/state/proc-start.js +26 -4
  42. package/dist/core/autoupdate/engine.d.ts +97 -0
  43. package/dist/core/autoupdate/engine.d.ts.map +1 -0
  44. package/dist/core/autoupdate/engine.js +548 -0
  45. package/dist/core/autoupdate/fingerprint.d.ts +16 -0
  46. package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
  47. package/dist/core/autoupdate/fingerprint.js +73 -0
  48. package/dist/core/autoupdate/git.d.ts +30 -0
  49. package/dist/core/autoupdate/git.d.ts.map +1 -0
  50. package/dist/core/autoupdate/git.js +82 -0
  51. package/dist/core/autoupdate/index.d.ts +6 -0
  52. package/dist/core/autoupdate/index.d.ts.map +1 -0
  53. package/dist/core/autoupdate/index.js +4 -0
  54. package/dist/core/autoupdate/state.d.ts +59 -0
  55. package/dist/core/autoupdate/state.d.ts.map +1 -0
  56. package/dist/core/autoupdate/state.js +214 -0
  57. package/dist/core/autoupdate/types.d.ts +113 -0
  58. package/dist/core/autoupdate/types.d.ts.map +1 -0
  59. package/dist/core/autoupdate/types.js +35 -0
  60. package/dist/core/config.d.ts +53 -1
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +114 -1
  63. package/dist/core/events/v3/bootstrap.js +1 -0
  64. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  65. package/dist/core/events/v3/runtime-build.js +1 -0
  66. package/dist/core/hooks/adapter/wiring.d.ts +12 -2
  67. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  68. package/dist/core/hooks/adapter/wiring.js +38 -8
  69. package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
  70. package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
  71. package/dist/core/hooks/autoupdate-cue.js +53 -0
  72. package/dist/core/hooks/cli.js +28 -3
  73. package/dist/core/hooks/effects/index.js +4 -4
  74. package/dist/core/hooks/resolve/coord-root.js +1 -0
  75. package/dist/core/semantic/contract.d.ts +3 -3
  76. package/dist/core/semantic/service.d.ts.map +1 -1
  77. package/dist/core/semantic/service.js +4 -2
  78. package/dist/core/storage/modes.d.ts +1 -1
  79. package/dist/core/storage/modes.d.ts.map +1 -1
  80. package/dist/core/storage/modes.js +5 -1
  81. package/dist/core/workflow/durable-record.d.ts +2 -0
  82. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  83. package/dist/core/workflow/durable-record.js +13 -1
  84. package/dist/core/workflow/engine.js +4 -0
  85. package/dist/core/workflow/spawn-claude.d.ts +7 -0
  86. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  87. package/dist/core/workflow/spawn-claude.js +16 -4
  88. package/dist/core/workflow/specialists.d.ts.map +1 -1
  89. package/dist/core/workflow/specialists.js +1 -0
  90. package/dist/core/workflow/types.d.ts +10 -0
  91. package/dist/core/workflow/types.d.ts.map +1 -1
  92. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  93. package/dist/core/workflow/workspaces/leases.js +1 -9
  94. package/dist/lib/computer/cua.d.ts +50 -0
  95. package/dist/lib/computer/cua.d.ts.map +1 -0
  96. package/dist/lib/computer/cua.js +474 -0
  97. package/dist/lib/computer/host.d.ts +19 -0
  98. package/dist/lib/computer/host.d.ts.map +1 -0
  99. package/dist/lib/computer/host.js +72 -0
  100. package/dist/lib/computer/lease.d.ts +13 -0
  101. package/dist/lib/computer/lease.d.ts.map +1 -0
  102. package/dist/lib/computer/lease.js +269 -0
  103. package/dist/lib/computer/types.d.ts +163 -0
  104. package/dist/lib/computer/types.d.ts.map +1 -0
  105. package/dist/lib/computer/types.js +10 -0
  106. package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
  107. package/dist/lib/docs-metadata-v2.js +6 -5
  108. package/dist/lib/exec.d.ts.map +1 -1
  109. package/dist/lib/exec.js +1 -0
  110. package/dist/lib/guarded-remove.d.ts.map +1 -1
  111. package/dist/lib/guarded-remove.js +5 -4
  112. package/dist/lib/headless/index.d.ts +33 -0
  113. package/dist/lib/headless/index.d.ts.map +1 -1
  114. package/dist/lib/headless/index.js +38 -2
  115. package/dist/lib/http/client.d.ts +2 -9
  116. package/dist/lib/http/client.d.ts.map +1 -1
  117. package/dist/lib/http/client.js +30 -9
  118. package/dist/lib/instructions/apply.d.ts +9 -0
  119. package/dist/lib/instructions/apply.d.ts.map +1 -1
  120. package/dist/lib/instructions/apply.js +92 -14
  121. package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
  122. package/dist/lib/instructions/git-hooks.js +8 -4
  123. package/dist/lib/instructions/templates.d.ts +11 -0
  124. package/dist/lib/instructions/templates.d.ts.map +1 -1
  125. package/dist/lib/instructions/templates.js +81 -1
  126. package/package.json +5 -5
  127. package/schemas/config.schema.json +89 -1
  128. package/src/commander.ts +98 -8
  129. package/src/commands/agents.ts +100 -32
  130. package/src/commands/autoupdate.ts +250 -0
  131. package/src/commands/computer.ts +682 -0
  132. package/src/commands/docs.ts +3 -1
  133. package/src/commands/fetch.ts +11 -1
  134. package/src/commands/grep.ts +274 -59
  135. package/src/commands/init.ts +17 -7
  136. package/src/commands/rm.ts +2 -2
  137. package/src/commands/web.ts +2 -0
  138. package/src/core/agents/canonical-emit.ts +2 -3
  139. package/src/core/agents/coord-bin.ts +30 -0
  140. package/src/core/agents/coord-client.ts +6 -2
  141. package/src/core/agents/render/prompt-context.ts +152 -53
  142. package/src/core/agents/state/proc-start.ts +29 -5
  143. package/src/core/autoupdate/engine.ts +691 -0
  144. package/src/core/autoupdate/fingerprint.ts +89 -0
  145. package/src/core/autoupdate/git.ts +120 -0
  146. package/src/core/autoupdate/index.ts +29 -0
  147. package/src/core/autoupdate/state.ts +269 -0
  148. package/src/core/autoupdate/types.ts +135 -0
  149. package/src/core/config.ts +142 -1
  150. package/src/core/events/v3/bootstrap.ts +1 -0
  151. package/src/core/events/v3/runtime-build.ts +1 -0
  152. package/src/core/hooks/adapter/wiring.ts +51 -6
  153. package/src/core/hooks/autoupdate-cue.ts +72 -0
  154. package/src/core/hooks/cli.ts +27 -1
  155. package/src/core/hooks/effects/index.ts +4 -4
  156. package/src/core/hooks/resolve/coord-root.ts +1 -0
  157. package/src/core/semantic/service.ts +6 -2
  158. package/src/core/storage/modes.ts +7 -1
  159. package/src/core/workflow/durable-record.ts +15 -1
  160. package/src/core/workflow/engine.ts +3 -0
  161. package/src/core/workflow/spawn-claude.ts +16 -4
  162. package/src/core/workflow/specialists.ts +1 -0
  163. package/src/core/workflow/types.ts +10 -0
  164. package/src/core/workflow/workspaces/leases.ts +1 -9
  165. package/src/lib/computer/cua.ts +572 -0
  166. package/src/lib/computer/host.ts +87 -0
  167. package/src/lib/computer/lease.ts +299 -0
  168. package/src/lib/computer/types.ts +177 -0
  169. package/src/lib/docs-metadata-v2.ts +24 -5
  170. package/src/lib/exec.ts +1 -0
  171. package/src/lib/guarded-remove.ts +5 -3
  172. package/src/lib/headless/index.ts +66 -2
  173. package/src/lib/http/client.ts +33 -19
  174. package/src/lib/instructions/apply.ts +100 -12
  175. package/src/lib/instructions/git-hooks.ts +8 -4
  176. package/src/lib/instructions/templates.ts +95 -1
@@ -308,6 +308,89 @@
308
308
  }
309
309
  }
310
310
  },
311
+ "computer": {
312
+ "type": "object",
313
+ "additionalProperties": false,
314
+ "description": "Opt-in desktop control through a pinned desktop driver (ADR 0228). Project config only; inert unless enabled is true.",
315
+ "properties": {
316
+ "enabled": {
317
+ "type": "boolean",
318
+ "default": false,
319
+ "description": "Allow `computer install`, `up`, `call`, and `lease acquire` in this project, and render the harn-computer skill on init."
320
+ },
321
+ "idle_seconds": {
322
+ "type": "number",
323
+ "exclusiveMinimum": 0,
324
+ "maximum": 86400,
325
+ "default": 120,
326
+ "description": "Desktop lease idle expiry. Every `computer call` by the holder refreshes it."
327
+ },
328
+ "wait_seconds": {
329
+ "type": "number",
330
+ "minimum": 0,
331
+ "maximum": 3600,
332
+ "default": 30,
333
+ "description": "How long `computer call` waits for another agent's lease before failing. `--wait` overrides per call."
334
+ }
335
+ }
336
+ },
337
+ "autoUpdate": {
338
+ "type": "object",
339
+ "additionalProperties": false,
340
+ "description": "Activity-fired project self-update (ADR 0229). Project config only; inert unless enabled is true. Prompts, session starts, and host CLI commands check a throttle stamp; when it is stale a detached child compares HEAD with the upstream branch (git ls-remote, no fetch), fingerprints the watched paths, and runs the steps only when something changed. Step protocol: each step runs from the project root with HARNERY_AUTOUPDATE_CHILD=1; a stdout or stderr line starting `harnery-notice:` becomes a notice shown once to every agent session in this checkout on its next prompt; exit code 75 means the step did what it could but needs a person, which is reported and not retried as a failure; any other non-zero exit stops the run and retries after a backoff.",
341
+ "properties": {
342
+ "enabled": {
343
+ "type": "boolean",
344
+ "default": false,
345
+ "description": "Turn self-update on for this project. Anything other than true leaves it off."
346
+ },
347
+ "steps": {
348
+ "type": "array",
349
+ "default": [],
350
+ "description": "Commands run in order when the upstream branch moved, a watched path changed, or the last run failed. Each entry is an argv array (no shell). The first failure stops the run. The host owns these commands: Harnery never fetches, resets, or discards changes itself.",
351
+ "items": {
352
+ "type": "array",
353
+ "minItems": 1,
354
+ "items": { "type": "string", "minLength": 1 }
355
+ }
356
+ },
357
+ "dailySteps": {
358
+ "type": "array",
359
+ "default": [],
360
+ "description": "Commands run at most once every 24 hours even when the checkout is current, for example a health check. Same argv form and protocol as steps.",
361
+ "items": {
362
+ "type": "array",
363
+ "minItems": 1,
364
+ "items": { "type": "string", "minLength": 1 }
365
+ }
366
+ },
367
+ "watch": {
368
+ "type": "array",
369
+ "default": [],
370
+ "description": "Paths outside Git whose change forces a run, such as managed tools or generated hook settings. `~/` and `$VAR` expand; relative paths resolve from the project root. A directory is fingerprinted shallowly (its own mtime plus each direct child's mtime and size).",
371
+ "items": { "type": "string", "minLength": 1 }
372
+ },
373
+ "checkMinutes": {
374
+ "type": "integer",
375
+ "minimum": 1,
376
+ "maximum": 1440,
377
+ "default": 15,
378
+ "description": "Minimum minutes between remote checks. Out-of-range values are clamped."
379
+ },
380
+ "timeoutMinutes": {
381
+ "type": "integer",
382
+ "minimum": 1,
383
+ "maximum": 240,
384
+ "default": 20,
385
+ "description": "Per-step timeout in minutes. A step that runs longer is stopped and counted as a failure. Out-of-range values are clamped."
386
+ },
387
+ "cleanup": {
388
+ "type": "boolean",
389
+ "default": true,
390
+ "description": "After a successful run, remove expired working artifacts (`artifacts clean --yes`) at most once a day."
391
+ }
392
+ }
393
+ },
311
394
  "files": {
312
395
  "type": "object",
313
396
  "additionalProperties": false,
@@ -395,7 +478,7 @@
395
478
  "type": "array",
396
479
  "items": { "type": "string" },
397
480
  "default": [],
398
- "description": "Shipped Harnery skills this repo suppresses. Installed skill IDs are harn-decide, harn-council, and harn-end."
481
+ "description": "Shipped Harnery skills this repo suppresses. Installed skill IDs are harn-decide, harn-council, harn-end, harn-team, and (only when computer.enabled is true) harn-computer."
399
482
  }
400
483
  }
401
484
  },
@@ -440,6 +523,11 @@
440
523
  "description": "Windows command name the bridge installs, for example codex-wsl-hook. Only a bridge that implements the decision record 0197 contract should install it."
441
524
  }
442
525
  }
526
+ },
527
+ "portableCommands": {
528
+ "type": "boolean",
529
+ "default": false,
530
+ "description": "Write the same adapter hook commands on every OS: agent-hook from PATH as each hook's command, plus a matching Codex commandWindows, and have init --check treat that form as current everywhere. For a project that commits its adapter settings and is cloned on Windows, Linux, WSL and macOS; every clone needs agent-hook on PATH. Project config only."
443
531
  }
444
532
  }
445
533
  },
package/src/commander.ts CHANGED
@@ -15,8 +15,8 @@
15
15
  * (Commander pattern).
16
16
  */
17
17
 
18
- import { readFileSync } from "node:fs";
19
- import { dirname, join } from "node:path";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { dirname, join, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { Command } from "commander";
22
22
  import {
@@ -25,6 +25,7 @@ import {
25
25
  loadAllLazyCommands,
26
26
  registerLazyCommandBundles,
27
27
  } from "./lazy-commands.ts";
28
+ import { coordEnv } from "./lib/env.ts";
28
29
 
29
30
  export {
30
31
  type LazyCommandBundle,
@@ -81,6 +82,9 @@ export interface EnvCheck {
81
82
  */
82
83
  export type EnvSection = () => Promise<EnvCheck[]>;
83
84
 
85
+ /** Verdict from `HarneryProgramContext.headerPolicy` for one caller header. */
86
+ export type FetchHeaderDecision = { allow: true } | { allow: false; message: string };
87
+
84
88
  export interface HarneryProgramContext {
85
89
  /** Project name (e.g., "my-monorepo"). Used in user-facing log lines + telemetry tags. */
86
90
  projectName?: string;
@@ -121,6 +125,17 @@ export interface HarneryProgramContext {
121
125
  * zones behind a WAF). harn standalone skips the callback entirely.
122
126
  */
123
127
  extraHeaders?: (url: string) => Record<string, string>;
128
+ /**
129
+ * Optional check run on every header the caller passed to `fetch` with
130
+ * `-H`, before any request leaves the process. Returning
131
+ * `{ allow: false, message }` stops the run with that message and a
132
+ * nonzero exit; there is no flag to override it. Only caller headers are
133
+ * checked: the default User-Agent and anything `extraHeaders` adds are the
134
+ * host's own. Redirect hops are not re-checked because a cross-origin hop
135
+ * already drops every caller header except a few browser headers. The
136
+ * message must not echo the header value. harn standalone checks nothing.
137
+ */
138
+ headerPolicy?: (host: string, name: string, value: string) => FetchHeaderDecision;
124
139
  /**
125
140
  * Optional callback that returns extra cookies to persist into the shared
126
141
  * jar before `fetch` / `browse` attach it. Runs only when a jar is in
@@ -130,12 +145,6 @@ export interface HarneryProgramContext {
130
145
  * harn standalone skips the callback entirely.
131
146
  */
132
147
  extraCookies?: import("./lib/cookies/index.ts").ExtraCookies;
133
- /**
134
- * When it returns true for a run, `fetch` follows redirects itself so the
135
- * cookie header, `extraHeaders`, and Set-Cookie persistence apply to every
136
- * hop instead of only the first request.
137
- */
138
- clientRedirects?: () => boolean;
139
148
  /**
140
149
  * Called once per `browse` run. Returns the guard that inspects that
141
150
  * session's live cookies before each request, after each response, and at
@@ -340,10 +349,75 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
340
349
  bundle.commands.every((command) => !skip.has(commandName(command.command))),
341
350
  );
342
351
  registerLazyCommandBundles(program, bundles);
352
+ program.hook("preAction", async (_root, actionCommand) => {
353
+ await triggerAutoUpdate(actionCommand, opts.context);
354
+ });
343
355
 
344
356
  return program;
345
357
  }
346
358
 
359
+ let autoUpdateTriggered = false;
360
+
361
+ /** Top-level command name below the program for a Commander action command. */
362
+ function topLevelCommandName(command: Command): string {
363
+ let current = command;
364
+ while (current.parent?.parent) current = current.parent;
365
+ return current.name();
366
+ }
367
+
368
+ /** Nearest enclosing directory carrying `.harnery/`, without spawning Git. */
369
+ function cheapCoordRoot(context: HarneryProgramContext | undefined): string | null {
370
+ const override = coordEnv("COORD_ROOT_OVERRIDE");
371
+ if (override) return override;
372
+ if (context?.resolveCoordRoot) return context.resolveCoordRoot();
373
+ const starts = [process.env.CLAUDE_PROJECT_DIR, process.cwd()].filter(
374
+ (dir): dir is string => typeof dir === "string" && dir.length > 0,
375
+ );
376
+ for (const start of starts) {
377
+ let dir = resolve(start);
378
+ while (true) {
379
+ if (existsSync(join(dir, ".harnery"))) return dir;
380
+ const parent = dirname(dir);
381
+ if (parent === dir) break;
382
+ dir = parent;
383
+ }
384
+ }
385
+ return null;
386
+ }
387
+
388
+ /**
389
+ * Host CLI activity fires the project self-update check (ADR 0229) once per
390
+ * process. A project without an `autoUpdate` section pays a directory walk
391
+ * and one small file read; the engine module loads only when the section is
392
+ * present. Never throws and never delays the command beyond the tick's stat.
393
+ */
394
+ async function triggerAutoUpdate(
395
+ actionCommand: Command,
396
+ context: HarneryProgramContext | undefined,
397
+ ): Promise<void> {
398
+ if (autoUpdateTriggered) return;
399
+ autoUpdateTriggered = true;
400
+ try {
401
+ if (coordEnv("AUTOUPDATE_CHILD") === "1") return;
402
+ if (topLevelCommandName(actionCommand) === "autoupdate") return;
403
+ const root = cheapCoordRoot(context);
404
+ if (!root) return;
405
+ let text: string;
406
+ try {
407
+ text = readFileSync(join(root, ".harnery", "config.jsonc"), "utf8");
408
+ } catch {
409
+ return;
410
+ }
411
+ if (!text.includes('"autoUpdate"')) return;
412
+ const { readAutoUpdateConfig } = await import("./core/config.ts");
413
+ if (readAutoUpdateConfig(root)?.enabled !== true) return;
414
+ const { autoUpdateTick } = await import("./core/autoupdate/index.ts");
415
+ autoUpdateTick(root);
416
+ } catch {
417
+ // Self-update is best effort; the user's command always runs.
418
+ }
419
+ }
420
+
347
421
  interface CommandBundleContext {
348
422
  emit: EmitContext;
349
423
  context: HarneryProgramContext | undefined;
@@ -545,6 +619,22 @@ function harneryCommandBundles({
545
619
  async (program) =>
546
620
  (await import("./commands/admission.ts")).registerAdmissionCommand(program, emit),
547
621
  ),
622
+ lazy(
623
+ "computer",
624
+ `Opt-in desktop control through a pinned desktop driver: \`computer status\`, \`install\`, \`up\`, \`call <tool> [json]\` under a desktop lease shared by every agent on the machine. Start with \`${binName} computer status\`.`,
625
+ async (program) =>
626
+ (await import("./commands/computer.ts")).registerComputerCommand(program, emit, binName),
627
+ ),
628
+ lazy(
629
+ "autoupdate",
630
+ `Activity-fired project self-update: \`autoupdate status\`, \`check\`, \`run\`. Configure steps under \`autoUpdate\` in .harnery/config.jsonc; start with \`${binName} autoupdate status\`.`,
631
+ async (program) =>
632
+ (await import("./commands/autoupdate.ts")).registerAutoUpdateCommand(
633
+ program,
634
+ emit,
635
+ binName,
636
+ ),
637
+ ),
548
638
  lazy(
549
639
  "browse-session",
550
640
  "Control an opted-in headed browse session through an owner-only descriptor",
@@ -28,7 +28,7 @@ import { basename, join, resolve } from "node:path";
28
28
  import type { Command } from "commander";
29
29
  import type { EmitContext, HarneryProgramContext } from "../commander.ts";
30
30
  import type { Adapter } from "../core/adapter.ts";
31
- import { coordBinPath } from "../core/agents/coord-bin.ts";
31
+ import { coordBinPath, spawnCoordBinSync } from "../core/agents/coord-bin.ts";
32
32
  import {
33
33
  checkGitFinalization,
34
34
  formatGitFinalizationFailure,
@@ -344,8 +344,14 @@ export function registerAgentsCommand(
344
344
 
345
345
  cmd
346
346
  .command("status")
347
- .description("End-of-turn status box (name + session age + files held + peer count)")
347
+ .description(
348
+ "End-of-turn status box (name, task, lifecycle, files held, live peers, time; diagnostics only when they report a problem)",
349
+ )
348
350
  .option("--json", "JSON output instead of the box")
351
+ .option(
352
+ "--full",
353
+ "Print every diagnostic row (session age, changes, activity, parent, context, resources)",
354
+ )
349
355
  .option(
350
356
  "--end-turn",
351
357
  "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",
@@ -361,7 +367,13 @@ export function registerAgentsCommand(
361
367
  "The Stop hook payload includes session_id; pass it through.",
362
368
  )
363
369
  .action(
364
- (opts: { endTurn?: boolean; endSession?: boolean; json?: boolean; sessionId?: string }) => {
370
+ (opts: {
371
+ endTurn?: boolean;
372
+ endSession?: boolean;
373
+ json?: boolean;
374
+ full?: boolean;
375
+ sessionId?: string;
376
+ }) => {
365
377
  runStatus(opts);
366
378
  },
367
379
  );
@@ -1915,7 +1927,7 @@ function ensureAdapterSession(root: string): void {
1915
1927
  : { source: "resume" }),
1916
1928
  });
1917
1929
 
1918
- spawnSync("bash", [agentHook, "session-start", "--adapter", bootstrap.adapter], {
1930
+ spawnCoordBinSync(agentHook, ["session-start", "--adapter", bootstrap.adapter], {
1919
1931
  input: payload,
1920
1932
  cwd: root,
1921
1933
  encoding: "utf8",
@@ -1977,7 +1989,7 @@ function runReleaseClaim(path: string): void {
1977
1989
  if (path.startsWith(`${root}/`)) canonical = path.slice(root.length + 1);
1978
1990
 
1979
1991
  const helper = agentCoordOrExit(root);
1980
- const result = spawnSync(helper, ["release-claim", myOwner, canonical], {
1992
+ const result = spawnCoordBinSync(helper, ["release-claim", myOwner, canonical], {
1981
1993
  encoding: "utf8",
1982
1994
  ...coordHelperOpts(root),
1983
1995
  });
@@ -2036,7 +2048,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
2036
2048
 
2037
2049
  // Heartbeat mutation goes through agent-coord (atomic temp+rename).
2038
2050
  const helper = agentCoordOrExit(root);
2039
- const result = spawnSync(helper, ["set-task", myOwner, task], {
2051
+ const result = spawnCoordBinSync(helper, ["set-task", myOwner, task], {
2040
2052
  encoding: "utf8",
2041
2053
  ...coordHelperOpts(root),
2042
2054
  });
@@ -2364,6 +2376,7 @@ function runStatus(opts: {
2364
2376
  endTurn?: boolean;
2365
2377
  endSession?: boolean;
2366
2378
  json?: boolean;
2379
+ full?: boolean;
2367
2380
  sessionId?: string;
2368
2381
  }): void {
2369
2382
  const root = monorepoRoot();
@@ -2616,29 +2629,30 @@ function runStatus(opts: {
2616
2629
  return;
2617
2630
  }
2618
2631
 
2619
- const rows: Array<[string, string]> = [
2620
- ["session", formatAge(ageSecs)],
2621
- ...(turnElapsed ? ([["turn", turnElapsed.value]] as Array<[string, string]>) : []),
2622
- ["changes", turnChanges.value],
2623
- ["activity", activityOf(hb)],
2624
- ["lifecycle", lifecycleLabel(hb)],
2625
- ["parent", describeParentReference(data.parent_reference)],
2626
- ["context", ctxStr],
2627
- ["resources", formatResourceSummary(resources)],
2628
- ["files", filesStr],
2629
- ["peers", peersStr],
2630
- ["time", timeStr],
2631
- ];
2632
- // Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH. It sits
2633
- // directly above `activity`, which keeps it under the session/turn clocks
2634
- // whether or not the turn row rendered.
2635
- if (hb.task && hb.task.length > 0) {
2636
- rows.splice(
2637
- rows.findIndex((row) => row[0] === "activity"),
2638
- 0,
2639
- ["task", hb.task],
2640
- );
2641
- }
2632
+ const contextPercent = !ctxUsage
2633
+ ? null
2634
+ : ctxUsage.percentOnly
2635
+ ? ctxUsage.usedPercent
2636
+ : (ctxUsage.used / ctxUsage.window) * 100;
2637
+ const rows = statusBoxRows({
2638
+ full: opts.full === true,
2639
+ session: formatAge(ageSecs),
2640
+ turn: turnElapsed?.value ?? null,
2641
+ changes: turnChanges.value,
2642
+ task: hb.task ?? "",
2643
+ activity: activityOf(hb),
2644
+ lifecycle: lifecycleLabel(hb),
2645
+ parent: describeParentReference(data.parent_reference),
2646
+ parentProblem: data.parent_reference.state === "conflict",
2647
+ context: ctxStr,
2648
+ contextProblem: contextPercent !== null && contextPercent >= STATUS_CONTEXT_WARN_PERCENT,
2649
+ resources: formatResourceSummary(resources),
2650
+ resourcesProblem:
2651
+ resources.assessment.state === "elevated" || resources.assessment.state === "critical",
2652
+ files: filesStr,
2653
+ peers: peersStr,
2654
+ time: timeStr,
2655
+ });
2642
2656
  if (pendingCouncils.length > 0) {
2643
2657
  // Slot the council line right before `time` so it stays in the "what's
2644
2658
  // active for me" cluster of rows. Show the first ID + count; full list
@@ -2672,6 +2686,60 @@ function runStatus(opts: {
2672
2686
  process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
2673
2687
  }
2674
2688
 
2689
+ /** Context use at which the compact status box adds its context row. */
2690
+ const STATUS_CONTEXT_WARN_PERCENT = 80;
2691
+
2692
+ /**
2693
+ * Rows of the status box. The compact default carries what an operator acts
2694
+ * on (task, lifecycle, files held, live peers, time, with the turn duration
2695
+ * beside the time); diagnostic rows appear only when they report a problem.
2696
+ * `--full` restores every row. The Stop hook detects the box by its
2697
+ * `┌─ agent-` title, never by a row, so trimming rows cannot break it.
2698
+ */
2699
+ export function statusBoxRows(input: {
2700
+ full: boolean;
2701
+ session: string;
2702
+ turn: string | null;
2703
+ changes: string;
2704
+ task: string;
2705
+ activity: string;
2706
+ lifecycle: string;
2707
+ parent: string;
2708
+ parentProblem: boolean;
2709
+ context: string;
2710
+ contextProblem: boolean;
2711
+ resources: string;
2712
+ resourcesProblem: boolean;
2713
+ files: string;
2714
+ peers: string;
2715
+ time: string;
2716
+ }): Array<[string, string]> {
2717
+ const task: Array<[string, string]> = input.task ? [["task", input.task]] : [];
2718
+ if (input.full) {
2719
+ return [
2720
+ ["session", input.session],
2721
+ ...(input.turn ? ([["turn", input.turn]] as Array<[string, string]>) : []),
2722
+ ["changes", input.changes],
2723
+ ...task,
2724
+ ["activity", input.activity],
2725
+ ["lifecycle", input.lifecycle],
2726
+ ["parent", input.parent],
2727
+ ["context", input.context],
2728
+ ["resources", input.resources],
2729
+ ["files", input.files],
2730
+ ["peers", input.peers],
2731
+ ["time", input.time],
2732
+ ];
2733
+ }
2734
+ const rows: Array<[string, string]> = [...task, ["lifecycle", input.lifecycle]];
2735
+ if (input.parentProblem) rows.push(["parent", input.parent]);
2736
+ if (input.contextProblem) rows.push(["context", input.context]);
2737
+ if (input.resourcesProblem) rows.push(["resources", input.resources]);
2738
+ rows.push(["files", input.files], ["peers", input.peers]);
2739
+ rows.push(["time", input.turn ? `${input.time} (turn ${input.turn})` : input.time]);
2740
+ return rows;
2741
+ }
2742
+
2675
2743
  function qualityForStatus(
2676
2744
  mode: "off" | "shadow" | "report",
2677
2745
  snapshot: RunQualitySnapshot | null,
@@ -4792,7 +4860,7 @@ function replayAdapterSamples(
4792
4860
  }
4793
4861
 
4794
4862
  const subcommand = EVENT_SUBCOMMAND[event] ?? event;
4795
- const dispatch = spawnSync("bash", [agentHook, subcommand, "--adapter", adapterFlag], {
4863
+ const dispatch = spawnCoordBinSync(agentHook, [subcommand, "--adapter", adapterFlag], {
4796
4864
  cwd: sandbox,
4797
4865
  encoding: "utf8",
4798
4866
  input: JSON.stringify(payloadObj),
@@ -5371,7 +5439,7 @@ function runHeal(opts: {
5371
5439
 
5372
5440
  // Both recovery actions are handled by the bundled agent-coord binary.
5373
5441
  const helper = agentCoordOrExit(root);
5374
- let proc = spawnSync(helper, helperArgs, {
5442
+ let proc = spawnCoordBinSync(helper, helperArgs, {
5375
5443
  encoding: "utf8",
5376
5444
  ...coordHelperOpts(root),
5377
5445
  });
@@ -5412,7 +5480,7 @@ function runHeal(opts: {
5412
5480
  "the current native session could not establish authority; no fallback cache was accepted",
5413
5481
  );
5414
5482
  }
5415
- proc = spawnSync(helper, helperArgs, {
5483
+ proc = spawnCoordBinSync(helper, helperArgs, {
5416
5484
  encoding: "utf8",
5417
5485
  ...coordHelperOpts(root),
5418
5486
  });