harnery 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +52 -4
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +54 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* and forwards the result as the adapter-shaped additionalContext payload.
|
|
8
8
|
*
|
|
9
9
|
* Four subsections combined into one additionalContext payload:
|
|
10
|
-
* 1. Peer table:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 1. Peer table: re-emits only when a live peer arrives or leaves, changes
|
|
11
|
+
* lifecycle, or changes its claimed files. Activity and task text never
|
|
12
|
+
* trigger a reprint. Peers past the heartbeat-freshness window are left
|
|
13
|
+
* out and counted in "+N more".
|
|
13
14
|
* 2. Council pending: pending open-council IDs hashed; re-emits when the
|
|
14
15
|
* ID set changes.
|
|
15
16
|
* 3. Focus nudge. Until the session has displayed its current suggested
|
|
@@ -24,14 +25,37 @@
|
|
|
24
25
|
* 5. Codex status footer: a non-deduplicated reminder to put the live status
|
|
25
26
|
* box after the substantive answer. Stop stays observe-only, so missing the
|
|
26
27
|
* footer can never trigger a replacement response.
|
|
28
|
+
* 6. Turn ritual: for adapters whose Stop hook enforces the ritual, shown on
|
|
29
|
+
* the first prompt, after a compaction, and after a bounced turn.
|
|
27
30
|
*
|
|
28
31
|
* Hash files live at:
|
|
32
|
+
* .harnery/.last-turn-ritual.<instance_id>
|
|
29
33
|
* .harnery/.last-peer-hash.<instance_id>
|
|
30
34
|
* .harnery/.last-council-hash.<instance_id>
|
|
31
35
|
* .harnery/.last-task-nudge-hash.<instance_id>
|
|
32
36
|
*
|
|
33
37
|
* First call always emits (hash files don't exist).
|
|
34
38
|
*/
|
|
39
|
+
import { type RemoteMachine } from "../../presence/index.js";
|
|
40
|
+
import type { AgentActivity, TaskState } from "../state/session-state.js";
|
|
41
|
+
interface HeartbeatRow {
|
|
42
|
+
instance_id?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
kind?: string;
|
|
45
|
+
session_id?: string;
|
|
46
|
+
started_at?: string;
|
|
47
|
+
last_heartbeat?: string;
|
|
48
|
+
files_touched?: string[];
|
|
49
|
+
platform?: string;
|
|
50
|
+
task?: string;
|
|
51
|
+
activity?: AgentActivity;
|
|
52
|
+
task_state?: TaskState;
|
|
53
|
+
task_state_reason?: string;
|
|
54
|
+
task_updated_at?: string | null;
|
|
55
|
+
suggested_session_name?: string;
|
|
56
|
+
session_name_seen_for?: string;
|
|
57
|
+
workflow_run_id?: string;
|
|
58
|
+
}
|
|
35
59
|
export interface PromptContextOpts {
|
|
36
60
|
coordRoot: string;
|
|
37
61
|
instanceId: string;
|
|
@@ -50,10 +74,9 @@ export interface PromptContextOpts {
|
|
|
50
74
|
* The caller enables this only for human-facing Codex sessions. */
|
|
51
75
|
statusFooterNudge?: boolean;
|
|
52
76
|
/** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
|
|
53
|
-
* status check). When set, append a
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* status footer. */
|
|
77
|
+
* status check). When set, append a reminder of that contract on the
|
|
78
|
+
* session's first prompt, after a context compaction, and after a turn the
|
|
79
|
+
* Stop hook bounced (see `markTurnRitualBounced`). */
|
|
57
80
|
turnRitualNudge?: string;
|
|
58
81
|
/** The emitter acknowledges exactly these IDs only after output succeeds. */
|
|
59
82
|
onInboxPrepared?: (messageIds: readonly string[]) => void;
|
|
@@ -74,4 +97,29 @@ export declare function renderPromptContext(opts: PromptContextOpts): string;
|
|
|
74
97
|
* re-emit the same advice.
|
|
75
98
|
*/
|
|
76
99
|
export declare function resourceWarningIfChanged(coordRoot: string, instanceId: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Called by the Stop hook when it blocks a turn for a missing ritual step, so
|
|
102
|
+
* the next prompt carries the reminder again.
|
|
103
|
+
*/
|
|
104
|
+
export declare function markTurnRitualBounced(coordRoot: string, instanceId: string): void;
|
|
105
|
+
export interface PeerTableInput {
|
|
106
|
+
peers: HeartbeatRow[];
|
|
107
|
+
remote: RemoteMachine[];
|
|
108
|
+
mySessionId: string;
|
|
109
|
+
nowMs: number;
|
|
110
|
+
/** Sessions whose last heartbeat is older than this are folded into "+N more". */
|
|
111
|
+
freshnessSeconds: number;
|
|
112
|
+
relations?: Map<string, string>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Pure peer-table render plus its change key. The key covers only what makes
|
|
116
|
+
* the table news: which live sessions exist, their lifecycle, and the files
|
|
117
|
+
* they claim. Activity and task text change on nearly every peer tool call, so
|
|
118
|
+
* they render in the table but never trigger a reprint by themselves.
|
|
119
|
+
*/
|
|
120
|
+
export declare function renderPeerTable(input: PeerTableInput): {
|
|
121
|
+
hash: string;
|
|
122
|
+
table: string;
|
|
123
|
+
};
|
|
124
|
+
export {};
|
|
77
125
|
//# sourceMappingURL=prompt-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAwBH,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AASjF,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG1E,UAAU,YAAY;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;0DAGsD;IACtD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA6EnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA6CtF;AAsBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEjF;AA0LD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA2CtF"}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* and forwards the result as the adapter-shaped additionalContext payload.
|
|
8
8
|
*
|
|
9
9
|
* Four subsections combined into one additionalContext payload:
|
|
10
|
-
* 1. Peer table:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 1. Peer table: re-emits only when a live peer arrives or leaves, changes
|
|
11
|
+
* lifecycle, or changes its claimed files. Activity and task text never
|
|
12
|
+
* trigger a reprint. Peers past the heartbeat-freshness window are left
|
|
13
|
+
* out and counted in "+N more".
|
|
13
14
|
* 2. Council pending: pending open-council IDs hashed; re-emits when the
|
|
14
15
|
* ID set changes.
|
|
15
16
|
* 3. Focus nudge. Until the session has displayed its current suggested
|
|
@@ -24,8 +25,11 @@
|
|
|
24
25
|
* 5. Codex status footer: a non-deduplicated reminder to put the live status
|
|
25
26
|
* box after the substantive answer. Stop stays observe-only, so missing the
|
|
26
27
|
* footer can never trigger a replacement response.
|
|
28
|
+
* 6. Turn ritual: for adapters whose Stop hook enforces the ritual, shown on
|
|
29
|
+
* the first prompt, after a compaction, and after a bounced turn.
|
|
27
30
|
*
|
|
28
31
|
* Hash files live at:
|
|
32
|
+
* .harnery/.last-turn-ritual.<instance_id>
|
|
29
33
|
* .harnery/.last-peer-hash.<instance_id>
|
|
30
34
|
* .harnery/.last-council-hash.<instance_id>
|
|
31
35
|
* .harnery/.last-task-nudge-hash.<instance_id>
|
|
@@ -36,7 +40,8 @@ import { createHash } from "node:crypto";
|
|
|
36
40
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
37
41
|
import { join } from "node:path";
|
|
38
42
|
import { coordEnv } from "../../../lib/env.js";
|
|
39
|
-
import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.js";
|
|
43
|
+
import { coordFreshnessSeconds, endOfTurnStatusCommand, hostPromptReminder, resolveBinName, } from "../../config.js";
|
|
44
|
+
import { readContextState } from "../../context/index.js";
|
|
40
45
|
import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.js";
|
|
41
46
|
import { readRemoteMachines } from "../../presence/index.js";
|
|
42
47
|
import { readResourceStatus } from "../../resources/status.js";
|
|
@@ -107,13 +112,12 @@ export function renderPromptContext(opts) {
|
|
|
107
112
|
if (statusMsg)
|
|
108
113
|
sections.push(statusMsg);
|
|
109
114
|
}
|
|
110
|
-
// 7. Adapters whose Stop hook ENFORCES the turn ritual get
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// hash-deduped for the same reason as the Codex footer.
|
|
115
|
+
// 7. Adapters whose Stop hook ENFORCES the turn ritual get the reminder when
|
|
116
|
+
// it is news: on the session's first prompt, after a context compaction,
|
|
117
|
+
// and after a turn the Stop hook bounced. The Stop hook keeps enforcing on
|
|
118
|
+
// every turn, so a missed ritual still costs one retry and re-arms this.
|
|
115
119
|
if (turnRitualNudge) {
|
|
116
|
-
const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, turnRitualNudge);
|
|
120
|
+
const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, sessionId, turnRitualNudge);
|
|
117
121
|
if (ritualMsg)
|
|
118
122
|
sections.push(ritualMsg);
|
|
119
123
|
}
|
|
@@ -181,17 +185,46 @@ function renderHostPromptReminder(coordRoot, selfInstanceId) {
|
|
|
181
185
|
return "";
|
|
182
186
|
return hostPromptReminder(coordRoot) ?? "";
|
|
183
187
|
}
|
|
184
|
-
function
|
|
188
|
+
function turnRitualStatePath(coordRoot, instanceId) {
|
|
189
|
+
return join(coordRoot, ".harnery", `.last-turn-ritual.${instanceId}`);
|
|
190
|
+
}
|
|
191
|
+
const TURN_RITUAL_BOUNCED = "bounced";
|
|
192
|
+
/**
|
|
193
|
+
* Called by the Stop hook when it blocks a turn for a missing ritual step, so
|
|
194
|
+
* the next prompt carries the reminder again.
|
|
195
|
+
*/
|
|
196
|
+
export function markTurnRitualBounced(coordRoot, instanceId) {
|
|
197
|
+
writeHashFile(turnRitualStatePath(coordRoot, instanceId), TURN_RITUAL_BOUNCED);
|
|
198
|
+
}
|
|
199
|
+
function renderTurnRitualReminder(coordRoot, selfInstanceId, sessionId, adapter) {
|
|
185
200
|
const hb = readLiveCoordinationRow(coordRoot, selfInstanceId);
|
|
186
201
|
if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
|
|
187
202
|
return "";
|
|
203
|
+
// The key changes when native context compaction drops the earlier reminder.
|
|
204
|
+
let generation = 0;
|
|
205
|
+
try {
|
|
206
|
+
generation = readContextState(coordRoot, sessionId)?.generation ?? 0;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* no continuity state: treat as the first context generation */
|
|
210
|
+
}
|
|
211
|
+
const key = `context-generation=${generation}`;
|
|
212
|
+
const statePath = turnRitualStatePath(coordRoot, selfInstanceId);
|
|
213
|
+
const previous = safeRead(statePath);
|
|
214
|
+
if (previous === key)
|
|
215
|
+
return "";
|
|
216
|
+
writeHashFile(statePath, key);
|
|
217
|
+
const bounced = previous === TURN_RITUAL_BOUNCED
|
|
218
|
+
? "Your previous turn was bounced by the Stop hook because the ritual was incomplete. "
|
|
219
|
+
: "";
|
|
188
220
|
const bin = resolveBinName(coordRoot);
|
|
189
221
|
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
190
222
|
// Claude Code and Cursor can both enforce completed assistant reply text.
|
|
191
223
|
const paste = adapter === "claude-code" || adapter === "cursor"
|
|
192
224
|
? " and paste its output verbatim in a fenced code block at the end of your reply"
|
|
193
225
|
: "";
|
|
194
|
-
return (
|
|
226
|
+
return (bounced +
|
|
227
|
+
`Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
|
|
195
228
|
"A turn that ends without these is bounced by the Stop hook and costs a retry.");
|
|
196
229
|
}
|
|
197
230
|
function renderStatusFooterReminder(coordRoot, selfInstanceId) {
|
|
@@ -299,8 +332,43 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
299
332
|
const remote = readRemoteMachinesSafe(coordRoot);
|
|
300
333
|
if (peers.length === 0 && remote.length === 0)
|
|
301
334
|
return "";
|
|
302
|
-
|
|
303
|
-
|
|
335
|
+
const rendered = renderPeerTable({
|
|
336
|
+
peers,
|
|
337
|
+
remote,
|
|
338
|
+
mySessionId,
|
|
339
|
+
nowMs: Date.now(),
|
|
340
|
+
freshnessSeconds: coordFreshnessSeconds(coordRoot),
|
|
341
|
+
relations: forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))),
|
|
342
|
+
});
|
|
343
|
+
if (!rendered.table)
|
|
344
|
+
return "";
|
|
345
|
+
const hashFile = join(coordRoot, ".harnery", `.last-peer-hash.${selfInstanceId}`);
|
|
346
|
+
const oldHash = safeRead(hashFile);
|
|
347
|
+
if (oldHash && oldHash === rendered.hash)
|
|
348
|
+
return "";
|
|
349
|
+
// Persist hash atomically (temp + rename, same convention as other coord writes).
|
|
350
|
+
writeHashFile(hashFile, rendered.hash);
|
|
351
|
+
return rendered.table;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Pure peer-table render plus its change key. The key covers only what makes
|
|
355
|
+
* the table news: which live sessions exist, their lifecycle, and the files
|
|
356
|
+
* they claim. Activity and task text change on nearly every peer tool call, so
|
|
357
|
+
* they render in the table but never trigger a reprint by themselves.
|
|
358
|
+
*/
|
|
359
|
+
export function renderPeerTable(input) {
|
|
360
|
+
const { peers, remote, mySessionId, nowMs, freshnessSeconds } = input;
|
|
361
|
+
const cutoffMs = nowMs - freshnessSeconds * 1000;
|
|
362
|
+
const live = [];
|
|
363
|
+
const stale = [];
|
|
364
|
+
for (const peer of peers) {
|
|
365
|
+
const seenMs = Date.parse(peer.last_heartbeat ?? "");
|
|
366
|
+
if (Number.isFinite(seenMs) && seenMs >= cutoffMs)
|
|
367
|
+
live.push(peer);
|
|
368
|
+
else
|
|
369
|
+
stale.push(peer);
|
|
370
|
+
}
|
|
371
|
+
const basis = live
|
|
304
372
|
.map((p) => ({
|
|
305
373
|
name: p.name ?? null,
|
|
306
374
|
instance_id: p.instance_id ?? null,
|
|
@@ -309,39 +377,28 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
309
377
|
started_at: p.started_at ?? null,
|
|
310
378
|
files_touched: Array.from(p.files_touched ?? []).sort(),
|
|
311
379
|
platform: p.platform ?? null,
|
|
312
|
-
activity: p.activity ?? "unknown",
|
|
313
380
|
task_state: p.task_state ?? "active",
|
|
314
381
|
task_state_reason: p.task_state_reason ?? null,
|
|
315
382
|
}))
|
|
316
383
|
.sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
|
|
317
|
-
// Remote basis: machine + per-agent identity
|
|
318
|
-
//
|
|
384
|
+
// Remote basis: machine + per-agent identity, lifecycle, and files
|
|
385
|
+
// (published_at, activity, and task are excluded for the same reason).
|
|
319
386
|
const remoteBasis = remote.map((m) => ({
|
|
320
387
|
machine: m.machine,
|
|
321
388
|
agents: m.agents.map((a) => ({
|
|
322
389
|
instance_id: a.instance_id,
|
|
323
390
|
name: a.name ?? null,
|
|
324
|
-
task: a.task ?? null,
|
|
325
|
-
activity: a.activity,
|
|
326
391
|
task_state: a.task_state,
|
|
327
392
|
task_state_reason: a.task_state_reason ?? null,
|
|
328
393
|
files_touched: Array.from(a.files_touched ?? []).sort(),
|
|
329
394
|
})),
|
|
330
395
|
}));
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
return "";
|
|
336
|
-
// Render peer table via the same formatter used at SessionStart.
|
|
337
|
-
const local = formatPeerTable(peers, mySessionId, forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))));
|
|
338
|
-
const remoteTable = formatRemoteTable(remote);
|
|
396
|
+
const hash = sha256Hex16(JSON.stringify({ local: basis, remote: remoteBasis }));
|
|
397
|
+
const nowSec = Math.floor(nowMs / 1000);
|
|
398
|
+
const local = formatPeerTable(live, stale, mySessionId, nowSec, input.relations);
|
|
399
|
+
const remoteTable = formatRemoteTable(remote, nowSec);
|
|
339
400
|
const table = [local, remoteTable].filter(Boolean).join("\n\n");
|
|
340
|
-
|
|
341
|
-
return "";
|
|
342
|
-
// Persist hash atomically (temp + rename, same convention as other coord writes).
|
|
343
|
-
writeHashFile(hashFile, newHash);
|
|
344
|
-
return table;
|
|
401
|
+
return { hash, table };
|
|
345
402
|
}
|
|
346
403
|
/** readRemoteMachines with a local failure guard (rendering must never throw). */
|
|
347
404
|
function readRemoteMachinesSafe(coordRoot) {
|
|
@@ -353,10 +410,9 @@ function readRemoteMachinesSafe(coordRoot) {
|
|
|
353
410
|
}
|
|
354
411
|
}
|
|
355
412
|
/** Render sessions on other machines (presence transport) as a subtable. */
|
|
356
|
-
function formatRemoteTable(remote) {
|
|
413
|
+
function formatRemoteTable(remote, nowSec) {
|
|
357
414
|
if (remote.length === 0)
|
|
358
415
|
return "";
|
|
359
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
360
416
|
const lines = [];
|
|
361
417
|
for (const m of remote) {
|
|
362
418
|
for (const a of m.agents.slice(0, 10)) {
|
|
@@ -451,16 +507,11 @@ function writeHashFile(path, value) {
|
|
|
451
507
|
/* swallow */
|
|
452
508
|
}
|
|
453
509
|
}
|
|
454
|
-
|
|
455
|
-
* intentionally co-located here so the prompt-context renderer is self-contained
|
|
456
|
-
* (no cross-file imports of formatting internals). If both renderers diverge,
|
|
457
|
-
* pull the shared bits into a small util module. */
|
|
458
|
-
function formatPeerTable(peers, mySessionId, relations = new Map()) {
|
|
510
|
+
function formatPeerTable(peers, stalePeers, mySessionId, nowSec, relations = new Map()) {
|
|
459
511
|
if (peers.length === 0)
|
|
460
512
|
return "";
|
|
461
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
462
513
|
const fold = {};
|
|
463
|
-
for (const p of peers) {
|
|
514
|
+
for (const p of [...peers, ...stalePeers]) {
|
|
464
515
|
const kind = p.kind ?? "unknown";
|
|
465
516
|
if (kind === "transient" && p.session_id) {
|
|
466
517
|
fold[p.session_id] = (fold[p.session_id] ?? []).concat(p.files_touched ?? []);
|
|
@@ -474,28 +525,52 @@ function formatPeerTable(peers, mySessionId, relations = new Map()) {
|
|
|
474
525
|
const relation = relations.get(p.instance_id ?? "");
|
|
475
526
|
return { ...p, display_files: display, ...(relation ? { relation } : {}) };
|
|
476
527
|
});
|
|
528
|
+
assignUniqueLabels(rows);
|
|
529
|
+
const staleRows = stalePeers.filter((p) => (p.kind ?? "unknown") !== "transient");
|
|
477
530
|
// Related sessions sort first so a fork parent never hides in "+N more".
|
|
478
531
|
const blocking = rows
|
|
479
532
|
.filter((p) => p.session_id !== mySessionId)
|
|
480
533
|
.sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
|
|
481
534
|
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
535
|
+
const staleBlocking = staleRows.filter((p) => p.session_id !== mySessionId).length;
|
|
536
|
+
const staleGroup = staleRows.length - staleBlocking;
|
|
482
537
|
const out = [];
|
|
483
|
-
const blk = renderSubtable(blocking, "Other agent groups active (their files block you):", nowSec);
|
|
538
|
+
const blk = renderSubtable(blocking, staleBlocking, "Other agent groups active (their files block you):", nowSec);
|
|
484
539
|
if (blk)
|
|
485
540
|
out.push(blk);
|
|
486
|
-
const grp = renderSubtable(group, "Your group (subagents / parent / siblings; no mutual block):", nowSec);
|
|
541
|
+
const grp = renderSubtable(group, staleGroup, "Your group (subagents / parent / siblings; no mutual block):", nowSec);
|
|
487
542
|
if (grp)
|
|
488
543
|
out.push(grp);
|
|
489
544
|
return out.join("\n\n");
|
|
490
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* One full, distinct label per row. An unnamed session shows its whole
|
|
548
|
+
* instance id (an 8-character prefix collides for time-ordered ids), and a
|
|
549
|
+
* name two live rows share carries its instance id so they read as two.
|
|
550
|
+
*/
|
|
551
|
+
function assignUniqueLabels(rows) {
|
|
552
|
+
const base = (r) => r.name ?? r.instance_id ?? "unknown";
|
|
553
|
+
const counts = new Map();
|
|
554
|
+
for (const r of rows)
|
|
555
|
+
counts.set(base(r), (counts.get(base(r)) ?? 0) + 1);
|
|
556
|
+
for (const r of rows) {
|
|
557
|
+
const label = base(r);
|
|
558
|
+
r.label =
|
|
559
|
+
(counts.get(label) ?? 0) > 1 && r.name && r.instance_id
|
|
560
|
+
? `${label} (${r.instance_id})`
|
|
561
|
+
: label;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
491
564
|
function byStartedAt(a, b) {
|
|
492
565
|
return (a.started_at ?? "").localeCompare(b.started_at ?? "");
|
|
493
566
|
}
|
|
494
|
-
function renderSubtable(rows, header, nowSec) {
|
|
567
|
+
function renderSubtable(rows, staleCount, header, nowSec) {
|
|
495
568
|
if (rows.length === 0)
|
|
496
569
|
return "";
|
|
497
570
|
const first = rows.slice(0, 10).map((r) => formatRow(r, nowSec));
|
|
498
|
-
const
|
|
571
|
+
const more = Math.max(0, rows.length - 10) + staleCount;
|
|
572
|
+
const staleNote = staleCount > 0 ? ` (${staleCount} with no recent heartbeat)` : "";
|
|
573
|
+
const overflow = more > 0 ? `\n +${more} more${staleNote}` : "";
|
|
499
574
|
return `${header}\n${first.join("\n")}${overflow}`;
|
|
500
575
|
}
|
|
501
576
|
function formatRow(r, nowSec) {
|
|
@@ -506,9 +581,8 @@ function formatRow(r, nowSec) {
|
|
|
506
581
|
const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
|
|
507
582
|
const ageFrom = startedSec === null ? "age unknown" : fmtAge(nowSec - startedSec);
|
|
508
583
|
const filesPart = fmtFiles(r.display_files);
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
|
|
584
|
+
// An unnamed row shows its full instance id; a prefix can collide.
|
|
585
|
+
const label = r.label ?? r.name ?? r.instance_id ?? "unknown";
|
|
512
586
|
const relation = r.relation ? ` [${r.relation}]` : "";
|
|
513
587
|
return ` - agent-${label}${relation}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
|
|
514
588
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAgBlD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,aAAa,CAmHjF"}
|
|
@@ -17,7 +17,9 @@ import { spawnSync } from "node:child_process";
|
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { adapterFromPlatform as sharedAdapterFromPlatform } from "../../adapter.js";
|
|
20
|
-
import {
|
|
20
|
+
import { resolveBinName } from "../../config.js";
|
|
21
|
+
import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.js";
|
|
22
|
+
import { LiveCoordinationAuthorityV3Error, recordLiveClaimChangeV3 } from "../live-authority-v3.js";
|
|
21
23
|
import { readLiveCoordinationRows } from "../state/live-coordination-view.js";
|
|
22
24
|
const FRESHNESS_SECS = 600;
|
|
23
25
|
/**
|
|
@@ -47,8 +49,9 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
47
49
|
// prune the claim from their heartbeat and allow the edit. Skips when the
|
|
48
50
|
// file doesn't exist (intent-to-create claim); that surface IS load-bearing.
|
|
49
51
|
if (isFileCommittedClean(coordRoot, req.path)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const refused = releaseClaimThroughLedger(coordRoot, conflict.instance_id, req.path, req);
|
|
53
|
+
if (refused) {
|
|
54
|
+
return authorityUnavailable(coordRoot, req, req.path, "release the stale claim on", refused);
|
|
52
55
|
}
|
|
53
56
|
// Fall through to ordering check + allow.
|
|
54
57
|
}
|
|
@@ -109,8 +112,9 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
109
112
|
// Every blocker is a finished (committed-clean) edit: prune them so they
|
|
110
113
|
// stop constraining future acquisitions, then fall through to allow.
|
|
111
114
|
for (const p of blockers) {
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
const refused = releaseClaimThroughLedger(coordRoot, req.instance_id, p, req);
|
|
116
|
+
if (refused) {
|
|
117
|
+
return authorityUnavailable(coordRoot, req, p, "release the completed claim on", refused);
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
}
|
|
@@ -118,9 +122,9 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
118
122
|
// Acquire the claim: atomic check-and-set. Adds req.path to my
|
|
119
123
|
// files_touched if not already present.
|
|
120
124
|
if (req.mode !== "read") {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const refused = acquireClaimThroughLedger(coordRoot, req);
|
|
126
|
+
if (refused)
|
|
127
|
+
return authorityUnavailable(coordRoot, req, req.path, "acquire", refused);
|
|
124
128
|
}
|
|
125
129
|
return { allow: true, exit_code: 0, rule: "claim.pass" };
|
|
126
130
|
}
|
|
@@ -205,6 +209,7 @@ function isFileCommittedClean(coordRoot, relPath) {
|
|
|
205
209
|
return false;
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
/** Returns null when the ledger recorded the claim, else the refusal reason. */
|
|
208
213
|
function acquireClaimThroughLedger(coordRoot, req) {
|
|
209
214
|
const owner = readPeers(coordRoot).find((peer) => peer.instance_id === req.instance_id);
|
|
210
215
|
try {
|
|
@@ -217,10 +222,10 @@ function acquireClaimThroughLedger(coordRoot, req) {
|
|
|
217
222
|
path: req.path,
|
|
218
223
|
access: "write",
|
|
219
224
|
});
|
|
220
|
-
return
|
|
225
|
+
return null;
|
|
221
226
|
}
|
|
222
|
-
catch {
|
|
223
|
-
return
|
|
227
|
+
catch (error) {
|
|
228
|
+
return refusalReason(error);
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
function releaseClaimThroughLedger(coordRoot, subjectInstanceId, relPath, actor) {
|
|
@@ -236,17 +241,32 @@ function releaseClaimThroughLedger(coordRoot, subjectInstanceId, relPath, actor)
|
|
|
236
241
|
path: relPath,
|
|
237
242
|
access: "write",
|
|
238
243
|
});
|
|
239
|
-
return
|
|
244
|
+
return null;
|
|
240
245
|
}
|
|
241
|
-
catch {
|
|
242
|
-
return
|
|
246
|
+
catch (error) {
|
|
247
|
+
return refusalReason(error);
|
|
243
248
|
}
|
|
244
249
|
}
|
|
245
|
-
function
|
|
250
|
+
function refusalReason(error) {
|
|
251
|
+
if (error instanceof LiveCoordinationAuthorityV3Error)
|
|
252
|
+
return error.reason;
|
|
253
|
+
return error instanceof Error ? error.message : String(error);
|
|
254
|
+
}
|
|
255
|
+
function authorityUnavailable(coordRoot, req, path, operation, refusal) {
|
|
256
|
+
// The ledger's own reason is the only evidence of why a healthy-looking
|
|
257
|
+
// session cannot claim; a generic message sent agents to diagnose the wrong
|
|
258
|
+
// layer, so keep the reason in the verdict and in a durable diagnostic.
|
|
259
|
+
writeProducerDiagnosticV3(coordRoot, "claim_authority_refused", {
|
|
260
|
+
instance_id: req.instance_id,
|
|
261
|
+
operation,
|
|
262
|
+
reason: refusal,
|
|
263
|
+
});
|
|
246
264
|
return {
|
|
247
265
|
allow: false,
|
|
248
266
|
exit_code: 2,
|
|
249
267
|
rule: "claim.authority_unavailable",
|
|
250
|
-
reason: `Cannot safely ${operation} ${path};
|
|
268
|
+
reason: `Cannot safely ${operation} ${path}; the coordination ledger refused the claim ` +
|
|
269
|
+
`(${refusal}). Run \`${resolveBinName(coordRoot)} agents health\` and retry; if health ` +
|
|
270
|
+
"is safe, the refusal reason above names the session state that blocked it.",
|
|
251
271
|
};
|
|
252
272
|
}
|
|
@@ -26,6 +26,12 @@ export interface StopHookRequest {
|
|
|
26
26
|
session_name_observation_unavailable?: boolean;
|
|
27
27
|
bypass?: boolean;
|
|
28
28
|
workflow_child?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Claude Code moved this session's foreground command into the background
|
|
31
|
+
* and has not yet reported it finished. Finalization cannot pass until it
|
|
32
|
+
* does, so status and reply-box demands wait for the notification turn.
|
|
33
|
+
*/
|
|
34
|
+
pending_background_handoff?: boolean;
|
|
29
35
|
}
|
|
30
36
|
export declare const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
|
|
31
37
|
export declare function evaluateStopHook(coordRoot: string, req: StopHookRequest): VerdictResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAM3D,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,iFAAiF;IACjF,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAM3D,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,iFAAiF;IACjF,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAInE,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CA6BvF;AAED,oFAAoF;AACpF,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,SAAS,OAAO,EAAE,GACzB,aAAa,CA2If"}
|
|
@@ -101,6 +101,8 @@ export function evaluateStopHookV3Events(coordRoot, req, events) {
|
|
|
101
101
|
// Pure prose turns keep the human-visible acknowledgement but do not require
|
|
102
102
|
// task or final-status mutations. This preserves the pre-V3 policy exactly.
|
|
103
103
|
if (!usedTool) {
|
|
104
|
+
if (!ackPresent && req.pending_background_handoff)
|
|
105
|
+
return backgroundHandoffDeferred();
|
|
104
106
|
if (!ackPresent)
|
|
105
107
|
return withCycleAnchor(ackBlock(), cycleAnchor);
|
|
106
108
|
return {
|
|
@@ -133,11 +135,28 @@ export function evaluateStopHookV3Events(coordRoot, req, events) {
|
|
|
133
135
|
pending.push(sessionNameBlock(coordRoot, req.instance_id));
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
|
-
|
|
138
|
+
// A status check run now would refuse on the in-flight commit and a box
|
|
139
|
+
// cannot be issued without it, so bouncing only loops until the task ends.
|
|
140
|
+
// Task and naming evidence do not depend on the task and stay enforced.
|
|
141
|
+
const enforced = req.pending_background_handoff
|
|
142
|
+
? pending.filter((block) => block.rule !== "stop-hook.rule_1_3" && block.rule !== "stop-hook.rule_2_3")
|
|
143
|
+
: pending;
|
|
144
|
+
const blocked = mergeStopBlocks(enforced);
|
|
137
145
|
if (blocked)
|
|
138
146
|
return withCycleAnchor(blocked, cycleAnchor);
|
|
147
|
+
if (enforced.length < pending.length)
|
|
148
|
+
return backgroundHandoffDeferred();
|
|
139
149
|
return { allow: true, exit_code: 0, rule: "stop-hook.pass" };
|
|
140
150
|
}
|
|
151
|
+
function backgroundHandoffDeferred() {
|
|
152
|
+
return {
|
|
153
|
+
allow: true,
|
|
154
|
+
exit_code: 0,
|
|
155
|
+
rule: "stop-hook.background_handoff_deferred",
|
|
156
|
+
reason: "a command Claude Code moved to the background is still running; status and reply-box " +
|
|
157
|
+
"evidence are required on the turn its completion notification starts",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
141
160
|
function unconditionalVerdict(req) {
|
|
142
161
|
if (req.bypass) {
|
|
143
162
|
return {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* two callers reading the same live process disagree.
|
|
32
32
|
*/
|
|
33
33
|
/** Which probe answers "when did this process start?" on this machine. */
|
|
34
|
-
export type StartTokenProbe = "procfs" | "ps";
|
|
34
|
+
export type StartTokenProbe = "procfs" | "ps" | "windows";
|
|
35
35
|
/**
|
|
36
36
|
* Which probe this machine uses, decided once.
|
|
37
37
|
*
|
|
@@ -68,6 +68,8 @@ export declare function psStartToken(pid: number, run?: (pid: number) => {
|
|
|
68
68
|
* will not say.
|
|
69
69
|
*/
|
|
70
70
|
export declare function processStartToken(pid: number): string | null;
|
|
71
|
+
/** Windows process creation time in invariant UTC ticks, independent of locale. */
|
|
72
|
+
export declare function windowsStartToken(pid: number): string | null;
|
|
71
73
|
export type ProcessGenerationVerdict = "same" | "different" | "unknown";
|
|
72
74
|
type ReadProcessStartToken = (pid: number) => string | null;
|
|
73
75
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC;AAc1D;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAQjD;AAyBD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAiB,GAC7E,MAAM,GAAG,IAAI,CAUf;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAExE,KAAK,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,SAAS,GAAE,qBAAyC,GACnD,wBAAwB,CAuB1B;AAOD,MAAM,WAAW,mCAAmC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAClE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,YAAY,GAAE,mCAAwC,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,CAUtD"}
|