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
|
@@ -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
|
|
@@ -25,7 +28,11 @@
|
|
|
25
28
|
|
|
26
29
|
import { existsSync, readFileSync } from "node:fs";
|
|
27
30
|
import { join } from "node:path";
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
agentsRequireGitFinalization,
|
|
33
|
+
endOfTurnStatusCommand,
|
|
34
|
+
resolveBinName,
|
|
35
|
+
} from "../../config.ts";
|
|
29
36
|
|
|
30
37
|
export type { VerdictResult } from "./verdict.ts";
|
|
31
38
|
|
|
@@ -106,18 +113,6 @@ const RECENT_EVENT_WINDOW_LINES = 5_000;
|
|
|
106
113
|
*/
|
|
107
114
|
export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
|
|
108
115
|
|
|
109
|
-
/**
|
|
110
|
-
* The pre-marker message shape, still recognized on purpose.
|
|
111
|
-
*
|
|
112
|
-
* A followup already sitting in the stream was written by whichever build was
|
|
113
|
-
* running when it fired. An agent whose session spans an upgrade would otherwise
|
|
114
|
-
* keep alternating until the first new-format followup appeared, which is the
|
|
115
|
-
* exact failure this change removes. Recognizing the older trailing form closes
|
|
116
|
-
* that rollout window at no cost: it only ever appears in a `prompt_text`
|
|
117
|
-
* because Harnery put it there.
|
|
118
|
-
*/
|
|
119
|
-
const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
|
|
120
|
-
|
|
121
116
|
/** How far back the remediation walk may travel. Adapters cap their own
|
|
122
117
|
* followup chains (Cursor's `loop_limit`), and each prompt can appear once per
|
|
123
118
|
* observing adapter, so this only guards a pathological stream. */
|
|
@@ -126,7 +121,7 @@ const MAX_REMEDIATION_WALK_BACK = 16;
|
|
|
126
121
|
function isStopRemediationPrompt(event: CanonicalEvent): boolean {
|
|
127
122
|
const text = event.data.prompt_text;
|
|
128
123
|
if (typeof text !== "string") return false;
|
|
129
|
-
return text.includes(STOP_REMEDIATION_MARKER)
|
|
124
|
+
return text.includes(STOP_REMEDIATION_MARKER);
|
|
130
125
|
}
|
|
131
126
|
|
|
132
127
|
/**
|
|
@@ -233,7 +228,7 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
233
228
|
allow: true,
|
|
234
229
|
exit_code: 0,
|
|
235
230
|
rule: "stop-hook.no_history",
|
|
236
|
-
reason: "no canonical events for this owner;
|
|
231
|
+
reason: "no canonical events for this owner; nothing to evaluate",
|
|
237
232
|
};
|
|
238
233
|
}
|
|
239
234
|
|
|
@@ -255,7 +250,11 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
255
250
|
});
|
|
256
251
|
|
|
257
252
|
const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
|
|
258
|
-
const
|
|
253
|
+
const statusEvents = inTurn.filter((e) => e.event_type === "state.status_checked");
|
|
254
|
+
const statusChecked = statusEvents.length > 0;
|
|
255
|
+
const requiredStatusChecked = agentsRequireGitFinalization(coordRoot)
|
|
256
|
+
? statusEvents.some((e) => e.data.git_finalization_checked === true)
|
|
257
|
+
: statusChecked;
|
|
259
258
|
const taskSet = inTurn.some((e) => e.event_type === "state.task_set");
|
|
260
259
|
|
|
261
260
|
// Rule 2/3: status_box_present on the most recent turn.stop for this owner.
|
|
@@ -270,8 +269,14 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
270
269
|
// on Claude Code / Codex it's the transcript-scanned `status_box_present`.
|
|
271
270
|
// The matching block helper carries the right "how to fix" message.
|
|
272
271
|
const ackSignal = ackSignalFor(req.adapter);
|
|
273
|
-
const ackPresent =
|
|
274
|
-
|
|
272
|
+
const ackPresent =
|
|
273
|
+
ackSignal === "status_checked"
|
|
274
|
+
? toolPreUseInTurn
|
|
275
|
+
? requiredStatusChecked
|
|
276
|
+
: statusChecked
|
|
277
|
+
: boxPresent;
|
|
278
|
+
const ackBlock =
|
|
279
|
+
ackSignal === "status_checked" ? () => rule13Block(coordRoot) : () => rule23Block(coordRoot);
|
|
275
280
|
|
|
276
281
|
// Pure-prose-turn exemption: only the ack signal applies. Parity
|
|
277
282
|
// across adapters: CC requires the box; Cursor requires status_checked.
|
|
@@ -287,12 +292,43 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
287
292
|
};
|
|
288
293
|
}
|
|
289
294
|
|
|
290
|
-
if (!
|
|
295
|
+
if (!requiredStatusChecked) return rule13Block(coordRoot);
|
|
291
296
|
// On Cursor `ackPresent === statusChecked` (already true here), so this is a
|
|
292
297
|
// no-op and rule 2/3 is not enforced; on CC/Codex it's the box-paste check.
|
|
293
298
|
if (!ackPresent) return ackBlock();
|
|
294
299
|
if (!taskSet) return rule33Block();
|
|
295
300
|
|
|
301
|
+
// Session-naming rule (at most once per session, evaluated last so
|
|
302
|
+
// remediation stays monotonic in the established order): the turn whose
|
|
303
|
+
// set-task produced the suggested session name must show that name in the
|
|
304
|
+
// reply. Claude Code semantics only — Cursor's Stop payload carries neither
|
|
305
|
+
// a transcript nor the assistant message, so the fence is undetectable there
|
|
306
|
+
// (the same reason rule 2/3 collapses into 1/3 on Cursor), and Codex
|
|
307
|
+
// returned observe-only above. Detection rides `turn.stop`'s
|
|
308
|
+
// `session_name_present` (assistant-text-only transcript scan), mirroring
|
|
309
|
+
// how rule 2/3 rides `status_box_present`.
|
|
310
|
+
if (ackSignal === "status_box_present") {
|
|
311
|
+
const namingEvent = inTurn.find(
|
|
312
|
+
(e) =>
|
|
313
|
+
((e.event_type === "state.task_set" && e.data.first_of_session === true) ||
|
|
314
|
+
(e.event_type === "state.task_state" && e.data.name_reminted === true)) &&
|
|
315
|
+
typeof e.data.suggested_session_name === "string" &&
|
|
316
|
+
e.data.suggested_session_name.length > 0,
|
|
317
|
+
);
|
|
318
|
+
if (namingEvent) {
|
|
319
|
+
// ANY in-window turn.stop with the flag satisfies the rule, not just the
|
|
320
|
+
// latest: once seen, `session_name_seen_at` is stamped and later
|
|
321
|
+
// turn.stops OMIT the field, so a later block on a different rule (whose
|
|
322
|
+
// repair fires a fresh turn.stop) must not un-satisfy this one.
|
|
323
|
+
const namePresent = inTurn.some(
|
|
324
|
+
(e) => e.event_type === "turn.stop" && e.data.session_name_present === true,
|
|
325
|
+
);
|
|
326
|
+
if (!namePresent) {
|
|
327
|
+
return sessionNameBlock(String(namingEvent.data.suggested_session_name));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
296
332
|
return {
|
|
297
333
|
allow: true,
|
|
298
334
|
exit_code: 0,
|
|
@@ -300,21 +336,33 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
300
336
|
};
|
|
301
337
|
}
|
|
302
338
|
|
|
303
|
-
function rule13Block(): VerdictResult {
|
|
339
|
+
function rule13Block(coordRoot?: string): VerdictResult {
|
|
340
|
+
const missingEvidence = agentsRequireGitFinalization(coordRoot)
|
|
341
|
+
? "no Git-finalized state.status_checked event found in this turn"
|
|
342
|
+
: "no state.status_checked event found in this turn";
|
|
304
343
|
return {
|
|
305
344
|
allow: false,
|
|
306
345
|
exit_code: 2,
|
|
307
346
|
rule: "stop-hook.rule_1_3",
|
|
308
|
-
reason: `End-of-turn rule (1/3):
|
|
347
|
+
reason: `End-of-turn rule (1/3): ${missingEvidence}; run \`${endOfTurnStatusCommand(coordRoot)}\` as your last tool call.`,
|
|
309
348
|
};
|
|
310
349
|
}
|
|
311
350
|
|
|
312
|
-
function rule23Block(): VerdictResult {
|
|
351
|
+
function rule23Block(coordRoot?: string): VerdictResult {
|
|
313
352
|
return {
|
|
314
353
|
allow: false,
|
|
315
354
|
exit_code: 2,
|
|
316
355
|
rule: "stop-hook.rule_2_3",
|
|
317
|
-
reason: `End-of-turn rule (2/3): turn.stop did not see the agent-status box in your reply text. Paste the \`${
|
|
356
|
+
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).`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function sessionNameBlock(name: string): VerdictResult {
|
|
361
|
+
return {
|
|
362
|
+
allow: false,
|
|
363
|
+
exit_code: 2,
|
|
364
|
+
rule: "stop-hook.session_name",
|
|
365
|
+
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}`,
|
|
318
366
|
};
|
|
319
367
|
}
|
|
320
368
|
|
|
@@ -1,11 +1,9 @@
|
|
|
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
8
|
|
|
11
9
|
import { randomBytes } from "node:crypto";
|
|
@@ -15,28 +13,16 @@ import { dirname, resolve } from "node:path";
|
|
|
15
13
|
import { normalizeAdapter, resolveEmitRoot } from "./canonical-emit.ts";
|
|
16
14
|
import { emit } from "./events/emit.ts";
|
|
17
15
|
|
|
18
|
-
/** Event types accepted by `writeSessionEvent`.
|
|
19
|
-
|
|
20
|
-
* call-site compatibility but are no-ops (the agents CLI emits those itself). */
|
|
21
|
-
export type SessionEventType =
|
|
22
|
-
| "command_start"
|
|
23
|
-
| "output"
|
|
24
|
-
| "command_end"
|
|
25
|
-
| "end_of_turn"
|
|
26
|
-
| "hook_event"
|
|
27
|
-
| "set_task"
|
|
28
|
-
| "file_claim"
|
|
29
|
-
| "file_release"
|
|
30
|
-
| "peer_change"
|
|
31
|
-
| "narration";
|
|
16
|
+
/** Event types accepted by `writeSessionEvent`. */
|
|
17
|
+
export type SessionEventType = "command_start" | "output" | "command_end" | "narration";
|
|
32
18
|
|
|
33
19
|
/**
|
|
34
20
|
* Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
|
|
35
21
|
* containerized reader can pick it up through a single bind mount.
|
|
36
22
|
*/
|
|
37
|
-
export function
|
|
23
|
+
export function canonicalEventsPath(): string {
|
|
38
24
|
// Explicit override (tests + non-monorepo invocations).
|
|
39
|
-
const explicit = process.env.
|
|
25
|
+
const explicit = process.env.HARNERY_EVENTS_PATH;
|
|
40
26
|
if (explicit) return explicit;
|
|
41
27
|
// Superproject-aware root resolution (git first, cwd walk fallback) via
|
|
42
28
|
// resolveEmitRoot. A plain cwd walk here mis-anchored to a NESTED
|
|
@@ -64,7 +50,7 @@ export function newCmdId(): string {
|
|
|
64
50
|
*/
|
|
65
51
|
export function readLastIntent(instanceId?: string): string | null {
|
|
66
52
|
if (!instanceId) return null;
|
|
67
|
-
// Same superproject-aware root resolution as
|
|
53
|
+
// Same superproject-aware root resolution as canonicalEventsPath(): the
|
|
68
54
|
// intent stamp is written by the PreToolUse hook into the SUPERPROJECT's
|
|
69
55
|
// .harnery/, so a nested-`.harnery/` cwd must not redirect the read.
|
|
70
56
|
const root = resolveEmitRoot();
|
|
@@ -81,15 +67,7 @@ export function readLastIntent(instanceId?: string): string | null {
|
|
|
81
67
|
}
|
|
82
68
|
}
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
* Dual-write: mirror command/narration session-events
|
|
86
|
-
* into the canonical `.harnery/events.ndjson` stream so the legacy
|
|
87
|
-
* session-events.ndjson writer + its web consumers can be retired.
|
|
88
|
-
* Only the command-stream + narration types migrate; the coord/state types
|
|
89
|
-
* (`set_task`, `file_claim`, `peer_change`, …) are already emitted canonically
|
|
90
|
-
* by the agents CLI, so re-emitting them here would double-count.
|
|
91
|
-
*/
|
|
92
|
-
const CANONICAL_TYPE: Partial<Record<SessionEventType, string>> = {
|
|
70
|
+
const CANONICAL_TYPE: Record<SessionEventType, string> = {
|
|
93
71
|
command_start: "command.start",
|
|
94
72
|
output: "command.output",
|
|
95
73
|
command_end: "command.end",
|
|
@@ -131,7 +109,7 @@ function enrichFromHeartbeat(coordRoot: string, instanceId: string): HeartbeatEn
|
|
|
131
109
|
}
|
|
132
110
|
}
|
|
133
111
|
|
|
134
|
-
/** Project the flat
|
|
112
|
+
/** Project the flat middleware `fields` into the canonical event's `data` shape.
|
|
135
113
|
* Unknown types never reach here, guarded by CANONICAL_TYPE. */
|
|
136
114
|
function canonicalData(
|
|
137
115
|
type: SessionEventType,
|
|
@@ -158,15 +136,15 @@ function canonicalData(
|
|
|
158
136
|
|
|
159
137
|
/** Emit a command/narration event to the canonical stream. Swallows every
|
|
160
138
|
* error and skips when identity can't be resolved: telemetry must never break
|
|
161
|
-
*
|
|
139
|
+
* or slow down a command. */
|
|
162
140
|
function emitCanonicalCommand(type: SessionEventType, fields: Record<string, unknown>): void {
|
|
163
141
|
const eventType = CANONICAL_TYPE[type];
|
|
164
142
|
if (!eventType) return;
|
|
165
143
|
const instanceId = typeof fields.instance_id === "string" ? fields.instance_id : undefined;
|
|
166
144
|
if (!instanceId) return;
|
|
167
145
|
try {
|
|
168
|
-
// coordRoot = the dir containing `.harnery/`;
|
|
169
|
-
const coordRoot = dirname(dirname(
|
|
146
|
+
// coordRoot = the dir containing `.harnery/`; canonicalEventsPath() anchors it.
|
|
147
|
+
const coordRoot = dirname(dirname(canonicalEventsPath()));
|
|
170
148
|
const enrich = enrichFromHeartbeat(coordRoot, instanceId);
|
|
171
149
|
if (!enrich) return;
|
|
172
150
|
emit(coordRoot, {
|
|
@@ -182,16 +160,11 @@ function emitCanonicalCommand(type: SessionEventType, fields: Record<string, unk
|
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
/**
|
|
185
|
-
* Emit a
|
|
186
|
-
*
|
|
187
|
-
* are accepted for call-site compatibility but are no-ops here (the agents CLI
|
|
188
|
-
* emits those itself). Best-effort, never throws into the caller; a command
|
|
189
|
-
* must never break or slow on telemetry. The `agentName` arg is retained for
|
|
190
|
-
* the stable call signature (canonical events key on instance_id, not name).
|
|
163
|
+
* Emit a command or narration event to `.harnery/events.ndjson`. Best-effort,
|
|
164
|
+
* never throws into the caller; telemetry must not break or slow a command.
|
|
191
165
|
*/
|
|
192
166
|
export function writeSessionEvent(
|
|
193
167
|
type: SessionEventType,
|
|
194
|
-
_agentName: string,
|
|
195
168
|
fields: Record<string, unknown> = {},
|
|
196
169
|
): void {
|
|
197
170
|
emitCanonicalCommand(type, fields);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import type { CanonicalEvent } from "../events/consume.ts";
|
|
15
|
+
import { type AgentActivity, applySessionStateEvent, type TaskState } from "./session-state.ts";
|
|
15
16
|
|
|
16
17
|
export interface V2Heartbeat {
|
|
17
18
|
instance_id: string;
|
|
@@ -21,6 +22,10 @@ export interface V2Heartbeat {
|
|
|
21
22
|
name?: string;
|
|
22
23
|
kind?: "session" | "subagent" | "transient";
|
|
23
24
|
model?: string;
|
|
25
|
+
instruction_bundle_id?: string;
|
|
26
|
+
instruction_source_id?: string;
|
|
27
|
+
instruction_profile_root?: string;
|
|
28
|
+
instruction_component_count?: number;
|
|
24
29
|
platform?: string;
|
|
25
30
|
subagent_call_id?: string;
|
|
26
31
|
parent_session_id?: string;
|
|
@@ -34,6 +39,15 @@ export interface V2Heartbeat {
|
|
|
34
39
|
last_tool_at?: string;
|
|
35
40
|
task?: string;
|
|
36
41
|
task_updated_at?: string;
|
|
42
|
+
activity?: AgentActivity;
|
|
43
|
+
activity_updated_at?: string;
|
|
44
|
+
activity_source?: string;
|
|
45
|
+
task_state?: TaskState;
|
|
46
|
+
task_state_updated_at?: string;
|
|
47
|
+
task_state_reason?: string;
|
|
48
|
+
suggested_session_name?: string;
|
|
49
|
+
session_name_seen_at?: string;
|
|
50
|
+
session_name_seen_for?: string;
|
|
37
51
|
last_status_at?: string;
|
|
38
52
|
presence?: "mobile" | "office";
|
|
39
53
|
last_intent?: string;
|
|
@@ -177,6 +191,14 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
177
191
|
hb.v2_meta.last_projected = new Date().toISOString();
|
|
178
192
|
if (ev.turn_id) hb.last_turn_id = ev.turn_id;
|
|
179
193
|
|
|
194
|
+
const sessionState = applySessionStateEvent(hb, ev);
|
|
195
|
+
hb.activity = sessionState.activity;
|
|
196
|
+
hb.activity_updated_at = sessionState.activity_updated_at;
|
|
197
|
+
hb.activity_source = sessionState.activity_source;
|
|
198
|
+
hb.task_state = sessionState.task_state;
|
|
199
|
+
hb.task_state_updated_at = sessionState.task_state_updated_at;
|
|
200
|
+
hb.task_state_reason = sessionState.task_state_reason;
|
|
201
|
+
|
|
180
202
|
const d = ev.data;
|
|
181
203
|
switch (ev.event_type) {
|
|
182
204
|
case "session.start":
|
|
@@ -185,6 +207,16 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
185
207
|
{
|
|
186
208
|
const model = pickStr(d, "model");
|
|
187
209
|
if (model) hb.model = model;
|
|
210
|
+
const instructionBundleId = pickStr(d, "instruction_bundle_id");
|
|
211
|
+
if (instructionBundleId) hb.instruction_bundle_id = instructionBundleId;
|
|
212
|
+
const instructionSourceId = pickStr(d, "instruction_source_id");
|
|
213
|
+
if (instructionSourceId) hb.instruction_source_id = instructionSourceId;
|
|
214
|
+
const instructionProfileRoot = pickStr(d, "instruction_profile_root");
|
|
215
|
+
if (instructionProfileRoot) hb.instruction_profile_root = instructionProfileRoot;
|
|
216
|
+
const instructionComponentCount = pickNum(d, "instruction_component_count");
|
|
217
|
+
if (instructionComponentCount !== undefined) {
|
|
218
|
+
hb.instruction_component_count = instructionComponentCount;
|
|
219
|
+
}
|
|
188
220
|
const platform = pickStr(d, "platform") ?? adapterToPlatform(ev.adapter);
|
|
189
221
|
hb.platform = platform;
|
|
190
222
|
const name = pickStr(d, "name");
|
|
@@ -245,6 +277,18 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
245
277
|
case "turn.stop":
|
|
246
278
|
hb.last_turn_stop_at = ev.ts;
|
|
247
279
|
hb.last_turn_status_box_present = pickBool(d, "status_box_present");
|
|
280
|
+
// Rebuild fidelity for the naming ritual: the live stamp is written by
|
|
281
|
+
// the Stop hook (stampSessionNameSeen); reproduce it from the event so a
|
|
282
|
+
// full projector rebuild doesn't re-open a satisfied naming window.
|
|
283
|
+
if (d.session_name_present === true) {
|
|
284
|
+
hb.session_name_seen_at ??= ev.ts;
|
|
285
|
+
// Attribute the sighting to the name the scan actually covered. Reading
|
|
286
|
+
// the name current at this point in the replay instead re-attributed an
|
|
287
|
+
// old sighting to a re-minted name, which faked "already seen" for a
|
|
288
|
+
// name no reply had shown. An event without the field predates it, so
|
|
289
|
+
// leave the attribution unset and let the next stop re-scan.
|
|
290
|
+
hb.session_name_seen_for = pickStr(d, "session_name_present_for");
|
|
291
|
+
}
|
|
248
292
|
{
|
|
249
293
|
const summary = pickStr(d, "turn_summary");
|
|
250
294
|
if (summary) {
|
|
@@ -301,6 +345,26 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
301
345
|
hb.task = task;
|
|
302
346
|
}
|
|
303
347
|
hb.task_updated_at = ev.ts;
|
|
348
|
+
// Rebuild fidelity: the naming call carries the name it produced.
|
|
349
|
+
const suggested = pickStr(d, "suggested_session_name");
|
|
350
|
+
if (suggested && !hb.suggested_session_name) {
|
|
351
|
+
hb.suggested_session_name = suggested;
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case "state.task_state": {
|
|
357
|
+
const suggested = pickStr(d, "suggested_session_name");
|
|
358
|
+
if (suggested && suggested !== hb.suggested_session_name) {
|
|
359
|
+
// Older heartbeats predate session_name_seen_for. A lifecycle re-mint
|
|
360
|
+
// still needs a mismatch the prompt and Stop paths can enforce, so
|
|
361
|
+
// attribute the prior title before replacing it. Ordinary legacy
|
|
362
|
+
// heartbeats remain quiet until a real lifecycle event changes them.
|
|
363
|
+
if (!hb.session_name_seen_for && hb.suggested_session_name) {
|
|
364
|
+
hb.session_name_seen_for = hb.suggested_session_name;
|
|
365
|
+
}
|
|
366
|
+
hb.suggested_session_name = suggested;
|
|
367
|
+
}
|
|
304
368
|
break;
|
|
305
369
|
}
|
|
306
370
|
|
|
@@ -322,6 +386,19 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
322
386
|
break;
|
|
323
387
|
}
|
|
324
388
|
|
|
389
|
+
case "claim.acquire": {
|
|
390
|
+
const path = pickStr(d, "path");
|
|
391
|
+
const mode = pickStr(d, "mode");
|
|
392
|
+
if (path && mode === "write") {
|
|
393
|
+
const canonical = path.startsWith(`${coordRoot}/`)
|
|
394
|
+
? path.slice(coordRoot.length + 1)
|
|
395
|
+
: path;
|
|
396
|
+
if (!hb.files_touched) hb.files_touched = [];
|
|
397
|
+
if (!hb.files_touched.includes(canonical)) hb.files_touched.push(canonical);
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
325
402
|
case "claim.release": {
|
|
326
403
|
const path = pickStr(d, "path");
|
|
327
404
|
if (path && hb.files_touched) {
|
|
@@ -341,7 +418,7 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
341
418
|
}
|
|
342
419
|
|
|
343
420
|
function adapterToPlatform(adapter: string): string {
|
|
344
|
-
if (adapter === "claude-code") return "
|
|
421
|
+
if (adapter === "claude-code") return "claude-code";
|
|
345
422
|
if (adapter === "cursor") return "cursor";
|
|
346
423
|
if (adapter === "codex") return "codex";
|
|
347
424
|
return adapter;
|
|
@@ -480,6 +557,10 @@ function writeHeartbeat(coordRoot: string, instanceId: string, hb: V2Heartbeat):
|
|
|
480
557
|
setIfDefined(merged, "workflow_run_id", hb.workflow_run_id);
|
|
481
558
|
setIfDefined(merged, "workflow_agent_id", hb.workflow_agent_id);
|
|
482
559
|
setIfDefined(merged, "model", hb.model);
|
|
560
|
+
setIfDefined(merged, "instruction_bundle_id", hb.instruction_bundle_id);
|
|
561
|
+
setIfDefined(merged, "instruction_source_id", hb.instruction_source_id);
|
|
562
|
+
setIfDefined(merged, "instruction_profile_root", hb.instruction_profile_root);
|
|
563
|
+
setIfDefined(merged, "instruction_component_count", hb.instruction_component_count);
|
|
483
564
|
setIfDefined(merged, "platform", hb.platform);
|
|
484
565
|
setIfDefined(merged, "started_at", hb.started_at);
|
|
485
566
|
// files_touched is a required-array invariant for every reader
|
|
@@ -493,6 +574,27 @@ function writeHeartbeat(coordRoot: string, instanceId: string, hb: V2Heartbeat):
|
|
|
493
574
|
setIfDefined(merged, "last_tool_at", hb.last_tool_at);
|
|
494
575
|
setIfDefined(merged, "task", hb.task);
|
|
495
576
|
setIfDefined(merged, "task_updated_at", hb.task_updated_at);
|
|
577
|
+
setIfDefined(merged, "activity", hb.activity);
|
|
578
|
+
setIfDefined(merged, "activity_updated_at", hb.activity_updated_at);
|
|
579
|
+
setIfDefined(merged, "activity_source", hb.activity_source);
|
|
580
|
+
setIfDefined(merged, "task_state", hb.task_state);
|
|
581
|
+
setIfDefined(merged, "task_state_updated_at", hb.task_state_updated_at);
|
|
582
|
+
if (hb.task_state_reason) {
|
|
583
|
+
merged.task_state_reason = hb.task_state_reason;
|
|
584
|
+
} else if (hb.task_state) {
|
|
585
|
+
delete merged.task_state_reason;
|
|
586
|
+
}
|
|
587
|
+
setIfDefined(merged, "suggested_session_name", hb.suggested_session_name);
|
|
588
|
+
setIfDefined(merged, "session_name_seen_at", hb.session_name_seen_at);
|
|
589
|
+
if (hb.session_name_seen_for) {
|
|
590
|
+
merged.session_name_seen_for = hb.session_name_seen_for;
|
|
591
|
+
} else {
|
|
592
|
+
// Unlike most projected fields, an absent attribution is meaningful: a
|
|
593
|
+
// legacy sighting cannot prove which suggested name it covered. Remove a
|
|
594
|
+
// stale value inherited through the additive merge so the next stop
|
|
595
|
+
// scans the current name instead of treating it as already satisfied.
|
|
596
|
+
delete merged.session_name_seen_for;
|
|
597
|
+
}
|
|
496
598
|
setIfDefined(merged, "last_status_at", hb.last_status_at);
|
|
497
599
|
setIfDefined(merged, "turn_summary", hb.turn_summary);
|
|
498
600
|
setIfDefined(merged, "turn_summary_updated_at", hb.turn_summary_updated_at);
|
|
@@ -536,3 +638,8 @@ function pickBool(o: Record<string, unknown>, k: string): boolean | undefined {
|
|
|
536
638
|
const v = o[k];
|
|
537
639
|
return typeof v === "boolean" ? v : undefined;
|
|
538
640
|
}
|
|
641
|
+
|
|
642
|
+
function pickNum(o: Record<string, unknown>, k: string): number | undefined {
|
|
643
|
+
const v = o[k];
|
|
644
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
645
|
+
}
|