harnery 0.31.3 → 0.31.5

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.
package/README.md CHANGED
@@ -7,28 +7,28 @@
7
7
 
8
8
  # Harnery
9
9
 
10
- > Multi-agent coordination for AI coding agents — Claude Code, Cursor, and Codex.
10
+ > Multi-agent coordination for AI coding agents: Claude Code, Cursor, and Codex.
11
11
 
12
12
  [![CI](https://github.com/ryanjkelly/harnery/actions/workflows/ci.yml/badge.svg)](https://github.com/ryanjkelly/harnery/actions/workflows/ci.yml)
13
13
  [![npm version](https://img.shields.io/npm/v/harnery.svg)](https://www.npmjs.com/package/harnery)
14
14
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
15
15
 
16
- > ⚠️ **Pre-1.0.** API surface is still settling. Pin a specific minor version (`harnery@^0.7.0`) and read the [CHANGELOG](CHANGELOG.md) before each upgrade.
16
+ > ⚠️ **Pre-1.0.** API surface is still settling. Pin a specific minor version (`harnery@^0.31.0`) and read the [CHANGELOG](CHANGELOG.md) before each upgrade.
17
17
 
18
18
  ## What it is
19
19
 
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:
20
+ `harnery` keeps multiple AI coding agents from stepping on each other in a shared checkout. It came out of years of running several Claude Code / Cursor / Codex sessions at once against the same monorepo.
21
21
 
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 journals, and adapter 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.
24
- - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless adapter-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
25
- - **Optional isolated workspaces:** add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. Inspect with `harn workflow workspace`, then use the separate `workflow integration prepare`, `integration apply`, and `cleanup` phases; shared execution remains the default.
26
- - **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Reusable workflow code receives the exact work ID, title, objective, acceptance criteria, and attempt identity as frozen typed context. A retry also receives the prior run's bounded error and unresolved proof criteria, while retry authority and attempt ceilings remain unchanged. The manifest, proof, and append-only ledger bind the same context, and the one-shot reconciler reports the next explicit action without silently spawning, retrying, or accepting work.
27
- - **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
28
- - **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)).
29
- - **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).
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 journals, and adapter 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.
24
+ - **Coordination-aware workflows.** `harn workflow run <script>` runs bounded, schema-gated multi-subagent workflow scripts. Plain JS stages fan out to headless adapter-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), and deterministic code decides the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`. CLI `ASK` requests park durably for an explicit approve/deny and same-run resume; library callers stay fail-closed by default.
25
+ - **Optional isolated workspaces.** Add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. Inspect with `harn workflow workspace`, then use the separate `workflow integration prepare`, `integration apply`, and `cleanup` phases. Shared execution remains the default.
26
+ - **Durable work above attempts.** `harn work` keeps an objective alive across bounded workflow attempts, dependencies, approval parks, failures, and review. Reusable workflow code receives the exact work ID, title, objective, acceptance criteria, and attempt identity as frozen typed context. A retry also receives the prior run's bounded error and unresolved proof criteria; retry authority and attempt ceilings stay unchanged. The manifest, proof, and append-only ledger bind the same context, and the one-shot reconciler reports the next explicit action without silently spawning, retrying, or accepting work.
27
+ - **Bounded goal supervision.** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority. Old generations stay immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
28
+ - **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)).
29
+ - **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).
30
30
 
31
- The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`, `grep` — parallel, ripgrep-accelerated monorepo search — `docs`, `browse`, `fetch`, `read`, `devtools`, and more) plus the library toolkit they're built from. Useful, cross-platform, dependency-light — and deliberately not the headline. Coordination is why harnery exists; the toolkit exists because the CLI needed it (see [Public surface tiers](#public-surface-tiers)).
31
+ The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`, `grep` for parallel, ripgrep-accelerated monorepo search, plus `docs`, `browse`, `fetch`, `read`, `devtools`, and more) and the library toolkit they're built from. Useful, cross-platform, and dependency-light, but deliberately not the headline. Coordination is why harnery exists; the toolkit exists because the CLI needed it (see [Public surface tiers](#public-surface-tiers)).
32
32
 
33
33
  ## Install
34
34
 
@@ -36,7 +36,7 @@ The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`,
36
36
  curl -fsSL https://harnery.com/install.sh | bash
37
37
  ```
38
38
 
39
- One line, no clone: it installs the `harn` CLI globally (npm preferred, Bun fallback), puts it on your `PATH`, and verifies it. Or drive your package manager directly — `npm install -g harnery` / `bun add -g harnery` (or `npm install harnery` for a project dep). Then wire a project:
39
+ One line, no clone: it installs the `harn` CLI globally (npm preferred, Bun fallback), puts it on your `PATH`, and verifies it. Or drive your package manager directly: `npm install -g harnery` / `bun add -g harnery` (or `npm install harnery` for a project dep). Then wire a project:
40
40
 
41
41
  ```bash
42
42
  harn init # creates .harnery/ + registers the adapter hooks
@@ -62,7 +62,7 @@ harn deinit --purge-state # also delete .harnery/ (destructive)
62
62
  curl -fsSL https://harnery.com/uninstall.sh | bash
63
63
  ```
64
64
 
65
- From a git clone, `./scripts/teardown.sh` is the mirror of `./scripts/setup.sh`: it runs `harn deinit`, removes the `PATH` symlinks, and — on a terminal — asks whether to also delete this project's `.harnery/` history and the clone itself. Both default to no; pre-answer with `--purge-state` and `--remove-clone` for unattended runs.
65
+ From a git clone, `./scripts/teardown.sh` is the mirror of `./scripts/setup.sh`: it runs `harn deinit`, removes the `PATH` symlinks, and (on a terminal) asks whether to also delete this project's `.harnery/` history and the clone itself. Both default to no; pre-answer with `--purge-state` and `--remove-clone` for unattended runs.
66
66
 
67
67
  ## Use as a CLI library
68
68
 
@@ -90,8 +90,8 @@ await program.parseAsync(process.argv);
90
90
 
91
91
  The exports map draws the line between what harnery *is* and what it *ships with*:
92
92
 
93
- - **Product tier** — `harnery`, `harnery/commander`, `harnery/core/*`: the coordination layer and CLI composition. This is the API to build against, and the reason to install harnery.
94
- - **Toolkit tier** — every `harnery/lib/*` subpath (`http`, `cookies`, `format`, `readability`, `browser`, `machine`, …): the supporting utilities harnery's own CLI is built from, exposed for embedding hosts that want to lean on them. Supported, but secondary: it can evolve faster than the product tier, and it isn't a reason to adopt harnery on its own.
93
+ - **Product tier** (`harnery`, `harnery/commander`, `harnery/core/*`): the coordination layer and CLI composition. This is the API to build against, and the reason to install harnery.
94
+ - **Toolkit tier** (every `harnery/lib/*` subpath: `http`, `cookies`, `format`, `readability`, `browser`, `machine`, …): the supporting utilities harnery's own CLI is built from, exposed for embedding hosts that want to lean on them. Supported, but secondary: it can evolve faster than the product tier, and it isn't a reason to adopt harnery on its own.
95
95
 
96
96
  The boundary is enforced, not aspirational: CI verifies that no `harnery/lib/*` export imports the coordination core, directly or transitively (`scripts/check-layering.ts`), so pulling a toolkit module never drags in coordination state. Details: [Embedding + surface tiers](https://harnery.com/concepts/embedding/).
97
97
 
@@ -15,6 +15,12 @@
15
15
  * Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
16
16
  * the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
17
17
  * as a user-visible mobile cue.
18
+ *
19
+ * Remediation continuity: an adapter whose Stop channel re-prompts by
20
+ * submitting a new user message (Cursor's `followup_message`) opens a fresh
21
+ * turn to repair the previous one. Such a turn inherits the window of the turn
22
+ * it repairs, so ritual events accumulate instead of alternating. See
23
+ * `resolveTurnStartMs`.
18
24
  */
19
25
  export type { VerdictResult } from "./verdict.js";
20
26
  import type { VerdictResult } from "./verdict.js";
@@ -43,6 +49,16 @@ export interface StopHookRequest {
43
49
  * event capture on: the child stays visible to peers and the coord layer. */
44
50
  workflow_child?: boolean;
45
51
  }
52
+ /**
53
+ * Machine marker that opens a Stop-remediation message. Written by
54
+ * `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
55
+ * message, and read back here off `user_prompt.submit.data.prompt_text` to
56
+ * recognize a turn Harnery itself opened.
57
+ *
58
+ * Leading position is deliberate: `prompt_text` is clamped when recorded, so a
59
+ * trailing marker could be truncated away on a long reason.
60
+ */
61
+ export declare const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
46
62
  /**
47
63
  * Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
48
64
  * when all three pass). Soft-fails open when the event stream isn't
@@ -1 +1 @@
1
- {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CAmHvF"}
1
+ {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAwEnE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CAqHvF"}
@@ -15,6 +15,12 @@
15
15
  * Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
16
16
  * the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
17
17
  * as a user-visible mobile cue.
18
+ *
19
+ * Remediation continuity: an adapter whose Stop channel re-prompts by
20
+ * submitting a new user message (Cursor's `followup_message`) opens a fresh
21
+ * turn to repair the previous one. Such a turn inherits the window of the turn
22
+ * it repairs, so ritual events accumulate instead of alternating. See
23
+ * `resolveTurnStartMs`.
18
24
  */
19
25
  import { existsSync, readFileSync } from "node:fs";
20
26
  import { join } from "node:path";
@@ -23,6 +29,80 @@ function ackSignalFor(adapter) {
23
29
  return adapter === "cursor" ? "status_checked" : "status_box_present";
24
30
  }
25
31
  const RECENT_EVENT_WINDOW_LINES = 5_000;
32
+ /**
33
+ * Machine marker that opens a Stop-remediation message. Written by
34
+ * `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
35
+ * message, and read back here off `user_prompt.submit.data.prompt_text` to
36
+ * recognize a turn Harnery itself opened.
37
+ *
38
+ * Leading position is deliberate: `prompt_text` is clamped when recorded, so a
39
+ * trailing marker could be truncated away on a long reason.
40
+ */
41
+ export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
42
+ /**
43
+ * The pre-marker message shape, still recognized on purpose.
44
+ *
45
+ * A followup already sitting in the stream was written by whichever build was
46
+ * running when it fired. An agent whose session spans an upgrade would otherwise
47
+ * keep alternating until the first new-format followup appeared, which is the
48
+ * exact failure this change removes. Recognizing the older trailing form closes
49
+ * that rollout window at no cost: it only ever appears in a `prompt_text`
50
+ * because Harnery put it there.
51
+ */
52
+ const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
53
+ /** How far back the remediation walk may travel. Adapters cap their own
54
+ * followup chains (Cursor's `loop_limit`), and each prompt can appear once per
55
+ * observing adapter, so this only guards a pathological stream. */
56
+ const MAX_REMEDIATION_WALK_BACK = 16;
57
+ function isStopRemediationPrompt(event) {
58
+ const text = event.data.prompt_text;
59
+ if (typeof text !== "string")
60
+ return false;
61
+ return text.includes(STOP_REMEDIATION_MARKER) || text.includes(STOP_REMEDIATION_LEGACY_MARKER);
62
+ }
63
+ /**
64
+ * Start of the window this Stop verdict judges.
65
+ *
66
+ * Normally the most recent `user_prompt.submit`. The exception is the
67
+ * remediation chain: Cursor does not continue a turn on Stop, it auto-submits
68
+ * our message as a new user turn (`followup_message`). Scoping the verdict to
69
+ * that new turn alone makes the ritual unsatisfiable in one pass, because
70
+ * remediation itself runs a tool, so the repair turn needs every rule again
71
+ * while the signal from the previous turn sits just outside the window. Running
72
+ * `set-task` then clears rule 3/3 and fails rule 1/3, running `status` clears
73
+ * 1/3 and fails 3/3, and the pair alternates until the adapter's loop cap.
74
+ *
75
+ * Anchoring the window at the last prompt Harnery did not author makes progress
76
+ * monotonic: each repair adds a signal and none are lost, so the chain ends.
77
+ * A genuine human prompt always anchors a fresh window, so this cannot be used
78
+ * to inherit ritual credit across real turns.
79
+ */
80
+ function resolveTurnStartMs(ownerEvents, nowMs, fallbackMs) {
81
+ // Honor the cutoff: `now_ms` marks what is relevant yet, so a prompt after it
82
+ // cannot anchor this turn. In production the cutoff is the clock and nothing
83
+ // follows it, but a replay over recorded history passes a past cutoff, and an
84
+ // unclamped search would anchor on a later turn and hand back an empty window.
85
+ const prompts = ownerEvents.filter((e) => {
86
+ if (e.event_type !== "user_prompt.submit")
87
+ return false;
88
+ const t = Date.parse(e.ts);
89
+ return Number.isFinite(t) && t <= nowMs;
90
+ });
91
+ if (prompts.length === 0)
92
+ return fallbackMs;
93
+ let index = prompts.length - 1;
94
+ let hops = 0;
95
+ while (index > 0 && hops < MAX_REMEDIATION_WALK_BACK) {
96
+ const prompt = prompts[index];
97
+ if (!prompt || !isStopRemediationPrompt(prompt))
98
+ break;
99
+ index -= 1;
100
+ hops += 1;
101
+ }
102
+ const anchor = prompts[index];
103
+ const parsed = anchor ? Date.parse(anchor.ts) : Number.NaN;
104
+ return Number.isFinite(parsed) ? parsed : fallbackMs;
105
+ }
26
106
  /**
27
107
  * Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
28
108
  * when all three pass). Soft-fails open when the event stream isn't
@@ -81,17 +161,19 @@ export function evaluateStopHook(coordRoot, req) {
81
161
  };
82
162
  }
83
163
  // Turn window: from the most recent user_prompt.submit (this owner) up to
84
- // either the explicit now_ms or the last event we see. Fall back to a
164
+ // either the explicit now_ms or the last event we see, walking back over any
165
+ // Harnery-authored Stop-remediation prompts so a repair turn is judged
166
+ // together with the turn it repairs (see `resolveTurnStartMs`). Fall back to a
85
167
  // 5-minute window when no user_prompt.submit is in scope (fresh wiring,
86
168
  // out-of-window prompt, etc.) so a stale stream doesn't silently pass.
87
169
  const nowMs = req.now_ms ?? Date.now();
88
- const lastUserPrompt = [...ownerEvents]
89
- .reverse()
90
- .find((e) => e.event_type === "user_prompt.submit");
91
- const startMs = lastUserPrompt ? Date.parse(lastUserPrompt.ts) : nowMs - 5 * 60 * 1000;
170
+ const startMs = req.turn_window
171
+ ? req.turn_window.start_ms
172
+ : resolveTurnStartMs(ownerEvents, nowMs, nowMs - 5 * 60 * 1000);
173
+ const endMs = req.turn_window ? req.turn_window.end_ms : nowMs;
92
174
  const inTurn = ownerEvents.filter((e) => {
93
175
  const t = Date.parse(e.ts);
94
- return Number.isFinite(t) && t >= startMs && t <= nowMs;
176
+ return Number.isFinite(t) && t >= startMs && t <= endMs;
95
177
  });
96
178
  const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
97
179
  const statusChecked = inTurn.some((e) => e.event_type === "state.status_checked");
@@ -70,6 +70,33 @@ export declare function groupUnclaim(coordRoot: string, groupId: string, path: s
70
70
  export declare function killHeartbeat(coordRoot: string, instanceId: string): boolean;
71
71
  export declare function healPidmap(coordRoot: string, instanceId: string, pid: number): void;
72
72
  export declare function healHeartbeat(coordRoot: string, instanceId: string, sessionId?: string, model?: string, adapter?: string): Heartbeat | null;
73
+ /**
74
+ * Register a workflow child in the coordination layer from the ENGINE side.
75
+ *
76
+ * Previously a spawned child was visible only if its adapter fired Harnery's
77
+ * hooks, which made coordination visibility a property of the vendor CLI
78
+ * rather than of the engine. Headless `codex exec` fires no hooks, so codex
79
+ * children never appeared in `harn agents list` and rendered as "no live
80
+ * session" on the run page while actively working — for the entire length of
81
+ * a multi-minute stage.
82
+ *
83
+ * The engine already knows every fact a heartbeat needs at spawn time, so it
84
+ * writes one itself. Hook-firing adapters keep enriching the same file
85
+ * (last_tool, files_touched); non-hook adapters at least become visible and
86
+ * attributable. Pair with `killHeartbeat` when the child ends.
87
+ *
88
+ * Idempotent: re-registering refreshes in place and preserves `started_at`
89
+ * and any claims a hook already recorded.
90
+ */
91
+ export declare function registerWorkflowChild(coordRoot: string, opts: {
92
+ instanceId: string;
93
+ runId: string;
94
+ agentId: string;
95
+ sessionId?: string;
96
+ adapter?: string;
97
+ label?: string;
98
+ model?: string;
99
+ }): Heartbeat;
73
100
  /**
74
101
  * Stamp the heartbeat with the most-recent tool name + target. Written from
75
102
  * the post-tool-use hook.
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA6DH,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAiBD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQrF;AAeD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAS7F;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKxF;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,SAAS,GAAG,IAAI,CAMlB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAYlB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE,CAsChG;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAS5E;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAuBnF;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,GAAG,IAAI,CAsDlB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,SAAS,GAAG,IAAI,CAOlB"}
1
+ {"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"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"}
@@ -280,6 +280,49 @@ export function healHeartbeat(coordRoot, instanceId, sessionId, model, adapter)
280
280
  });
281
281
  return hb;
282
282
  }
283
+ /**
284
+ * Register a workflow child in the coordination layer from the ENGINE side.
285
+ *
286
+ * Previously a spawned child was visible only if its adapter fired Harnery's
287
+ * hooks, which made coordination visibility a property of the vendor CLI
288
+ * rather than of the engine. Headless `codex exec` fires no hooks, so codex
289
+ * children never appeared in `harn agents list` and rendered as "no live
290
+ * session" on the run page while actively working — for the entire length of
291
+ * a multi-minute stage.
292
+ *
293
+ * The engine already knows every fact a heartbeat needs at spawn time, so it
294
+ * writes one itself. Hook-firing adapters keep enriching the same file
295
+ * (last_tool, files_touched); non-hook adapters at least become visible and
296
+ * attributable. Pair with `killHeartbeat` when the child ends.
297
+ *
298
+ * Idempotent: re-registering refreshes in place and preserves `started_at`
299
+ * and any claims a hook already recorded.
300
+ */
301
+ export function registerWorkflowChild(coordRoot, opts) {
302
+ const now = nowIsoSeconds();
303
+ const prior = readHeartbeat(coordRoot, opts.instanceId);
304
+ const hb = {
305
+ ...(prior ?? {}),
306
+ schema_version: 1,
307
+ instance_id: opts.instanceId,
308
+ // The reader keys children by session_id and skips any heartbeat missing
309
+ // one, so it must always be set even before the adapter mints a real id.
310
+ session_id: prior?.session_id ?? opts.sessionId ?? opts.instanceId,
311
+ name: opts.label || opts.agentId,
312
+ kind: "workflow-child",
313
+ agent_id: opts.agentId,
314
+ model: opts.model ?? prior?.model ?? "",
315
+ platform: adapterToPlatform(opts.adapter),
316
+ started_at: prior?.started_at ?? now,
317
+ last_heartbeat: now,
318
+ files_touched: prior?.files_touched ?? [],
319
+ task: opts.label ?? "",
320
+ workflow_run_id: opts.runId,
321
+ workflow_agent_id: opts.agentId,
322
+ };
323
+ atomicWrite(heartbeatPath(coordRoot, opts.instanceId), JSON.stringify(hb, null, 2));
324
+ return hb;
325
+ }
283
326
  /**
284
327
  * Stamp the heartbeat with the most-recent tool name + target. Written from
285
328
  * the post-tool-use hook.
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAEhF,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAI/D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,GAAG,CAAC,CAYjG"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAEhF,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAI/D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,GAAG,CAAC,CAuBjG"}
@@ -17,6 +17,8 @@
17
17
  * Every helper writes to process.stdout + newline-terminates so callers can
18
18
  * fire-and-forget. Empty text → no-op (no JSON written).
19
19
  */
20
+ import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.js";
21
+ import { resolveBinName } from "../../config.js";
20
22
  /** Emit a context-injection (peer table, wiring check, council pending, …). */
21
23
  export function emitContext(adapter, event, text) {
22
24
  if (!text || text.length === 0)
@@ -56,12 +58,25 @@ export function emitDeny(adapter, reason) {
56
58
  export function emitStopBlock(adapter, verdict) {
57
59
  if (adapter === "codex")
58
60
  return 0;
59
- const message = `${verdict.reason ?? "End-of-turn coordination ritual incomplete."}\n[agent-hook stop]: rule=${verdict.rule}`;
61
+ const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
60
62
  if (adapter === "cursor") {
63
+ // The marker leads the message so the Stop verdict can recognize the turn
64
+ // Cursor opens from it (`STOP_REMEDIATION_MARKER`), and so the operator can
65
+ // see in chat that the message came from Harnery rather than from them.
66
+ // Both commands are named because one message that repairs the whole ritual
67
+ // ends the chain in a single followup; the failing rule is context.
68
+ const bin = resolveBinName();
69
+ const message = [
70
+ `${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
71
+ reason,
72
+ `Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${bin} agents status\` as your last tool call.`,
73
+ ].join("\n");
61
74
  process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
62
75
  return 0;
63
76
  }
64
- process.stderr.write(`${message}\n`);
77
+ // Claude Code continues the SAME turn on exit 2, so the single failing rule is
78
+ // both accurate and sufficient there. Left unchanged deliberately.
79
+ process.stderr.write(`${reason}\n[agent-hook stop]: rule=${verdict.rule}\n`);
65
80
  return 2;
66
81
  }
67
82
  function buildContextJson(adapter, event, text) {
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA0CH,OAAO,KAAK,EAIV,UAAU,EAEV,SAAS,EAYV,MAAM,YAAY,CAAC;AAsCpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;CAOvF;AAED;;;;6EAI6E;AAC7E,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAEjB,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAUhD;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA6E1F"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA2CH,OAAO,KAAK,EAIV,UAAU,EAEV,SAAS,EAYV,MAAM,YAAY,CAAC;AAiDpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;CAOvF;AAED;;;;6EAI6E;AAC7E,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAEjB,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAUhD;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA6E1F"}
@@ -28,6 +28,7 @@ import { createHash, randomBytes } from "node:crypto";
28
28
  import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
29
29
  import { isAbsolute, join, resolve } from "node:path";
30
30
  import { pathToFileURL } from "node:url";
31
+ import { killHeartbeat, registerWorkflowChild } from "../agents/state/heartbeat-writer.js";
31
32
  import { snapshotRepo } from "../context/index.js";
32
33
  import { evaluatePolicy, normalizePolicy, PolicyDeniedError, policyDigest, summarizePolicyRequest, } from "../policy/index.js";
33
34
  import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.js";
@@ -60,6 +61,16 @@ const DEFAULT_MAX_AGENTS = 50;
60
61
  const DEFAULT_CONCURRENCY = 4;
61
62
  const DEFAULT_MAX_ATTEMPTS = 2;
62
63
  const DEFAULT_TIMEOUT_MS = 300_000;
64
+ /**
65
+ * Deterministic coordination identity for a spawned child.
66
+ *
67
+ * Stable per (run, agent) so a re-register refreshes one heartbeat instead of
68
+ * accumulating duplicates, and so an orphan left by a killed engine is
69
+ * greppable straight back to the run that owned it.
70
+ */
71
+ function childInstanceId(runId, agentId) {
72
+ return `${runId}-${agentId}`;
73
+ }
63
74
  const DEFAULT_MAX_TURNS = 25;
64
75
  const DEFAULT_POLICY_ASK_TIMEOUT_MS = 60_000;
65
76
  const MAX_POLICY_DECISIONS = 50;
@@ -790,6 +801,24 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState, workCon
790
801
  effort: agentOpts.effort ?? null,
791
802
  });
792
803
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${adapter}] ${label}`);
804
+ // Register the child ourselves rather than trusting its adapter to fire
805
+ // Harnery's hooks. Headless `codex exec` fires none, so codex children
806
+ // were invisible to `harn agents list` and the run page for the whole
807
+ // duration of a stage. The engine knows everything the heartbeat needs.
808
+ // Best-effort: coordination visibility must never fail a run.
809
+ try {
810
+ registerWorkflowChild(opts.coordRoot, {
811
+ instanceId: childInstanceId(runId, id),
812
+ runId,
813
+ agentId: id,
814
+ adapter,
815
+ label,
816
+ model: agentOpts.model,
817
+ });
818
+ }
819
+ catch {
820
+ /* visibility is not worth failing a spawn over */
821
+ }
793
822
  let attemptPrompt = dispatchPrompt;
794
823
  let last = null;
795
824
  let agentCostUsd = 0;
@@ -893,6 +922,15 @@ async function executeWorkflow(scriptPath, absScript, opts, resumeState, workCon
893
922
  }
894
923
  finally {
895
924
  reservedCostUsd = Math.max(0, reservedCostUsd - reservedForDispatch);
925
+ // Deregister on every exit path, including throw. A child heartbeat that
926
+ // outlives its process reads as a live agent forever and blocks peers on
927
+ // its stale claims. The transcript remains the durable record.
928
+ try {
929
+ killHeartbeat(opts.coordRoot, childInstanceId(runId, id));
930
+ }
931
+ catch {
932
+ /* already gone, or swept */
933
+ }
896
934
  release();
897
935
  }
898
936
  };
@@ -1 +1 @@
1
- {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/proof.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EAEzB,YAAY,EAEZ,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EAEZ,aAAa,EAIb,iCAAiC,EACjC,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAEhB,qCAAqC,EACtC,MAAM,uBAAuB,CAAC;AAoB/B,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,mBAAmB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IAClD,IAAI,EAAE,sBAAsB,CAAC;IAC7B,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,QAAQ,EAAE,sBAAsB,EAAE,CAAC;IACnC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,GAAG,SAAS,CAAC,CAAC,CAAC;IAClF,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,iCAAiC,CAAC;IACtD;+DAC2D;IAC3D,mBAAmB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,SAAS,CAAC,CAAC,CAAC;IACvF,MAAM,CAAC,EAAE;QACP,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;QACnC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;QACrC,SAAS,EAAE,eAAe,CAAC;QAC3B,aAAa,EAAE,mBAAmB,CAAC;KACpC,CAAC;IACF;mFAC+E;IAC/E,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,iBAAiB,CAAC,EAAE,qCAAqC,CAAC;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,GAAG,SAAS,EAC9B,YAAY,EAAE,MAAM,GACnB,sBAAsB,CAqCxB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,KAAK,EAAE,qBAAqB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,sBAAsB,CAyBzB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,QAAQ,EAAE,sBAAsB,EAAE,GACjC;IAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,CAuB9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,YAAY,CAQjF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,QAAQ,EAC9B,MAAM,EAAE,SAAS,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE,GAC9D,iBAAiB,GAAG,SAAS,CAS/B;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,aAAa,CA6DhF;AAgCD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAO3E;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAoDjF;AA2BD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAoChE"}
1
+ {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/proof.ts"],"names":[],"mappings":"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"}
@@ -68,7 +68,12 @@ export function createEvidenceRecord(input) {
68
68
  recorded_at: input.recordedAt ?? new Date().toISOString(),
69
69
  kind: enumValue(value.kind, EVIDENCE_KINDS, "evidence kind"),
70
70
  status: enumValue(value.status, ["passed", "failed", "observed", "unknown"], "evidence status"),
71
- label: boundedRequired(value.label, "evidence label", MAX_LABEL_CHARS),
71
+ // Truncate rather than throw. A label is a display string; the substance
72
+ // lives in `summary` and `ref`. Throwing here discards the whole run at
73
+ // the point evidence is recorded, which is the END of the work — a real
74
+ // three-agent review was lost because its label ran 31 characters long.
75
+ // Same posture the transcript writer already takes: shrink and say so.
76
+ label: truncatedRequired(value.label, "evidence label", MAX_LABEL_CHARS),
72
77
  summary: boundedOptional(value.summary, "evidence summary", MAX_SUMMARY_CHARS),
73
78
  ref: boundedOptional(value.ref, "evidence ref", MAX_REF_CHARS),
74
79
  stage: boundedOptional(input.stage, "evidence stage", MAX_LABEL_CHARS),
@@ -442,6 +447,24 @@ function boundedRequired(value, field, max) {
442
447
  throw new Error(`${field} exceeds ${max} characters`);
443
448
  return normalized;
444
449
  }
450
+ /**
451
+ * Like `boundedRequired`, but shortens an over-long value instead of throwing.
452
+ *
453
+ * For fields where the string is a display label rather than load-bearing
454
+ * content: losing the tail of a label is trivial, losing the run that produced
455
+ * it is not. The marker keeps the truncation visible so a reader never mistakes
456
+ * a shortened label for the whole thing. Still throws when the value is missing
457
+ * or blank — that is a caller bug, not an overflow.
458
+ */
459
+ function truncatedRequired(value, field, max) {
460
+ if (typeof value !== "string" || value.trim() === "")
461
+ throw new Error(`${field} is required`);
462
+ const normalized = value.trim();
463
+ if (normalized.length <= max)
464
+ return normalized;
465
+ const marker = "…[truncated]";
466
+ return `${normalized.slice(0, Math.max(0, max - marker.length))}${marker}`;
467
+ }
445
468
  function boundedOptional(value, field, max) {
446
469
  if (value === undefined)
447
470
  return undefined;
@@ -34,6 +34,12 @@ export interface BrowserOptions {
34
34
  };
35
35
  /** Default navigation timeout in ms. Default 30000. */
36
36
  navigationTimeout?: number;
37
+ /**
38
+ * Max ms to wait for Chromium to start (`launchPersistentContext` timeout).
39
+ * When unset, Playwright's default (30s) applies. Tests that need a faster
40
+ * fail→retry loop should set this explicitly (e.g. 10_000).
41
+ */
42
+ launchTimeout?: number;
37
43
  /**
38
44
  * `wait_until` strategy for `navigate`. Default `"load"`.
39
45
  * Use `"domcontentloaded"` for sites with long-running analytics scripts
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
@@ -68,6 +68,7 @@ export class Browser {
68
68
  this.context = await chromium.launchPersistentContext(this.profileDir, {
69
69
  headless: !this.opts.headed,
70
70
  viewport: this.opts.viewport ?? { width: 1280, height: 800 },
71
+ ...(this.opts.launchTimeout !== undefined ? { timeout: this.opts.launchTimeout } : {}),
71
72
  ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
72
73
  ? { args: this.opts.launchArgs }
73
74
  : {}),
@@ -14,7 +14,7 @@
14
14
  * 2. A **fully-owned file** harnery creates whole (a shipped skill's
15
15
  * `SKILL.md`), carrying an ownership header comment so `deinit` deletes
16
16
  * only files harnery generated and `--check` flags a hand-edit:
17
- * <!-- harnery:generated <name> v=<hash> — machine-owned … -->
17
+ * <!-- harnery:generated <name> v=<hash>; machine-owned … -->
18
18
  *
19
19
  * Modeled on the first host's HTML-theme splicer (regenerate + byte-compare,
20
20
  * sha256-8 hash, content outside the region untouchable). Pure (no fs) so it's
@@ -14,7 +14,7 @@
14
14
  * 2. A **fully-owned file** harnery creates whole (a shipped skill's
15
15
  * `SKILL.md`), carrying an ownership header comment so `deinit` deletes
16
16
  * only files harnery generated and `--check` flags a hand-edit:
17
- * <!-- harnery:generated <name> v=<hash> — machine-owned … -->
17
+ * <!-- harnery:generated <name> v=<hash>; machine-owned … -->
18
18
  *
19
19
  * Modeled on the first host's HTML-theme splicer (regenerate + byte-compare,
20
20
  * sha256-8 hash, content outside the region untouchable). Pure (no fs) so it's
@@ -103,7 +103,7 @@ export function buildOwnedSkill(opts) {
103
103
  "---",
104
104
  ].join("\n");
105
105
  const body = opts.body.trim();
106
- const marker = `<!-- harnery:generated ${opts.name} v=${shortHash(body)} — machine-owned; ` +
106
+ const marker = `<!-- harnery:generated ${opts.name} v=${shortHash(body)}; machine-owned; ` +
107
107
  `regenerated by \`${opts.binName} init\`, removed by \`${opts.binName} deinit\`. ` +
108
108
  `Edit the harnery template, not this file. -->`;
109
109
  return `${fm}\n${marker}\n\n${body}\n`;
@@ -115,7 +115,7 @@ surfaces prior decisions, so check for precedent before re-deciding. ${decidePoi
115
115
  function decideBody(b) {
116
116
  return `The decision docket is a persistent queue for decisions you would otherwise
117
117
  route to a human. It's built on the \`${b} decision\` engine. This skill is the
118
- mechanics — file, find precedent, claim, and resolve with evidence. *When* a
118
+ mechanics: file, find precedent, claim, and resolve with evidence. *When* a
119
119
  decision needs a human at all (versus one you settle yourself) is host policy;
120
120
  if this project defines that rubric, follow it.
121
121
 
@@ -128,7 +128,7 @@ if this project defines that rubric, follow it.
128
128
  ## Capture (default)
129
129
 
130
130
  1. **Check precedent first.** \`${b} decision search "<key terms>"\`. If a resolved
131
- decision already answers this, cite it — don't re-litigate.
131
+ decision already answers this, cite it; don't re-litigate.
132
132
  2. **File it** when the choice has a second consumer (a human will want to see it,
133
133
  or a future agent will face it again) or reversal is expensive. Skip the
134
134
  docket for pure local mechanics (a variable name, one of two equivalent idioms).
@@ -141,7 +141,7 @@ if this project defines that rubric, follow it.
141
141
 
142
142
  For a decision with real substance, write a brief to a file and pass
143
143
  \`--brief <path>\` so the reviewer sees options + evidence, not a cold prompt.
144
- 3. **Proceed on your default.** Filing does not mean blocking — note the id in your
144
+ 3. **Proceed on your default.** Filing does not mean blocking. Note the id in your
145
145
  reply and keep working.
146
146
 
147
147
  ## Resolve (\`resolve <id>\`)
@@ -151,7 +151,7 @@ ${b} decision show <id> # read the question + context + any brief
151
151
  ${b} decision claim <id> # mark it deliberating (claimed by you)
152
152
  \`\`\`
153
153
 
154
- Research it for real — run the queries, read the files, compute the costs. Then
154
+ Research it for real: run the queries, read the files, compute the costs. Then
155
155
  resolve with **cited evidence** (the engine rejects an evidence-free resolution):
156
156
 
157
157
  \`\`\`bash
@@ -210,7 +210,7 @@ ${b} council list --mine --json
210
210
 
211
211
  One section per council you're a member of: id + objective, round N (open /
212
212
  collected) with N/M contributors, and your status (awaiting prompt / prompt ready
213
- / already contributed) with the next command to run. Stop after listing — don't
213
+ / already contributed) with the next command to run. Stop after listing; don't
214
214
  auto-route into contribute.
215
215
 
216
216
  ## Create (\`create <objective>\`)
@@ -224,18 +224,18 @@ http://localhost:9000/councils/new?objective=<encoded>
224
224
 
225
225
  If the dev server isn't up, start it with \`${b} web up\`.
226
226
 
227
- ## Contribute (\`contribute <id>\`) — the guarded flow
227
+ ## Contribute (\`contribute <id>\`): the guarded flow
228
228
 
229
229
  Run these checks in order; refuse with a specific reason if any fails.
230
230
 
231
- 1. **Membership.** If your \`whoami\` name isn't in \`manifest.members\`, refuse — the
232
- router likely meant a different agent's session.
233
- 2. **Already contributed.** If you're in \`current_round_contributors\`, refuse —
234
- wait for the steward to advance the round.
231
+ 1. **Membership.** If your \`whoami\` name isn't in \`manifest.members\`, refuse.
232
+ The router likely meant a different agent's session.
233
+ 2. **Already contributed.** If you're in \`current_round_contributors\`, refuse.
234
+ Wait for the steward to advance the round.
235
235
  3. **Prompt routing.** Find your entry in \`current_round_prompts\`. If none is
236
236
  drafted for you, refuse (the steward must write one first). If the routed body
237
237
  carries a \`<!-- council-route … member: <name> -->\` header naming a *different*
238
- agent, refuse — the wrong prompt was pasted into your session.
238
+ agent, refuse: the wrong prompt was pasted into your session.
239
239
  4. **Compose** per your prompt (read \`manifest.target_doc\` in full if set; strip
240
240
  the route header before treating the body as instructions).
241
241
  5. **Submit:**
@@ -250,20 +250,20 @@ Run these checks in order; refuse with a specific reason if any fails.
250
250
  \`<trivial>\` angle-bracket tag on the final line (the exit-criterion parser keys
251
251
  on it). When in doubt, lean \`<trivial>\`.
252
252
 
253
- ## Prompts (\`prompts <id>\`) — steward
253
+ ## Prompts (\`prompts <id>\`): steward
254
254
 
255
255
  1. **Authority.** If your \`whoami\` name ≠ the council's \`steward\`, stop.
256
256
  2. **Plan the round** from the target doc + prior rounds. Round 1 must include a
257
- completeness critic — assign one member the explicit charge: "What important
258
- thing is NOT in this document at all — a missing dimension, not a flaw in what's
259
- written?" Lens-scoped reviewers reliably miss whole absent dimensions.
257
+ completeness critic. Assign one member the explicit charge: "What important
258
+ thing is NOT in this document at all (a missing dimension, not a flaw in what's
259
+ written)?" Lens-scoped reviewers reliably miss whole absent dimensions.
260
260
  3. **Draft + write** one prompt per member missing from \`current_round_prompts\`:
261
261
 
262
262
  \`\`\`bash
263
263
  ${b} council prompt <id> agent-<Name> --message "..." # or --file <path>
264
264
  \`\`\`
265
265
 
266
- The CLI auto-prepends the \`<!-- council-route … -->\` header — never write it by
266
+ The CLI auto-prepends the \`<!-- council-route … -->\` header; never write it by
267
267
  hand. Every prompt must instruct the member to end with the literal
268
268
  \`<substantive>\` / \`<trivial>\` tag.
269
269
 
@@ -280,7 +280,7 @@ export const SKILLS = [
280
280
  relPath: "harn-decide/SKILL.md",
281
281
  render: (binName) => buildOwnedSkill({
282
282
  name: "harn-decide",
283
- description: "File a decision into the docket instead of blocking on a human — search precedent, file it, and proceed on a reversible default; or pick up and resolve an open decision with cited evidence. Use whenever you're about to ask a human a decision-shaped question you could resolve yourself.",
283
+ description: "File a decision into the docket instead of blocking on a human: search precedent, file it, and proceed on a reversible default; or pick up and resolve an open decision with cited evidence. Use whenever you're about to ask a human a decision-shaped question you could resolve yourself.",
284
284
  argumentHint: "[<the decision / question you're facing> | resolve <id> | review]",
285
285
  binName,
286
286
  body: decideBody(binName),
@@ -291,7 +291,7 @@ export const SKILLS = [
291
291
  relPath: "harn-council/SKILL.md",
292
292
  render: (binName) => buildOwnedSkill({
293
293
  name: "harn-council",
294
- description: "Interact with the multi-agent council system: list / create / show / prompts (steward) / contribute (member). Guards against misrouting — refuses to contribute when you aren't a member, have already contributed, or weren't routed a prompt.",
294
+ description: "Interact with the multi-agent council system: list / create / show / prompts (steward) / contribute (member). Guards against misrouting: refuses to contribute when you aren't a member, have already contributed, or weren't routed a prompt.",
295
295
  argumentHint: "[<id-or-fragment> | create <objective> | contribute <id> | prompts <id> | show <id>]",
296
296
  binName,
297
297
  body: councilBody(binName),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.31.3",
3
+ "version": "0.31.5",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -15,6 +15,12 @@
15
15
  * Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
16
16
  * the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
17
17
  * as a user-visible mobile cue.
18
+ *
19
+ * Remediation continuity: an adapter whose Stop channel re-prompts by
20
+ * submitting a new user message (Cursor's `followup_message`) opens a fresh
21
+ * turn to repair the previous one. Such a turn inherits the window of the turn
22
+ * it repairs, so ritual events accumulate instead of alternating. See
23
+ * `resolveTurnStartMs`.
18
24
  */
19
25
 
20
26
  import { existsSync, readFileSync } from "node:fs";
@@ -89,6 +95,87 @@ function ackSignalFor(adapter?: string): AckSignal {
89
95
 
90
96
  const RECENT_EVENT_WINDOW_LINES = 5_000;
91
97
 
98
+ /**
99
+ * Machine marker that opens a Stop-remediation message. Written by
100
+ * `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
101
+ * message, and read back here off `user_prompt.submit.data.prompt_text` to
102
+ * recognize a turn Harnery itself opened.
103
+ *
104
+ * Leading position is deliberate: `prompt_text` is clamped when recorded, so a
105
+ * trailing marker could be truncated away on a long reason.
106
+ */
107
+ export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
108
+
109
+ /**
110
+ * The pre-marker message shape, still recognized on purpose.
111
+ *
112
+ * A followup already sitting in the stream was written by whichever build was
113
+ * running when it fired. An agent whose session spans an upgrade would otherwise
114
+ * keep alternating until the first new-format followup appeared, which is the
115
+ * exact failure this change removes. Recognizing the older trailing form closes
116
+ * that rollout window at no cost: it only ever appears in a `prompt_text`
117
+ * because Harnery put it there.
118
+ */
119
+ const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
120
+
121
+ /** How far back the remediation walk may travel. Adapters cap their own
122
+ * followup chains (Cursor's `loop_limit`), and each prompt can appear once per
123
+ * observing adapter, so this only guards a pathological stream. */
124
+ const MAX_REMEDIATION_WALK_BACK = 16;
125
+
126
+ function isStopRemediationPrompt(event: CanonicalEvent): boolean {
127
+ const text = event.data.prompt_text;
128
+ if (typeof text !== "string") return false;
129
+ return text.includes(STOP_REMEDIATION_MARKER) || text.includes(STOP_REMEDIATION_LEGACY_MARKER);
130
+ }
131
+
132
+ /**
133
+ * Start of the window this Stop verdict judges.
134
+ *
135
+ * Normally the most recent `user_prompt.submit`. The exception is the
136
+ * remediation chain: Cursor does not continue a turn on Stop, it auto-submits
137
+ * our message as a new user turn (`followup_message`). Scoping the verdict to
138
+ * that new turn alone makes the ritual unsatisfiable in one pass, because
139
+ * remediation itself runs a tool, so the repair turn needs every rule again
140
+ * while the signal from the previous turn sits just outside the window. Running
141
+ * `set-task` then clears rule 3/3 and fails rule 1/3, running `status` clears
142
+ * 1/3 and fails 3/3, and the pair alternates until the adapter's loop cap.
143
+ *
144
+ * Anchoring the window at the last prompt Harnery did not author makes progress
145
+ * monotonic: each repair adds a signal and none are lost, so the chain ends.
146
+ * A genuine human prompt always anchors a fresh window, so this cannot be used
147
+ * to inherit ritual credit across real turns.
148
+ */
149
+ function resolveTurnStartMs(
150
+ ownerEvents: CanonicalEvent[],
151
+ nowMs: number,
152
+ fallbackMs: number,
153
+ ): number {
154
+ // Honor the cutoff: `now_ms` marks what is relevant yet, so a prompt after it
155
+ // cannot anchor this turn. In production the cutoff is the clock and nothing
156
+ // follows it, but a replay over recorded history passes a past cutoff, and an
157
+ // unclamped search would anchor on a later turn and hand back an empty window.
158
+ const prompts = ownerEvents.filter((e) => {
159
+ if (e.event_type !== "user_prompt.submit") return false;
160
+ const t = Date.parse(e.ts);
161
+ return Number.isFinite(t) && t <= nowMs;
162
+ });
163
+ if (prompts.length === 0) return fallbackMs;
164
+
165
+ let index = prompts.length - 1;
166
+ let hops = 0;
167
+ while (index > 0 && hops < MAX_REMEDIATION_WALK_BACK) {
168
+ const prompt = prompts[index];
169
+ if (!prompt || !isStopRemediationPrompt(prompt)) break;
170
+ index -= 1;
171
+ hops += 1;
172
+ }
173
+
174
+ const anchor = prompts[index];
175
+ const parsed = anchor ? Date.parse(anchor.ts) : Number.NaN;
176
+ return Number.isFinite(parsed) ? parsed : fallbackMs;
177
+ }
178
+
92
179
  /**
93
180
  * Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
94
181
  * when all three pass). Soft-fails open when the event stream isn't
@@ -151,18 +238,20 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
151
238
  }
152
239
 
153
240
  // Turn window: from the most recent user_prompt.submit (this owner) up to
154
- // either the explicit now_ms or the last event we see. Fall back to a
241
+ // either the explicit now_ms or the last event we see, walking back over any
242
+ // Harnery-authored Stop-remediation prompts so a repair turn is judged
243
+ // together with the turn it repairs (see `resolveTurnStartMs`). Fall back to a
155
244
  // 5-minute window when no user_prompt.submit is in scope (fresh wiring,
156
245
  // out-of-window prompt, etc.) so a stale stream doesn't silently pass.
157
246
  const nowMs = req.now_ms ?? Date.now();
158
- const lastUserPrompt = [...ownerEvents]
159
- .reverse()
160
- .find((e) => e.event_type === "user_prompt.submit");
161
- const startMs = lastUserPrompt ? Date.parse(lastUserPrompt.ts) : nowMs - 5 * 60 * 1000;
247
+ const startMs = req.turn_window
248
+ ? req.turn_window.start_ms
249
+ : resolveTurnStartMs(ownerEvents, nowMs, nowMs - 5 * 60 * 1000);
250
+ const endMs = req.turn_window ? req.turn_window.end_ms : nowMs;
162
251
 
163
252
  const inTurn = ownerEvents.filter((e) => {
164
253
  const t = Date.parse(e.ts);
165
- return Number.isFinite(t) && t >= startMs && t <= nowMs;
254
+ return Number.isFinite(t) && t >= startMs && t <= endMs;
166
255
  });
167
256
 
168
257
  const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
@@ -347,6 +347,61 @@ export function healHeartbeat(
347
347
  return hb;
348
348
  }
349
349
 
350
+ /**
351
+ * Register a workflow child in the coordination layer from the ENGINE side.
352
+ *
353
+ * Previously a spawned child was visible only if its adapter fired Harnery's
354
+ * hooks, which made coordination visibility a property of the vendor CLI
355
+ * rather than of the engine. Headless `codex exec` fires no hooks, so codex
356
+ * children never appeared in `harn agents list` and rendered as "no live
357
+ * session" on the run page while actively working — for the entire length of
358
+ * a multi-minute stage.
359
+ *
360
+ * The engine already knows every fact a heartbeat needs at spawn time, so it
361
+ * writes one itself. Hook-firing adapters keep enriching the same file
362
+ * (last_tool, files_touched); non-hook adapters at least become visible and
363
+ * attributable. Pair with `killHeartbeat` when the child ends.
364
+ *
365
+ * Idempotent: re-registering refreshes in place and preserves `started_at`
366
+ * and any claims a hook already recorded.
367
+ */
368
+ export function registerWorkflowChild(
369
+ coordRoot: string,
370
+ opts: {
371
+ instanceId: string;
372
+ runId: string;
373
+ agentId: string;
374
+ sessionId?: string;
375
+ adapter?: string;
376
+ label?: string;
377
+ model?: string;
378
+ },
379
+ ): Heartbeat {
380
+ const now = nowIsoSeconds();
381
+ const prior = readHeartbeat(coordRoot, opts.instanceId);
382
+ const hb: Heartbeat = {
383
+ ...(prior ?? {}),
384
+ schema_version: 1,
385
+ instance_id: opts.instanceId,
386
+ // The reader keys children by session_id and skips any heartbeat missing
387
+ // one, so it must always be set even before the adapter mints a real id.
388
+ session_id: prior?.session_id ?? opts.sessionId ?? opts.instanceId,
389
+ name: opts.label || opts.agentId,
390
+ kind: "workflow-child",
391
+ agent_id: opts.agentId,
392
+ model: opts.model ?? prior?.model ?? "",
393
+ platform: adapterToPlatform(opts.adapter),
394
+ started_at: prior?.started_at ?? now,
395
+ last_heartbeat: now,
396
+ files_touched: prior?.files_touched ?? [],
397
+ task: opts.label ?? "",
398
+ workflow_run_id: opts.runId,
399
+ workflow_agent_id: opts.agentId,
400
+ };
401
+ atomicWrite(heartbeatPath(coordRoot, opts.instanceId), JSON.stringify(hb, null, 2));
402
+ return hb;
403
+ }
404
+
350
405
  /**
351
406
  * Stamp the heartbeat with the most-recent tool name + target. Written from
352
407
  * the post-tool-use hook.
@@ -18,6 +18,8 @@
18
18
  * fire-and-forget. Empty text → no-op (no JSON written).
19
19
  */
20
20
 
21
+ import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.ts";
22
+ import { resolveBinName } from "../../config.ts";
21
23
  import type { Adapter } from "../events/schema.ts";
22
24
 
23
25
  export type SystemEvent = "SessionStart" | "UserPromptSubmit" | "SubagentStart";
@@ -61,14 +63,25 @@ export function emitDeny(adapter: Adapter, reason: string): void {
61
63
  export function emitStopBlock(adapter: Adapter, verdict: { reason?: string; rule: string }): 0 | 2 {
62
64
  if (adapter === "codex") return 0;
63
65
 
64
- const message = `${
65
- verdict.reason ?? "End-of-turn coordination ritual incomplete."
66
- }\n[agent-hook stop]: rule=${verdict.rule}`;
66
+ const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
67
67
  if (adapter === "cursor") {
68
+ // The marker leads the message so the Stop verdict can recognize the turn
69
+ // Cursor opens from it (`STOP_REMEDIATION_MARKER`), and so the operator can
70
+ // see in chat that the message came from Harnery rather than from them.
71
+ // Both commands are named because one message that repairs the whole ritual
72
+ // ends the chain in a single followup; the failing rule is context.
73
+ const bin = resolveBinName();
74
+ const message = [
75
+ `${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
76
+ reason,
77
+ `Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${bin} agents status\` as your last tool call.`,
78
+ ].join("\n");
68
79
  process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
69
80
  return 0;
70
81
  }
71
- process.stderr.write(`${message}\n`);
82
+ // Claude Code continues the SAME turn on exit 2, so the single failing rule is
83
+ // both accurate and sufficient there. Left unchanged deliberately.
84
+ process.stderr.write(`${reason}\n[agent-hook stop]: rule=${verdict.rule}\n`);
72
85
  return 2;
73
86
  }
74
87
 
@@ -21,6 +21,7 @@ import { createHash, randomBytes } from "node:crypto";
21
21
  import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
22
22
  import { isAbsolute, join, resolve } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
+ import { killHeartbeat, registerWorkflowChild } from "../agents/state/heartbeat-writer.ts";
24
25
  import { snapshotRepo } from "../context/index.ts";
25
26
  import type {
26
27
  ExternalMutationRequest,
@@ -109,6 +110,17 @@ const DEFAULT_MAX_AGENTS = 50;
109
110
  const DEFAULT_CONCURRENCY = 4;
110
111
  const DEFAULT_MAX_ATTEMPTS = 2;
111
112
  const DEFAULT_TIMEOUT_MS = 300_000;
113
+
114
+ /**
115
+ * Deterministic coordination identity for a spawned child.
116
+ *
117
+ * Stable per (run, agent) so a re-register refreshes one heartbeat instead of
118
+ * accumulating duplicates, and so an orphan left by a killed engine is
119
+ * greppable straight back to the run that owned it.
120
+ */
121
+ function childInstanceId(runId: string, agentId: string): string {
122
+ return `${runId}-${agentId}`;
123
+ }
112
124
  const DEFAULT_MAX_TURNS = 25;
113
125
  const DEFAULT_POLICY_ASK_TIMEOUT_MS = 60_000;
114
126
  const MAX_POLICY_DECISIONS = 50;
@@ -935,6 +947,24 @@ async function executeWorkflow(
935
947
  });
936
948
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${adapter}] ${label}`);
937
949
 
950
+ // Register the child ourselves rather than trusting its adapter to fire
951
+ // Harnery's hooks. Headless `codex exec` fires none, so codex children
952
+ // were invisible to `harn agents list` and the run page for the whole
953
+ // duration of a stage. The engine knows everything the heartbeat needs.
954
+ // Best-effort: coordination visibility must never fail a run.
955
+ try {
956
+ registerWorkflowChild(opts.coordRoot, {
957
+ instanceId: childInstanceId(runId, id),
958
+ runId,
959
+ agentId: id,
960
+ adapter,
961
+ label,
962
+ model: agentOpts.model,
963
+ });
964
+ } catch {
965
+ /* visibility is not worth failing a spawn over */
966
+ }
967
+
938
968
  let attemptPrompt = dispatchPrompt;
939
969
  let last: SpawnResult | null = null;
940
970
  let agentCostUsd = 0;
@@ -1047,6 +1077,14 @@ async function executeWorkflow(
1047
1077
  throw error;
1048
1078
  } finally {
1049
1079
  reservedCostUsd = Math.max(0, reservedCostUsd - reservedForDispatch);
1080
+ // Deregister on every exit path, including throw. A child heartbeat that
1081
+ // outlives its process reads as a live agent forever and blocks peers on
1082
+ // its stale claims. The transcript remains the durable record.
1083
+ try {
1084
+ killHeartbeat(opts.coordRoot, childInstanceId(runId, id));
1085
+ } catch {
1086
+ /* already gone, or swept */
1087
+ }
1050
1088
  release();
1051
1089
  }
1052
1090
  };
@@ -173,7 +173,12 @@ export function createEvidenceRecord(input: {
173
173
  recorded_at: input.recordedAt ?? new Date().toISOString(),
174
174
  kind: enumValue(value.kind, EVIDENCE_KINDS, "evidence kind"),
175
175
  status: enumValue(value.status, ["passed", "failed", "observed", "unknown"], "evidence status"),
176
- label: boundedRequired(value.label, "evidence label", MAX_LABEL_CHARS),
176
+ // Truncate rather than throw. A label is a display string; the substance
177
+ // lives in `summary` and `ref`. Throwing here discards the whole run at
178
+ // the point evidence is recorded, which is the END of the work — a real
179
+ // three-agent review was lost because its label ran 31 characters long.
180
+ // Same posture the transcript writer already takes: shrink and say so.
181
+ label: truncatedRequired(value.label, "evidence label", MAX_LABEL_CHARS),
177
182
  summary: boundedOptional(value.summary, "evidence summary", MAX_SUMMARY_CHARS),
178
183
  ref: boundedOptional(value.ref, "evidence ref", MAX_REF_CHARS),
179
184
  stage: boundedOptional(input.stage, "evidence stage", MAX_LABEL_CHARS),
@@ -584,6 +589,23 @@ function boundedRequired(value: unknown, field: string, max: number): string {
584
589
  return normalized;
585
590
  }
586
591
 
592
+ /**
593
+ * Like `boundedRequired`, but shortens an over-long value instead of throwing.
594
+ *
595
+ * For fields where the string is a display label rather than load-bearing
596
+ * content: losing the tail of a label is trivial, losing the run that produced
597
+ * it is not. The marker keeps the truncation visible so a reader never mistakes
598
+ * a shortened label for the whole thing. Still throws when the value is missing
599
+ * or blank — that is a caller bug, not an overflow.
600
+ */
601
+ function truncatedRequired(value: unknown, field: string, max: number): string {
602
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`${field} is required`);
603
+ const normalized = value.trim();
604
+ if (normalized.length <= max) return normalized;
605
+ const marker = "…[truncated]";
606
+ return `${normalized.slice(0, Math.max(0, max - marker.length))}${marker}`;
607
+ }
608
+
587
609
  function boundedOptional(value: unknown, field: string, max: number): string | undefined {
588
610
  if (value === undefined) return undefined;
589
611
  if (typeof value !== "string") throw new Error(`${field} must be a string`);
@@ -81,6 +81,12 @@ export interface BrowserOptions {
81
81
  viewport?: { width: number; height: number };
82
82
  /** Default navigation timeout in ms. Default 30000. */
83
83
  navigationTimeout?: number;
84
+ /**
85
+ * Max ms to wait for Chromium to start (`launchPersistentContext` timeout).
86
+ * When unset, Playwright's default (30s) applies. Tests that need a faster
87
+ * fail→retry loop should set this explicitly (e.g. 10_000).
88
+ */
89
+ launchTimeout?: number;
84
90
  /**
85
91
  * `wait_until` strategy for `navigate`. Default `"load"`.
86
92
  * Use `"domcontentloaded"` for sites with long-running analytics scripts
@@ -213,6 +219,7 @@ export class Browser {
213
219
  this.context = await chromium.launchPersistentContext(this.profileDir, {
214
220
  headless: !this.opts.headed,
215
221
  viewport: this.opts.viewport ?? { width: 1280, height: 800 },
222
+ ...(this.opts.launchTimeout !== undefined ? { timeout: this.opts.launchTimeout } : {}),
216
223
  ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
217
224
  ? { args: this.opts.launchArgs }
218
225
  : {}),
@@ -14,7 +14,7 @@
14
14
  * 2. A **fully-owned file** harnery creates whole (a shipped skill's
15
15
  * `SKILL.md`), carrying an ownership header comment so `deinit` deletes
16
16
  * only files harnery generated and `--check` flags a hand-edit:
17
- * <!-- harnery:generated <name> v=<hash> — machine-owned … -->
17
+ * <!-- harnery:generated <name> v=<hash>; machine-owned … -->
18
18
  *
19
19
  * Modeled on the first host's HTML-theme splicer (regenerate + byte-compare,
20
20
  * sha256-8 hash, content outside the region untouchable). Pure (no fs) so it's
@@ -133,7 +133,7 @@ export function buildOwnedSkill(opts: {
133
133
  ].join("\n");
134
134
  const body = opts.body.trim();
135
135
  const marker =
136
- `<!-- harnery:generated ${opts.name} v=${shortHash(body)} — machine-owned; ` +
136
+ `<!-- harnery:generated ${opts.name} v=${shortHash(body)}; machine-owned; ` +
137
137
  `regenerated by \`${opts.binName} init\`, removed by \`${opts.binName} deinit\`. ` +
138
138
  `Edit the harnery template, not this file. -->`;
139
139
  return `${fm}\n${marker}\n\n${body}\n`;
@@ -146,7 +146,7 @@ export interface SkillTemplate {
146
146
  function decideBody(b: string): string {
147
147
  return `The decision docket is a persistent queue for decisions you would otherwise
148
148
  route to a human. It's built on the \`${b} decision\` engine. This skill is the
149
- mechanics — file, find precedent, claim, and resolve with evidence. *When* a
149
+ mechanics: file, find precedent, claim, and resolve with evidence. *When* a
150
150
  decision needs a human at all (versus one you settle yourself) is host policy;
151
151
  if this project defines that rubric, follow it.
152
152
 
@@ -159,7 +159,7 @@ if this project defines that rubric, follow it.
159
159
  ## Capture (default)
160
160
 
161
161
  1. **Check precedent first.** \`${b} decision search "<key terms>"\`. If a resolved
162
- decision already answers this, cite it — don't re-litigate.
162
+ decision already answers this, cite it; don't re-litigate.
163
163
  2. **File it** when the choice has a second consumer (a human will want to see it,
164
164
  or a future agent will face it again) or reversal is expensive. Skip the
165
165
  docket for pure local mechanics (a variable name, one of two equivalent idioms).
@@ -172,7 +172,7 @@ if this project defines that rubric, follow it.
172
172
 
173
173
  For a decision with real substance, write a brief to a file and pass
174
174
  \`--brief <path>\` so the reviewer sees options + evidence, not a cold prompt.
175
- 3. **Proceed on your default.** Filing does not mean blocking — note the id in your
175
+ 3. **Proceed on your default.** Filing does not mean blocking. Note the id in your
176
176
  reply and keep working.
177
177
 
178
178
  ## Resolve (\`resolve <id>\`)
@@ -182,7 +182,7 @@ ${b} decision show <id> # read the question + context + any brief
182
182
  ${b} decision claim <id> # mark it deliberating (claimed by you)
183
183
  \`\`\`
184
184
 
185
- Research it for real — run the queries, read the files, compute the costs. Then
185
+ Research it for real: run the queries, read the files, compute the costs. Then
186
186
  resolve with **cited evidence** (the engine rejects an evidence-free resolution):
187
187
 
188
188
  \`\`\`bash
@@ -242,7 +242,7 @@ ${b} council list --mine --json
242
242
 
243
243
  One section per council you're a member of: id + objective, round N (open /
244
244
  collected) with N/M contributors, and your status (awaiting prompt / prompt ready
245
- / already contributed) with the next command to run. Stop after listing — don't
245
+ / already contributed) with the next command to run. Stop after listing; don't
246
246
  auto-route into contribute.
247
247
 
248
248
  ## Create (\`create <objective>\`)
@@ -256,18 +256,18 @@ http://localhost:9000/councils/new?objective=<encoded>
256
256
 
257
257
  If the dev server isn't up, start it with \`${b} web up\`.
258
258
 
259
- ## Contribute (\`contribute <id>\`) — the guarded flow
259
+ ## Contribute (\`contribute <id>\`): the guarded flow
260
260
 
261
261
  Run these checks in order; refuse with a specific reason if any fails.
262
262
 
263
- 1. **Membership.** If your \`whoami\` name isn't in \`manifest.members\`, refuse — the
264
- router likely meant a different agent's session.
265
- 2. **Already contributed.** If you're in \`current_round_contributors\`, refuse —
266
- wait for the steward to advance the round.
263
+ 1. **Membership.** If your \`whoami\` name isn't in \`manifest.members\`, refuse.
264
+ The router likely meant a different agent's session.
265
+ 2. **Already contributed.** If you're in \`current_round_contributors\`, refuse.
266
+ Wait for the steward to advance the round.
267
267
  3. **Prompt routing.** Find your entry in \`current_round_prompts\`. If none is
268
268
  drafted for you, refuse (the steward must write one first). If the routed body
269
269
  carries a \`<!-- council-route … member: <name> -->\` header naming a *different*
270
- agent, refuse — the wrong prompt was pasted into your session.
270
+ agent, refuse: the wrong prompt was pasted into your session.
271
271
  4. **Compose** per your prompt (read \`manifest.target_doc\` in full if set; strip
272
272
  the route header before treating the body as instructions).
273
273
  5. **Submit:**
@@ -282,20 +282,20 @@ Run these checks in order; refuse with a specific reason if any fails.
282
282
  \`<trivial>\` angle-bracket tag on the final line (the exit-criterion parser keys
283
283
  on it). When in doubt, lean \`<trivial>\`.
284
284
 
285
- ## Prompts (\`prompts <id>\`) — steward
285
+ ## Prompts (\`prompts <id>\`): steward
286
286
 
287
287
  1. **Authority.** If your \`whoami\` name ≠ the council's \`steward\`, stop.
288
288
  2. **Plan the round** from the target doc + prior rounds. Round 1 must include a
289
- completeness critic — assign one member the explicit charge: "What important
290
- thing is NOT in this document at all — a missing dimension, not a flaw in what's
291
- written?" Lens-scoped reviewers reliably miss whole absent dimensions.
289
+ completeness critic. Assign one member the explicit charge: "What important
290
+ thing is NOT in this document at all (a missing dimension, not a flaw in what's
291
+ written)?" Lens-scoped reviewers reliably miss whole absent dimensions.
292
292
  3. **Draft + write** one prompt per member missing from \`current_round_prompts\`:
293
293
 
294
294
  \`\`\`bash
295
295
  ${b} council prompt <id> agent-<Name> --message "..." # or --file <path>
296
296
  \`\`\`
297
297
 
298
- The CLI auto-prepends the \`<!-- council-route … -->\` header — never write it by
298
+ The CLI auto-prepends the \`<!-- council-route … -->\` header; never write it by
299
299
  hand. Every prompt must instruct the member to end with the literal
300
300
  \`<substantive>\` / \`<trivial>\` tag.
301
301
 
@@ -315,7 +315,7 @@ export const SKILLS: SkillTemplate[] = [
315
315
  buildOwnedSkill({
316
316
  name: "harn-decide",
317
317
  description:
318
- "File a decision into the docket instead of blocking on a human — search precedent, file it, and proceed on a reversible default; or pick up and resolve an open decision with cited evidence. Use whenever you're about to ask a human a decision-shaped question you could resolve yourself.",
318
+ "File a decision into the docket instead of blocking on a human: search precedent, file it, and proceed on a reversible default; or pick up and resolve an open decision with cited evidence. Use whenever you're about to ask a human a decision-shaped question you could resolve yourself.",
319
319
  argumentHint: "[<the decision / question you're facing> | resolve <id> | review]",
320
320
  binName,
321
321
  body: decideBody(binName),
@@ -328,7 +328,7 @@ export const SKILLS: SkillTemplate[] = [
328
328
  buildOwnedSkill({
329
329
  name: "harn-council",
330
330
  description:
331
- "Interact with the multi-agent council system: list / create / show / prompts (steward) / contribute (member). Guards against misrouting — refuses to contribute when you aren't a member, have already contributed, or weren't routed a prompt.",
331
+ "Interact with the multi-agent council system: list / create / show / prompts (steward) / contribute (member). Guards against misrouting: refuses to contribute when you aren't a member, have already contributed, or weren't routed a prompt.",
332
332
  argumentHint:
333
333
  "[<id-or-fragment> | create <objective> | contribute <id> | prompts <id> | show <id>]",
334
334
  binName,