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 +16 -16
- package/dist/core/agents/rules/stop-hook.d.ts +16 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +88 -6
- package/dist/core/agents/state/heartbeat-writer.d.ts +27 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +43 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +17 -2
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +38 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +24 -1
- package/dist/lib/browser/client.d.ts +6 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +1 -0
- package/dist/lib/instructions/splice.d.ts +1 -1
- package/dist/lib/instructions/splice.js +2 -2
- package/dist/lib/instructions/templates.js +18 -18
- package/package.json +1 -1
- package/src/core/agents/rules/stop-hook.ts +95 -6
- package/src/core/agents/state/heartbeat-writer.ts +55 -0
- package/src/core/hooks/adapter/output.ts +17 -4
- package/src/core/workflow/engine.ts +38 -0
- package/src/core/workflow/proof.ts +23 -1
- package/src/lib/browser/client.ts +7 -0
- package/src/lib/instructions/splice.ts +2 -2
- package/src/lib/instructions/templates.ts +18 -18
package/README.md
CHANGED
|
@@ -7,28 +7,28 @@
|
|
|
7
7
|
|
|
8
8
|
# Harnery
|
|
9
9
|
|
|
10
|
-
> Multi-agent coordination for AI coding agents
|
|
10
|
+
> Multi-agent coordination for AI coding agents: Claude Code, Cursor, and Codex.
|
|
11
11
|
|
|
12
12
|
[](https://github.com/ryanjkelly/harnery/actions/workflows/ci.yml)
|
|
13
13
|
[](https://www.npmjs.com/package/harnery)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
|
|
16
|
-
> ⚠️ **Pre-1.0.** API surface is still settling. Pin a specific minor version (`harnery@^0.
|
|
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
|
|
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
|
|
23
|
-
- **Cross-machine presence
|
|
24
|
-
- **Coordination-aware workflows
|
|
25
|
-
- **Optional isolated workspaces
|
|
26
|
-
- **Durable work above attempts
|
|
27
|
-
- **Bounded goal supervision
|
|
28
|
-
- **Standalone web UI
|
|
29
|
-
- **Backup + sync
|
|
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`
|
|
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
|
|
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
|
|
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**
|
|
94
|
-
- **Toolkit tier**
|
|
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
|
|
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
|
|
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
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
const
|
|
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 <=
|
|
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;
|
|
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
|
|
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
|
-
|
|
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;
|
|
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":"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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>\`)
|
|
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
|
|
232
|
-
router likely meant a different agent's session.
|
|
233
|
-
2. **Already contributed.** If you're in \`current_round_contributors\`, refuse
|
|
234
|
-
|
|
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
|
|
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>\`)
|
|
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
|
|
258
|
-
thing is NOT in this document at all
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
const
|
|
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 <=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>\`)
|
|
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
|
|
264
|
-
router likely meant a different agent's session.
|
|
265
|
-
2. **Already contributed.** If you're in \`current_round_contributors\`, refuse
|
|
266
|
-
|
|
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
|
|
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>\`)
|
|
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
|
|
290
|
-
thing is NOT in this document at all
|
|
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
|
|
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
|
|
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
|
|
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,
|