harnery 0.31.6 → 0.33.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 +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +5 -1
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +280 -94
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/claude-desktop.d.ts +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +74 -3
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +141 -19
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -5
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +4 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +2 -3
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +6 -5
- package/dist/core/agents/cli.js +128 -89
- package/dist/core/agents/coord-client.d.ts +18 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/render/prompt-context.d.ts +12 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +70 -16
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +23 -4
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +98 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +86 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/agents/state/session-state.d.ts +44 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +81 -0
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/parse.d.ts +1 -1
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +2 -0
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +258 -32
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +57 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/presence/blob.d.ts +4 -0
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +12 -0
- package/dist/core/presence/index.js +15 -1
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/claude-desktop.d.ts +47 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +169 -1
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/bundle.d.ts +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +2 -2
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +5 -1
- package/src/commands/agents.ts +341 -113
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +155 -21
- package/src/commands/init.ts +28 -9
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +7 -6
- package/src/core/agents/cli.ts +143 -89
- package/src/core/agents/coord-client.ts +21 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +96 -16
- package/src/core/agents/render/session-context.ts +29 -4
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +108 -1
- package/src/core/agents/state/heartbeat-writer.ts +129 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +296 -27
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +71 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/bundle.ts +199 -0
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* + sorted(files_touched) + platform, sorted by instance_id).
|
|
13
13
|
* 2. Council pending: pending open-council IDs hashed; re-emits when the
|
|
14
14
|
* ID set changes.
|
|
15
|
-
* 3. Focus nudge.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* 3. Focus nudge. Until the session has produced its suggested name (first
|
|
16
|
+
* non-empty set-task), tells every adapter — on every prompt, undeduped —
|
|
17
|
+
* to reproduce set-task's suggested session name in a fenced code block.
|
|
18
|
+
* Cursor/Codex additionally get the existing unset/stale-task reminder
|
|
19
|
+
* because their Stop hooks do not enforce task declarations as reliably
|
|
20
|
+
* as Claude Code's.
|
|
20
21
|
* 4. Codex status footer: a non-deduplicated reminder to put the live status
|
|
21
22
|
* box after the substantive answer. Stop stays observe-only, so missing the
|
|
22
23
|
* footer can never trigger a replacement response.
|
|
@@ -32,7 +33,7 @@ import { createHash } from "node:crypto";
|
|
|
32
33
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
33
34
|
import { join } from "node:path";
|
|
34
35
|
import { coordEnv } from "../../../lib/env.js";
|
|
35
|
-
import { resolveBinName } from "../../config.js";
|
|
36
|
+
import { endOfTurnStatusCommand, resolveBinName } from "../../config.js";
|
|
36
37
|
import { readRemoteMachines } from "../../presence/index.js";
|
|
37
38
|
import { formatPendingCouncils } from "./session-context.js";
|
|
38
39
|
/**
|
|
@@ -44,7 +45,7 @@ import { formatPendingCouncils } from "./session-context.js";
|
|
|
44
45
|
* councils are pending, matching the bash behavior).
|
|
45
46
|
*/
|
|
46
47
|
export function renderPromptContext(opts) {
|
|
47
|
-
const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, statusFooterNudge, } = opts;
|
|
48
|
+
const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, statusFooterNudge, turnRitualNudge, } = opts;
|
|
48
49
|
const sections = [];
|
|
49
50
|
// 1. Peer table with hash dedup.
|
|
50
51
|
const peerTable = computePeerTableIfChanged(coordRoot, instanceId, sessionId);
|
|
@@ -75,15 +76,40 @@ export function renderPromptContext(opts) {
|
|
|
75
76
|
if (statusMsg)
|
|
76
77
|
sections.push(statusMsg);
|
|
77
78
|
}
|
|
79
|
+
// 5. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
|
|
80
|
+
// on every prompt. Reminding before the reply is drafted is far cheaper than
|
|
81
|
+
// the bounce-and-retry the Stop hook otherwise forces: enforcement-only
|
|
82
|
+
// compliance was measured missing on a double-digit share of turns. Not
|
|
83
|
+
// hash-deduped for the same reason as the Codex footer.
|
|
84
|
+
if (turnRitualNudge) {
|
|
85
|
+
const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, turnRitualNudge);
|
|
86
|
+
if (ritualMsg)
|
|
87
|
+
sections.push(ritualMsg);
|
|
88
|
+
}
|
|
78
89
|
return sections.join("\n\n");
|
|
79
90
|
}
|
|
80
|
-
function
|
|
91
|
+
function renderTurnRitualReminder(coordRoot, selfInstanceId, adapter) {
|
|
81
92
|
const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
|
|
82
93
|
if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
|
|
83
94
|
return "";
|
|
84
95
|
const bin = resolveBinName(coordRoot);
|
|
96
|
+
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
97
|
+
// Claude Code renders the status box only if the reply carries it; Cursor
|
|
98
|
+
// shows the box inline on its own, so it is never asked for a second copy.
|
|
99
|
+
const paste = adapter === "claude-code"
|
|
100
|
+
? " and paste its output verbatim in a fenced code block at the end of your reply"
|
|
101
|
+
: "";
|
|
102
|
+
return (`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}. ` +
|
|
103
|
+
"A turn that ends without these is bounced by the Stop hook and costs a retry.");
|
|
104
|
+
}
|
|
105
|
+
function renderStatusFooterReminder(coordRoot, selfInstanceId) {
|
|
106
|
+
const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
|
|
107
|
+
if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
|
|
108
|
+
return "";
|
|
109
|
+
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
85
110
|
return ("Codex status footer: complete the user's request first. " +
|
|
86
|
-
`Then run \`${
|
|
111
|
+
`Then run \`${statusCommand}\` as your final shell call and append its stdout verbatim in a fenced code block at the bottom of the same substantive reply. ` +
|
|
112
|
+
"If it fails, finish the owned Git work and rerun it before replying. " +
|
|
87
113
|
"Keep the answer intact. If the footer is missed, do not retry or replace the reply; the Stop hook is observe-only.");
|
|
88
114
|
}
|
|
89
115
|
/**
|
|
@@ -114,13 +140,19 @@ function computeFocusNudgeIfChanged(coordRoot, selfInstanceId, opts) {
|
|
|
114
140
|
let needsNudge = false;
|
|
115
141
|
let message = "";
|
|
116
142
|
let nudgeKind = "";
|
|
117
|
-
if (opts.sessionNameNudge &&
|
|
143
|
+
if (opts.sessionNameNudge &&
|
|
144
|
+
(!hb.suggested_session_name ||
|
|
145
|
+
(hb.session_name_seen_for !== undefined &&
|
|
146
|
+
hb.suggested_session_name !== hb.session_name_seen_for))) {
|
|
147
|
+
// Keyed on "a name was ever produced", not task_updated_at: a bare clear
|
|
148
|
+
// as the first declaration must not end the naming window.
|
|
118
149
|
needsNudge = true;
|
|
119
150
|
nudgeKind = "session-name";
|
|
120
|
-
message =
|
|
121
|
-
`
|
|
151
|
+
message = hb.suggested_session_name
|
|
152
|
+
? `This session's lifecycle changed its suggested name. Reproduce this value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title: ${hb.suggested_session_name}`
|
|
153
|
+
: `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
|
|
122
154
|
"When it returns `first_of_session: true`, reproduce its `suggested_session_name` value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title. " +
|
|
123
|
-
"Then continue with the task; that
|
|
155
|
+
"Then continue with the task; that `set-task` also satisfies this turn's focus declaration.";
|
|
124
156
|
}
|
|
125
157
|
else if (opts.taskNudge && !taskValue) {
|
|
126
158
|
needsNudge = true;
|
|
@@ -145,8 +177,17 @@ function computeFocusNudgeIfChanged(coordRoot, selfInstanceId, opts) {
|
|
|
145
177
|
clearHashFile(hashFile);
|
|
146
178
|
return "";
|
|
147
179
|
}
|
|
148
|
-
//
|
|
149
|
-
//
|
|
180
|
+
// The session-name reminder is deliberately NOT deduped: it re-emits on
|
|
181
|
+
// every prompt until a name is produced, because the "still unnamed" state
|
|
182
|
+
// is the failure state — deduping it erases the only reminder after one
|
|
183
|
+
// ignored prompt (the miss mode operators reported). Bounded in practice:
|
|
184
|
+
// Stop rule 3/3 forces a set-task on the first tool-using turn.
|
|
185
|
+
if (nudgeKind === "session-name") {
|
|
186
|
+
clearHashFile(hashFile);
|
|
187
|
+
return message;
|
|
188
|
+
}
|
|
189
|
+
// Dedup the task-unset/stale reminders on kind + task state so a repeat
|
|
190
|
+
// prompt in the same state stays quiet.
|
|
150
191
|
const state = `${nudgeKind}|task=${taskValue}|updated=${hb.task_updated_at ?? ""}|threshold=${threshold}`;
|
|
151
192
|
const newHash = sha256Hex16(state);
|
|
152
193
|
const oldHash = safeRead(hashFile);
|
|
@@ -191,6 +232,9 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
191
232
|
started_at: p.started_at ?? null,
|
|
192
233
|
files_touched: Array.from(p.files_touched ?? []).sort(),
|
|
193
234
|
platform: p.platform ?? null,
|
|
235
|
+
activity: p.activity ?? "unknown",
|
|
236
|
+
task_state: p.task_state ?? "active",
|
|
237
|
+
task_state_reason: p.task_state_reason ?? null,
|
|
194
238
|
}))
|
|
195
239
|
.sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
|
|
196
240
|
// Remote basis: machine + per-agent identity/task/files (published_at is
|
|
@@ -201,6 +245,9 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
201
245
|
instance_id: a.instance_id,
|
|
202
246
|
name: a.name ?? null,
|
|
203
247
|
task: a.task ?? null,
|
|
248
|
+
activity: a.activity,
|
|
249
|
+
task_state: a.task_state,
|
|
250
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
204
251
|
files_touched: Array.from(a.files_touched ?? []).sort(),
|
|
205
252
|
})),
|
|
206
253
|
}));
|
|
@@ -380,6 +427,7 @@ function renderSubtable(rows, header, nowSec) {
|
|
|
380
427
|
}
|
|
381
428
|
function formatRow(r, nowSec) {
|
|
382
429
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
430
|
+
const statePart = formatState(r);
|
|
383
431
|
// Fall back started_at → last_heartbeat; if neither is a valid timestamp,
|
|
384
432
|
// show "age unknown" rather than the epoch-derived "20608d ago" ghost.
|
|
385
433
|
const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
|
|
@@ -390,7 +438,13 @@ function formatRow(r, nowSec) {
|
|
|
390
438
|
// Prefer a short instance_id over a bare "unknown" so an incomplete row is
|
|
391
439
|
// still identifiable.
|
|
392
440
|
const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
|
|
393
|
-
return ` - agent-${label}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
441
|
+
return ` - agent-${label}${taskPart} (${statePart}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
442
|
+
}
|
|
443
|
+
function formatState(r) {
|
|
444
|
+
const activity = r.activity ?? "unknown";
|
|
445
|
+
const lifecycle = r.task_state ?? "active";
|
|
446
|
+
const reason = lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
447
|
+
return `activity=${activity}, lifecycle=${lifecycle}${reason}`;
|
|
394
448
|
}
|
|
395
449
|
function fmtFiles(files) {
|
|
396
450
|
if (files.length === 0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AA0B1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6E7D;AAuJD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
|
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { resolveBinName, resolveHooksSetupHint } from "../../config.js";
|
|
13
13
|
import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.js";
|
|
14
14
|
import { readRemoteMachines } from "../../presence/index.js";
|
|
15
|
+
import { readForkParent } from "../state/names.js";
|
|
15
16
|
/**
|
|
16
17
|
* Build the combined SessionStart systemMessage. Returns the additionalContext
|
|
17
18
|
* string (or "" if there's nothing to say).
|
|
@@ -26,7 +27,19 @@ export function renderSessionContext(opts) {
|
|
|
26
27
|
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
27
28
|
if (agentName) {
|
|
28
29
|
const suffix = platformLabel ? ` (${platformLabel})` : "";
|
|
29
|
-
|
|
30
|
+
// The authority clause guards forked/branched chats. An adapter that forks a
|
|
31
|
+
// conversation copies the parent's transcript, so the fork's context still
|
|
32
|
+
// asserts the parent's name, claims, and task. When recorded fork lineage
|
|
33
|
+
// (.name-history forked_from) names the parent, say so specifically — the
|
|
34
|
+
// fork is about to read a transcript full of that exact name. Otherwise
|
|
35
|
+
// fall back to the generic clause, which needs no fork detection.
|
|
36
|
+
const forkParent = readForkParent(coordRoot, instanceId);
|
|
37
|
+
const authority = forkParent?.name
|
|
38
|
+
? `This conversation was branched from agent-${forkParent.name}'s session: earlier context showing agent-${forkParent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`
|
|
39
|
+
: forkParent
|
|
40
|
+
? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
|
|
41
|
+
: "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
42
|
+
const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
|
|
30
43
|
messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
|
|
31
44
|
}
|
|
32
45
|
else if (peerTable) {
|
|
@@ -48,7 +61,7 @@ export function renderSessionContext(opts) {
|
|
|
48
61
|
const hint = resolveHooksSetupHint(coordRoot);
|
|
49
62
|
const fix = hint
|
|
50
63
|
? `Run \`${hint}\` to install them.`
|
|
51
|
-
:
|
|
64
|
+
: `Run \`${resolveBinName(coordRoot)} init\` to install the harnery-managed git-hook regions (commit guard + claim pruning).`;
|
|
52
65
|
const wiringSummary = `Coordination hooks are NOT wired: the E-guard will not block conflicting commits, and post-commit claim pruning will not run. ${fix} Detected:\n${wiringIssues.map((i) => ` - ${i}`).join("\n")}`;
|
|
53
66
|
messages.push(wiringSummary);
|
|
54
67
|
}
|
|
@@ -88,7 +101,10 @@ function formatRemoteMachines(coordRoot) {
|
|
|
88
101
|
const files = a.files_touched?.length
|
|
89
102
|
? `holds: ${a.files_touched.slice(0, 3).join(", ")}${a.files_touched.length > 3 ? `, +${a.files_touched.length - 3} more` : ""}`
|
|
90
103
|
: "nothing held";
|
|
91
|
-
|
|
104
|
+
const reason = a.task_state === "blocked" && a.task_state_reason
|
|
105
|
+
? `: ${a.task_state_reason.slice(0, 80)}`
|
|
106
|
+
: "";
|
|
107
|
+
lines.push(` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (activity=${a.activity}, lifecycle=${a.task_state}${reason}, ${files})`);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
return `Sessions on other machines (advisory, via presence refs):\n${lines.join("\n")}`;
|
|
@@ -166,11 +182,14 @@ function renderSubtable(rows, header, nowSec) {
|
|
|
166
182
|
}
|
|
167
183
|
function formatRow(r, nowSec) {
|
|
168
184
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
185
|
+
const activity = r.activity ?? "unknown";
|
|
186
|
+
const lifecycle = r.task_state ?? "active";
|
|
187
|
+
const reason = lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
169
188
|
const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
|
|
170
189
|
const filesPart = fmtFiles(r.display_files);
|
|
171
190
|
const lastActivity = fmtLastActivity(r, nowSec);
|
|
172
191
|
const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
|
|
173
|
-
return ` - agent-${r.name ?? "unknown"}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
192
|
+
return ` - agent-${r.name ?? "unknown"}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
174
193
|
}
|
|
175
194
|
function fmtFiles(files) {
|
|
176
195
|
if (files.length === 0)
|
|
@@ -232,8 +232,8 @@ function isFresh(lastHeartbeat) {
|
|
|
232
232
|
* - diff shows non-empty output (genuinely dirty)
|
|
233
233
|
*/
|
|
234
234
|
function isFileCommittedClean(coordRoot, relPath) {
|
|
235
|
-
//
|
|
236
|
-
//
|
|
235
|
+
// Tool payloads can provide either absolute-under-coordRoot or canonical
|
|
236
|
+
// monorepo-relative paths.
|
|
237
237
|
const rel = relPath.startsWith(`${coordRoot}/`) ? relPath.slice(coordRoot.length + 1) : relPath;
|
|
238
238
|
const abs = join(coordRoot, rel);
|
|
239
239
|
if (!existsSync(abs))
|
|
@@ -269,7 +269,7 @@ function pruneClaimFromPeer(coordRoot, instanceId, relPath) {
|
|
|
269
269
|
const body = JSON.parse(readFileSync(path, "utf8"));
|
|
270
270
|
const files = body.files_touched ?? [];
|
|
271
271
|
// Normalize both sides: files_touched can hold absolute-under-coordRoot
|
|
272
|
-
// entries
|
|
272
|
+
// entries as well as canonical relative ones — an
|
|
273
273
|
// exact-string filter silently no-ops on the mixed-form case and the
|
|
274
274
|
// stale claim never heals.
|
|
275
275
|
const norm = (p) => p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pre-commit E-guard verdict. The caller (typically a bash pre-commit hook)
|
|
3
|
-
* sends a JSON request with the staged paths
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* sends a JSON request with the staged paths and prints the verdict
|
|
4
|
+
* messages + exits with verdict.exit_code. Committer identity is resolved
|
|
5
|
+
* HERE via `resolveOwner()` unless the request explicitly carries one.
|
|
6
6
|
*
|
|
7
7
|
* Three outcomes:
|
|
8
8
|
* 1. allow (no conflicts): exit 0.
|
|
@@ -30,9 +30,16 @@ interface Conflict {
|
|
|
30
30
|
short_name: string;
|
|
31
31
|
}
|
|
32
32
|
export interface CommitVerdictRequest {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Committer identity. Optional: when absent (or the legacy
|
|
35
|
+
* `__unattributed__` sentinel), the verdict resolves it via
|
|
36
|
+
* `resolveOwner()` — env → pid-map walk → validated Codex thread id —
|
|
37
|
+
* from its own process context. Callers should stop hand-rolling
|
|
38
|
+
* identity resolution and omit this.
|
|
39
|
+
*/
|
|
40
|
+
instance_id?: string;
|
|
41
|
+
/** Group key. Subagents inherit parent's session_id. Same optionality. */
|
|
42
|
+
session_id?: string;
|
|
36
43
|
/** Canonical monorepo-relative paths. */
|
|
37
44
|
staged_paths: string[];
|
|
38
45
|
/** Paths in `staged_paths` that resolve to submodule gitlinks (mode 160000)
|
|
@@ -45,6 +52,8 @@ export interface CommitVerdictResult {
|
|
|
45
52
|
allow: boolean;
|
|
46
53
|
exit_code: number;
|
|
47
54
|
rule: string;
|
|
55
|
+
/** The committer identity the verdict actually used (resolved or passed). */
|
|
56
|
+
instance_id: string;
|
|
48
57
|
/** Conflict details (for the caller to print). */
|
|
49
58
|
conflicts: Conflict[];
|
|
50
59
|
/** Path-specific gates that fired (for `coord_log` lines). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
1
|
+
{"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAiBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAuHhG"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pre-commit E-guard verdict. The caller (typically a bash pre-commit hook)
|
|
3
|
-
* sends a JSON request with the staged paths
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* sends a JSON request with the staged paths and prints the verdict
|
|
4
|
+
* messages + exits with verdict.exit_code. Committer identity is resolved
|
|
5
|
+
* HERE via `resolveOwner()` unless the request explicitly carries one.
|
|
6
6
|
*
|
|
7
7
|
* Three outcomes:
|
|
8
8
|
* 1. allow (no conflicts): exit 0.
|
|
@@ -23,13 +23,25 @@ import { spawnSync } from "node:child_process";
|
|
|
23
23
|
import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
24
|
import { basename, dirname, join } from "node:path";
|
|
25
25
|
import { coordFreshnessSeconds } from "../../config.js";
|
|
26
|
+
import { resolveOwner } from "../../hooks/resolve/owner.js";
|
|
26
27
|
import { instanceHasLivePid } from "../state/pidmap.js";
|
|
27
28
|
export function evaluateCommit(coordRoot, req) {
|
|
29
|
+
// Resolve the committer here, not in the calling hook. The bash shims used
|
|
30
|
+
// to carry their own pid-map walk (a pre-extraction fossil of resolveOwner);
|
|
31
|
+
// a hand-rolled copy can only fall behind as tiers are added, so the rule
|
|
32
|
+
// resolves identity from its own process context and callers just omit it.
|
|
33
|
+
const passed = req.instance_id?.trim();
|
|
34
|
+
const me = passed && passed !== "__unattributed__"
|
|
35
|
+
? passed
|
|
36
|
+
: (resolveOwner({ payload: null, coordRoot })?.instance_id ?? "__unattributed__");
|
|
37
|
+
const passedSession = req.session_id?.trim();
|
|
38
|
+
const mySession = passed && passedSession ? passedSession : me;
|
|
28
39
|
if (req.staged_paths.length === 0) {
|
|
29
40
|
return {
|
|
30
41
|
allow: true,
|
|
31
42
|
exit_code: 0,
|
|
32
43
|
rule: "commit.pass",
|
|
44
|
+
instance_id: me,
|
|
33
45
|
conflicts: [],
|
|
34
46
|
log_lines: [],
|
|
35
47
|
message: "",
|
|
@@ -41,10 +53,10 @@ export function evaluateCommit(coordRoot, req) {
|
|
|
41
53
|
const gitlinkSet = new Set(req.staged_gitlinks ?? []);
|
|
42
54
|
const conflicts = [];
|
|
43
55
|
for (const peer of peers) {
|
|
44
|
-
if (peer.instance_id ===
|
|
56
|
+
if (peer.instance_id === me)
|
|
45
57
|
continue;
|
|
46
58
|
const peerSession = peer.session_id ?? peer.instance_id;
|
|
47
|
-
if (
|
|
59
|
+
if (mySession !== "__unattributed__" && peerSession === mySession)
|
|
48
60
|
continue; // same group
|
|
49
61
|
const ts = peer.last_heartbeat ? Date.parse(peer.last_heartbeat) : 0;
|
|
50
62
|
if (!Number.isFinite(ts) || ts < cutoffMs)
|
|
@@ -71,6 +83,7 @@ export function evaluateCommit(coordRoot, req) {
|
|
|
71
83
|
allow: true,
|
|
72
84
|
exit_code: 0,
|
|
73
85
|
rule: "commit.pass",
|
|
86
|
+
instance_id: me,
|
|
74
87
|
conflicts: [],
|
|
75
88
|
log_lines: [],
|
|
76
89
|
message: "",
|
|
@@ -81,6 +94,7 @@ export function evaluateCommit(coordRoot, req) {
|
|
|
81
94
|
allow: true,
|
|
82
95
|
exit_code: 0,
|
|
83
96
|
rule: "commit.bypass",
|
|
97
|
+
instance_id: me,
|
|
84
98
|
conflicts,
|
|
85
99
|
log_lines: conflicts.map((c) => `COMMIT_BYPASSED path=${c.staged_path} owner=${c.short_name}`),
|
|
86
100
|
message: "⚠ Multi-Agent coordination: bypass active; staged paths claimed by other agents will be committed anyway:",
|
|
@@ -95,6 +109,7 @@ export function evaluateCommit(coordRoot, req) {
|
|
|
95
109
|
allow: true,
|
|
96
110
|
exit_code: 0,
|
|
97
111
|
rule: "commit.suppressed",
|
|
112
|
+
instance_id: me,
|
|
98
113
|
conflicts,
|
|
99
114
|
log_lines: conflicts.map((c) => `COMMIT_SUPPRESSED path=${c.staged_path} owner=${c.short_name} reason=self_attribution`),
|
|
100
115
|
message: "⚠ Multi-Agent coordination: self-attribution detected; staged paths\n" +
|
|
@@ -108,6 +123,7 @@ export function evaluateCommit(coordRoot, req) {
|
|
|
108
123
|
allow: false,
|
|
109
124
|
exit_code: 1,
|
|
110
125
|
rule: "commit.conflict",
|
|
126
|
+
instance_id: me,
|
|
111
127
|
conflicts,
|
|
112
128
|
log_lines: conflicts.map((c) => `COMMIT_BLOCKED path=${c.staged_path} owner=${c.short_name}`),
|
|
113
129
|
message: "✗ Commit blocked by multi-agent coordination (E guard).\n\n" +
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* stop currently firing carries that field via the in-flight event
|
|
12
12
|
* agent-hook emits before the stop hook runs).
|
|
13
13
|
* Rule 3/3: `state.task_set` event with matching turn boundary exists.
|
|
14
|
+
* Session-naming rule (at most once per session, CC semantics only): the turn
|
|
15
|
+
* whose set-task produced `suggested_session_name` must carry an
|
|
16
|
+
* in-window `turn.stop` with `session_name_present: true`.
|
|
14
17
|
*
|
|
15
18
|
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
16
19
|
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAUH,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;AA4DnE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CA8JvF"}
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* stop currently firing carries that field via the in-flight event
|
|
12
12
|
* agent-hook emits before the stop hook runs).
|
|
13
13
|
* Rule 3/3: `state.task_set` event with matching turn boundary exists.
|
|
14
|
+
* Session-naming rule (at most once per session, CC semantics only): the turn
|
|
15
|
+
* whose set-task produced `suggested_session_name` must carry an
|
|
16
|
+
* in-window `turn.stop` with `session_name_present: true`.
|
|
14
17
|
*
|
|
15
18
|
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
16
19
|
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
*/
|
|
25
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
26
29
|
import { join } from "node:path";
|
|
27
|
-
import { resolveBinName } from "../../config.js";
|
|
30
|
+
import { agentsRequireGitFinalization, endOfTurnStatusCommand, resolveBinName, } from "../../config.js";
|
|
28
31
|
function ackSignalFor(adapter) {
|
|
29
32
|
return adapter === "cursor" ? "status_checked" : "status_box_present";
|
|
30
33
|
}
|
|
@@ -39,17 +42,6 @@ const RECENT_EVENT_WINDOW_LINES = 5_000;
|
|
|
39
42
|
* trailing marker could be truncated away on a long reason.
|
|
40
43
|
*/
|
|
41
44
|
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
45
|
/** How far back the remediation walk may travel. Adapters cap their own
|
|
54
46
|
* followup chains (Cursor's `loop_limit`), and each prompt can appear once per
|
|
55
47
|
* observing adapter, so this only guards a pathological stream. */
|
|
@@ -58,7 +50,7 @@ function isStopRemediationPrompt(event) {
|
|
|
58
50
|
const text = event.data.prompt_text;
|
|
59
51
|
if (typeof text !== "string")
|
|
60
52
|
return false;
|
|
61
|
-
return text.includes(STOP_REMEDIATION_MARKER)
|
|
53
|
+
return text.includes(STOP_REMEDIATION_MARKER);
|
|
62
54
|
}
|
|
63
55
|
/**
|
|
64
56
|
* Start of the window this Stop verdict judges.
|
|
@@ -157,7 +149,7 @@ export function evaluateStopHook(coordRoot, req) {
|
|
|
157
149
|
allow: true,
|
|
158
150
|
exit_code: 0,
|
|
159
151
|
rule: "stop-hook.no_history",
|
|
160
|
-
reason: "no canonical events for this owner;
|
|
152
|
+
reason: "no canonical events for this owner; nothing to evaluate",
|
|
161
153
|
};
|
|
162
154
|
}
|
|
163
155
|
// Turn window: from the most recent user_prompt.submit (this owner) up to
|
|
@@ -176,7 +168,11 @@ export function evaluateStopHook(coordRoot, req) {
|
|
|
176
168
|
return Number.isFinite(t) && t >= startMs && t <= endMs;
|
|
177
169
|
});
|
|
178
170
|
const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
|
|
179
|
-
const
|
|
171
|
+
const statusEvents = inTurn.filter((e) => e.event_type === "state.status_checked");
|
|
172
|
+
const statusChecked = statusEvents.length > 0;
|
|
173
|
+
const requiredStatusChecked = agentsRequireGitFinalization(coordRoot)
|
|
174
|
+
? statusEvents.some((e) => e.data.git_finalization_checked === true)
|
|
175
|
+
: statusChecked;
|
|
180
176
|
const taskSet = inTurn.some((e) => e.event_type === "state.task_set");
|
|
181
177
|
// Rule 2/3: status_box_present on the most recent turn.stop for this owner.
|
|
182
178
|
// The turn.stop event fires from agent-hook stop, which is wired BEFORE
|
|
@@ -189,8 +185,12 @@ export function evaluateStopHook(coordRoot, req) {
|
|
|
189
185
|
// on Claude Code / Codex it's the transcript-scanned `status_box_present`.
|
|
190
186
|
// The matching block helper carries the right "how to fix" message.
|
|
191
187
|
const ackSignal = ackSignalFor(req.adapter);
|
|
192
|
-
const ackPresent = ackSignal === "status_checked"
|
|
193
|
-
|
|
188
|
+
const ackPresent = ackSignal === "status_checked"
|
|
189
|
+
? toolPreUseInTurn
|
|
190
|
+
? requiredStatusChecked
|
|
191
|
+
: statusChecked
|
|
192
|
+
: boxPresent;
|
|
193
|
+
const ackBlock = ackSignal === "status_checked" ? () => rule13Block(coordRoot) : () => rule23Block(coordRoot);
|
|
194
194
|
// Pure-prose-turn exemption: only the ack signal applies. Parity
|
|
195
195
|
// across adapters: CC requires the box; Cursor requires status_checked.
|
|
196
196
|
if (!toolPreUseInTurn) {
|
|
@@ -204,34 +204,70 @@ export function evaluateStopHook(coordRoot, req) {
|
|
|
204
204
|
reason: "no tool calls this turn; rules 1/3 + 3/3 skipped",
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
-
if (!
|
|
208
|
-
return rule13Block();
|
|
207
|
+
if (!requiredStatusChecked)
|
|
208
|
+
return rule13Block(coordRoot);
|
|
209
209
|
// On Cursor `ackPresent === statusChecked` (already true here), so this is a
|
|
210
210
|
// no-op and rule 2/3 is not enforced; on CC/Codex it's the box-paste check.
|
|
211
211
|
if (!ackPresent)
|
|
212
212
|
return ackBlock();
|
|
213
213
|
if (!taskSet)
|
|
214
214
|
return rule33Block();
|
|
215
|
+
// Session-naming rule (at most once per session, evaluated last so
|
|
216
|
+
// remediation stays monotonic in the established order): the turn whose
|
|
217
|
+
// set-task produced the suggested session name must show that name in the
|
|
218
|
+
// reply. Claude Code semantics only — Cursor's Stop payload carries neither
|
|
219
|
+
// a transcript nor the assistant message, so the fence is undetectable there
|
|
220
|
+
// (the same reason rule 2/3 collapses into 1/3 on Cursor), and Codex
|
|
221
|
+
// returned observe-only above. Detection rides `turn.stop`'s
|
|
222
|
+
// `session_name_present` (assistant-text-only transcript scan), mirroring
|
|
223
|
+
// how rule 2/3 rides `status_box_present`.
|
|
224
|
+
if (ackSignal === "status_box_present") {
|
|
225
|
+
const namingEvent = inTurn.find((e) => ((e.event_type === "state.task_set" && e.data.first_of_session === true) ||
|
|
226
|
+
(e.event_type === "state.task_state" && e.data.name_reminted === true)) &&
|
|
227
|
+
typeof e.data.suggested_session_name === "string" &&
|
|
228
|
+
e.data.suggested_session_name.length > 0);
|
|
229
|
+
if (namingEvent) {
|
|
230
|
+
// ANY in-window turn.stop with the flag satisfies the rule, not just the
|
|
231
|
+
// latest: once seen, `session_name_seen_at` is stamped and later
|
|
232
|
+
// turn.stops OMIT the field, so a later block on a different rule (whose
|
|
233
|
+
// repair fires a fresh turn.stop) must not un-satisfy this one.
|
|
234
|
+
const namePresent = inTurn.some((e) => e.event_type === "turn.stop" && e.data.session_name_present === true);
|
|
235
|
+
if (!namePresent) {
|
|
236
|
+
return sessionNameBlock(String(namingEvent.data.suggested_session_name));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
215
240
|
return {
|
|
216
241
|
allow: true,
|
|
217
242
|
exit_code: 0,
|
|
218
243
|
rule: "stop-hook.pass",
|
|
219
244
|
};
|
|
220
245
|
}
|
|
221
|
-
function rule13Block() {
|
|
246
|
+
function rule13Block(coordRoot) {
|
|
247
|
+
const missingEvidence = agentsRequireGitFinalization(coordRoot)
|
|
248
|
+
? "no Git-finalized state.status_checked event found in this turn"
|
|
249
|
+
: "no state.status_checked event found in this turn";
|
|
222
250
|
return {
|
|
223
251
|
allow: false,
|
|
224
252
|
exit_code: 2,
|
|
225
253
|
rule: "stop-hook.rule_1_3",
|
|
226
|
-
reason: `End-of-turn rule (1/3):
|
|
254
|
+
reason: `End-of-turn rule (1/3): ${missingEvidence}; run \`${endOfTurnStatusCommand(coordRoot)}\` as your last tool call.`,
|
|
227
255
|
};
|
|
228
256
|
}
|
|
229
|
-
function rule23Block() {
|
|
257
|
+
function rule23Block(coordRoot) {
|
|
230
258
|
return {
|
|
231
259
|
allow: false,
|
|
232
260
|
exit_code: 2,
|
|
233
261
|
rule: "stop-hook.rule_2_3",
|
|
234
|
-
reason: `End-of-turn rule (2/3): turn.stop did not see the agent-status box in your reply text. Paste the \`${
|
|
262
|
+
reason: `End-of-turn rule (2/3): turn.stop did not see the agent-status box in your reply text. Paste the \`${endOfTurnStatusCommand(coordRoot)}\` output verbatim as a fenced code block (the \`┌─ agent-\` prefix is the detection signal).`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function sessionNameBlock(name) {
|
|
266
|
+
return {
|
|
267
|
+
allow: false,
|
|
268
|
+
exit_code: 2,
|
|
269
|
+
rule: "stop-hook.session_name",
|
|
270
|
+
reason: `Session-naming rule: this turn's set-task produced the session name, but your reply doesn't show it. Reproduce it by itself in a fenced code block so the operator can one-click-copy it as the session/tab title: ${name}`,
|
|
235
271
|
};
|
|
236
272
|
}
|
|
237
273
|
function rule33Block() {
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command/narration event emitter for the coordination layer.
|
|
3
3
|
*
|
|
4
|
-
* `writeSessionEvent` emits command
|
|
4
|
+
* `writeSessionEvent` emits command and narration events straight to the
|
|
5
5
|
* **canonical** `.harnery/events.ndjson` (alongside the hook events), which the
|
|
6
|
-
* `/live` web viewer reads.
|
|
7
|
-
* `newCmdId`, `clampField`, `readLastIntent`) is stable so the session-tee
|
|
8
|
-
* middleware callers need no edits.
|
|
6
|
+
* `/live` web viewer reads.
|
|
9
7
|
*/
|
|
10
|
-
/** Event types accepted by `writeSessionEvent`.
|
|
11
|
-
|
|
12
|
-
* call-site compatibility but are no-ops (the agents CLI emits those itself). */
|
|
13
|
-
export type SessionEventType = "command_start" | "output" | "command_end" | "end_of_turn" | "hook_event" | "set_task" | "file_claim" | "file_release" | "peer_change" | "narration";
|
|
8
|
+
/** Event types accepted by `writeSessionEvent`. */
|
|
9
|
+
export type SessionEventType = "command_start" | "output" | "command_end" | "narration";
|
|
14
10
|
/**
|
|
15
11
|
* Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
|
|
16
12
|
* containerized reader can pick it up through a single bind mount.
|
|
17
13
|
*/
|
|
18
|
-
export declare function
|
|
14
|
+
export declare function canonicalEventsPath(): string;
|
|
19
15
|
/** Random 8-char hex id for grouping output lines under a single command. */
|
|
20
16
|
export declare function newCmdId(): string;
|
|
21
17
|
/**
|
|
@@ -28,14 +24,10 @@ export declare function newCmdId(): string;
|
|
|
28
24
|
*/
|
|
29
25
|
export declare function readLastIntent(instanceId?: string): string | null;
|
|
30
26
|
/**
|
|
31
|
-
* Emit a
|
|
32
|
-
*
|
|
33
|
-
* are accepted for call-site compatibility but are no-ops here (the agents CLI
|
|
34
|
-
* emits those itself). Best-effort, never throws into the caller; a command
|
|
35
|
-
* must never break or slow on telemetry. The `agentName` arg is retained for
|
|
36
|
-
* the stable call signature (canonical events key on instance_id, not name).
|
|
27
|
+
* Emit a command or narration event to `.harnery/events.ndjson`. Best-effort,
|
|
28
|
+
* never throws into the caller; telemetry must not break or slow a command.
|
|
37
29
|
*/
|
|
38
|
-
export declare function writeSessionEvent(type: SessionEventType,
|
|
30
|
+
export declare function writeSessionEvent(type: SessionEventType, fields?: Record<string, unknown>): void;
|
|
39
31
|
/** Trim long values to keep individual events small. */
|
|
40
32
|
export declare function clampField(v: string, max?: number): string;
|
|
41
33
|
//# sourceMappingURL=session-events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-events.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/agents/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,mDAAmD;AACnD,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,QAAQ,GAAG,aAAa,GAAG,WAAW,CAAC;AAExF;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAa5C;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE;AA8FD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,IAAI,CAEN;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,SAAO,GAAG,MAAM,CAGxD"}
|