harnery 0.31.4 → 0.31.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agents/rules/stop-hook.d.ts +16 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +88 -6
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +17 -2
- package/package.json +1 -1
- package/src/core/agents/rules/stop-hook.ts +95 -6
- package/src/core/hooks/adapter/output.ts +17 -4
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
16
16
|
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
17
17
|
* as a user-visible mobile cue.
|
|
18
|
+
*
|
|
19
|
+
* Remediation continuity: an adapter whose Stop channel re-prompts by
|
|
20
|
+
* submitting a new user message (Cursor's `followup_message`) opens a fresh
|
|
21
|
+
* turn to repair the previous one. Such a turn inherits the window of the turn
|
|
22
|
+
* it repairs, so ritual events accumulate instead of alternating. See
|
|
23
|
+
* `resolveTurnStartMs`.
|
|
18
24
|
*/
|
|
19
25
|
export type { VerdictResult } from "./verdict.js";
|
|
20
26
|
import type { VerdictResult } from "./verdict.js";
|
|
@@ -43,6 +49,16 @@ export interface StopHookRequest {
|
|
|
43
49
|
* event capture on: the child stays visible to peers and the coord layer. */
|
|
44
50
|
workflow_child?: boolean;
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Machine marker that opens a Stop-remediation message. Written by
|
|
54
|
+
* `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
|
|
55
|
+
* message, and read back here off `user_prompt.submit.data.prompt_text` to
|
|
56
|
+
* recognize a turn Harnery itself opened.
|
|
57
|
+
*
|
|
58
|
+
* Leading position is deliberate: `prompt_text` is clamped when recorded, so a
|
|
59
|
+
* trailing marker could be truncated away on a long reason.
|
|
60
|
+
*/
|
|
61
|
+
export declare const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
|
|
46
62
|
/**
|
|
47
63
|
* Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
|
|
48
64
|
* when all three pass). Soft-fails open when the event stream isn't
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAwEnE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CAqHvF"}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
16
16
|
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
17
17
|
* as a user-visible mobile cue.
|
|
18
|
+
*
|
|
19
|
+
* Remediation continuity: an adapter whose Stop channel re-prompts by
|
|
20
|
+
* submitting a new user message (Cursor's `followup_message`) opens a fresh
|
|
21
|
+
* turn to repair the previous one. Such a turn inherits the window of the turn
|
|
22
|
+
* it repairs, so ritual events accumulate instead of alternating. See
|
|
23
|
+
* `resolveTurnStartMs`.
|
|
18
24
|
*/
|
|
19
25
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
26
|
import { join } from "node:path";
|
|
@@ -23,6 +29,80 @@ function ackSignalFor(adapter) {
|
|
|
23
29
|
return adapter === "cursor" ? "status_checked" : "status_box_present";
|
|
24
30
|
}
|
|
25
31
|
const RECENT_EVENT_WINDOW_LINES = 5_000;
|
|
32
|
+
/**
|
|
33
|
+
* Machine marker that opens a Stop-remediation message. Written by
|
|
34
|
+
* `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
|
|
35
|
+
* message, and read back here off `user_prompt.submit.data.prompt_text` to
|
|
36
|
+
* recognize a turn Harnery itself opened.
|
|
37
|
+
*
|
|
38
|
+
* Leading position is deliberate: `prompt_text` is clamped when recorded, so a
|
|
39
|
+
* trailing marker could be truncated away on a long reason.
|
|
40
|
+
*/
|
|
41
|
+
export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
|
|
42
|
+
/**
|
|
43
|
+
* The pre-marker message shape, still recognized on purpose.
|
|
44
|
+
*
|
|
45
|
+
* A followup already sitting in the stream was written by whichever build was
|
|
46
|
+
* running when it fired. An agent whose session spans an upgrade would otherwise
|
|
47
|
+
* keep alternating until the first new-format followup appeared, which is the
|
|
48
|
+
* exact failure this change removes. Recognizing the older trailing form closes
|
|
49
|
+
* that rollout window at no cost: it only ever appears in a `prompt_text`
|
|
50
|
+
* because Harnery put it there.
|
|
51
|
+
*/
|
|
52
|
+
const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
|
|
53
|
+
/** How far back the remediation walk may travel. Adapters cap their own
|
|
54
|
+
* followup chains (Cursor's `loop_limit`), and each prompt can appear once per
|
|
55
|
+
* observing adapter, so this only guards a pathological stream. */
|
|
56
|
+
const MAX_REMEDIATION_WALK_BACK = 16;
|
|
57
|
+
function isStopRemediationPrompt(event) {
|
|
58
|
+
const text = event.data.prompt_text;
|
|
59
|
+
if (typeof text !== "string")
|
|
60
|
+
return false;
|
|
61
|
+
return text.includes(STOP_REMEDIATION_MARKER) || text.includes(STOP_REMEDIATION_LEGACY_MARKER);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Start of the window this Stop verdict judges.
|
|
65
|
+
*
|
|
66
|
+
* Normally the most recent `user_prompt.submit`. The exception is the
|
|
67
|
+
* remediation chain: Cursor does not continue a turn on Stop, it auto-submits
|
|
68
|
+
* our message as a new user turn (`followup_message`). Scoping the verdict to
|
|
69
|
+
* that new turn alone makes the ritual unsatisfiable in one pass, because
|
|
70
|
+
* remediation itself runs a tool, so the repair turn needs every rule again
|
|
71
|
+
* while the signal from the previous turn sits just outside the window. Running
|
|
72
|
+
* `set-task` then clears rule 3/3 and fails rule 1/3, running `status` clears
|
|
73
|
+
* 1/3 and fails 3/3, and the pair alternates until the adapter's loop cap.
|
|
74
|
+
*
|
|
75
|
+
* Anchoring the window at the last prompt Harnery did not author makes progress
|
|
76
|
+
* monotonic: each repair adds a signal and none are lost, so the chain ends.
|
|
77
|
+
* A genuine human prompt always anchors a fresh window, so this cannot be used
|
|
78
|
+
* to inherit ritual credit across real turns.
|
|
79
|
+
*/
|
|
80
|
+
function resolveTurnStartMs(ownerEvents, nowMs, fallbackMs) {
|
|
81
|
+
// Honor the cutoff: `now_ms` marks what is relevant yet, so a prompt after it
|
|
82
|
+
// cannot anchor this turn. In production the cutoff is the clock and nothing
|
|
83
|
+
// follows it, but a replay over recorded history passes a past cutoff, and an
|
|
84
|
+
// unclamped search would anchor on a later turn and hand back an empty window.
|
|
85
|
+
const prompts = ownerEvents.filter((e) => {
|
|
86
|
+
if (e.event_type !== "user_prompt.submit")
|
|
87
|
+
return false;
|
|
88
|
+
const t = Date.parse(e.ts);
|
|
89
|
+
return Number.isFinite(t) && t <= nowMs;
|
|
90
|
+
});
|
|
91
|
+
if (prompts.length === 0)
|
|
92
|
+
return fallbackMs;
|
|
93
|
+
let index = prompts.length - 1;
|
|
94
|
+
let hops = 0;
|
|
95
|
+
while (index > 0 && hops < MAX_REMEDIATION_WALK_BACK) {
|
|
96
|
+
const prompt = prompts[index];
|
|
97
|
+
if (!prompt || !isStopRemediationPrompt(prompt))
|
|
98
|
+
break;
|
|
99
|
+
index -= 1;
|
|
100
|
+
hops += 1;
|
|
101
|
+
}
|
|
102
|
+
const anchor = prompts[index];
|
|
103
|
+
const parsed = anchor ? Date.parse(anchor.ts) : Number.NaN;
|
|
104
|
+
return Number.isFinite(parsed) ? parsed : fallbackMs;
|
|
105
|
+
}
|
|
26
106
|
/**
|
|
27
107
|
* Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
|
|
28
108
|
* when all three pass). Soft-fails open when the event stream isn't
|
|
@@ -81,17 +161,19 @@ export function evaluateStopHook(coordRoot, req) {
|
|
|
81
161
|
};
|
|
82
162
|
}
|
|
83
163
|
// Turn window: from the most recent user_prompt.submit (this owner) up to
|
|
84
|
-
// either the explicit now_ms or the last event we see
|
|
164
|
+
// either the explicit now_ms or the last event we see, walking back over any
|
|
165
|
+
// Harnery-authored Stop-remediation prompts so a repair turn is judged
|
|
166
|
+
// together with the turn it repairs (see `resolveTurnStartMs`). Fall back to a
|
|
85
167
|
// 5-minute window when no user_prompt.submit is in scope (fresh wiring,
|
|
86
168
|
// out-of-window prompt, etc.) so a stale stream doesn't silently pass.
|
|
87
169
|
const nowMs = req.now_ms ?? Date.now();
|
|
88
|
-
const
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
const
|
|
170
|
+
const startMs = req.turn_window
|
|
171
|
+
? req.turn_window.start_ms
|
|
172
|
+
: resolveTurnStartMs(ownerEvents, nowMs, nowMs - 5 * 60 * 1000);
|
|
173
|
+
const endMs = req.turn_window ? req.turn_window.end_ms : nowMs;
|
|
92
174
|
const inTurn = ownerEvents.filter((e) => {
|
|
93
175
|
const t = Date.parse(e.ts);
|
|
94
|
-
return Number.isFinite(t) && t >= startMs && t <=
|
|
176
|
+
return Number.isFinite(t) && t >= startMs && t <= endMs;
|
|
95
177
|
});
|
|
96
178
|
const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
|
|
97
179
|
const statusChecked = inTurn.some((e) => e.event_type === "state.status_checked");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAEhF,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAI/D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,GAAG,CAAC,CAuBjG"}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* Every helper writes to process.stdout + newline-terminates so callers can
|
|
18
18
|
* fire-and-forget. Empty text → no-op (no JSON written).
|
|
19
19
|
*/
|
|
20
|
+
import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.js";
|
|
21
|
+
import { resolveBinName } from "../../config.js";
|
|
20
22
|
/** Emit a context-injection (peer table, wiring check, council pending, …). */
|
|
21
23
|
export function emitContext(adapter, event, text) {
|
|
22
24
|
if (!text || text.length === 0)
|
|
@@ -56,12 +58,25 @@ export function emitDeny(adapter, reason) {
|
|
|
56
58
|
export function emitStopBlock(adapter, verdict) {
|
|
57
59
|
if (adapter === "codex")
|
|
58
60
|
return 0;
|
|
59
|
-
const
|
|
61
|
+
const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
|
|
60
62
|
if (adapter === "cursor") {
|
|
63
|
+
// The marker leads the message so the Stop verdict can recognize the turn
|
|
64
|
+
// Cursor opens from it (`STOP_REMEDIATION_MARKER`), and so the operator can
|
|
65
|
+
// see in chat that the message came from Harnery rather than from them.
|
|
66
|
+
// Both commands are named because one message that repairs the whole ritual
|
|
67
|
+
// ends the chain in a single followup; the failing rule is context.
|
|
68
|
+
const bin = resolveBinName();
|
|
69
|
+
const message = [
|
|
70
|
+
`${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
|
|
71
|
+
reason,
|
|
72
|
+
`Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${bin} agents status\` as your last tool call.`,
|
|
73
|
+
].join("\n");
|
|
61
74
|
process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
|
|
62
75
|
return 0;
|
|
63
76
|
}
|
|
64
|
-
|
|
77
|
+
// Claude Code continues the SAME turn on exit 2, so the single failing rule is
|
|
78
|
+
// both accurate and sufficient there. Left unchanged deliberately.
|
|
79
|
+
process.stderr.write(`${reason}\n[agent-hook stop]: rule=${verdict.rule}\n`);
|
|
65
80
|
return 2;
|
|
66
81
|
}
|
|
67
82
|
function buildContextJson(adapter, event, text) {
|
package/package.json
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
16
16
|
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
17
17
|
* as a user-visible mobile cue.
|
|
18
|
+
*
|
|
19
|
+
* Remediation continuity: an adapter whose Stop channel re-prompts by
|
|
20
|
+
* submitting a new user message (Cursor's `followup_message`) opens a fresh
|
|
21
|
+
* turn to repair the previous one. Such a turn inherits the window of the turn
|
|
22
|
+
* it repairs, so ritual events accumulate instead of alternating. See
|
|
23
|
+
* `resolveTurnStartMs`.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -89,6 +95,87 @@ function ackSignalFor(adapter?: string): AckSignal {
|
|
|
89
95
|
|
|
90
96
|
const RECENT_EVENT_WINDOW_LINES = 5_000;
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Machine marker that opens a Stop-remediation message. Written by
|
|
100
|
+
* `hooks/adapter/output.ts` on the adapters whose Stop channel is a new user
|
|
101
|
+
* message, and read back here off `user_prompt.submit.data.prompt_text` to
|
|
102
|
+
* recognize a turn Harnery itself opened.
|
|
103
|
+
*
|
|
104
|
+
* Leading position is deliberate: `prompt_text` is clamped when recorded, so a
|
|
105
|
+
* trailing marker could be truncated away on a long reason.
|
|
106
|
+
*/
|
|
107
|
+
export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The pre-marker message shape, still recognized on purpose.
|
|
111
|
+
*
|
|
112
|
+
* A followup already sitting in the stream was written by whichever build was
|
|
113
|
+
* running when it fired. An agent whose session spans an upgrade would otherwise
|
|
114
|
+
* keep alternating until the first new-format followup appeared, which is the
|
|
115
|
+
* exact failure this change removes. Recognizing the older trailing form closes
|
|
116
|
+
* that rollout window at no cost: it only ever appears in a `prompt_text`
|
|
117
|
+
* because Harnery put it there.
|
|
118
|
+
*/
|
|
119
|
+
const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
|
|
120
|
+
|
|
121
|
+
/** How far back the remediation walk may travel. Adapters cap their own
|
|
122
|
+
* followup chains (Cursor's `loop_limit`), and each prompt can appear once per
|
|
123
|
+
* observing adapter, so this only guards a pathological stream. */
|
|
124
|
+
const MAX_REMEDIATION_WALK_BACK = 16;
|
|
125
|
+
|
|
126
|
+
function isStopRemediationPrompt(event: CanonicalEvent): boolean {
|
|
127
|
+
const text = event.data.prompt_text;
|
|
128
|
+
if (typeof text !== "string") return false;
|
|
129
|
+
return text.includes(STOP_REMEDIATION_MARKER) || text.includes(STOP_REMEDIATION_LEGACY_MARKER);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start of the window this Stop verdict judges.
|
|
134
|
+
*
|
|
135
|
+
* Normally the most recent `user_prompt.submit`. The exception is the
|
|
136
|
+
* remediation chain: Cursor does not continue a turn on Stop, it auto-submits
|
|
137
|
+
* our message as a new user turn (`followup_message`). Scoping the verdict to
|
|
138
|
+
* that new turn alone makes the ritual unsatisfiable in one pass, because
|
|
139
|
+
* remediation itself runs a tool, so the repair turn needs every rule again
|
|
140
|
+
* while the signal from the previous turn sits just outside the window. Running
|
|
141
|
+
* `set-task` then clears rule 3/3 and fails rule 1/3, running `status` clears
|
|
142
|
+
* 1/3 and fails 3/3, and the pair alternates until the adapter's loop cap.
|
|
143
|
+
*
|
|
144
|
+
* Anchoring the window at the last prompt Harnery did not author makes progress
|
|
145
|
+
* monotonic: each repair adds a signal and none are lost, so the chain ends.
|
|
146
|
+
* A genuine human prompt always anchors a fresh window, so this cannot be used
|
|
147
|
+
* to inherit ritual credit across real turns.
|
|
148
|
+
*/
|
|
149
|
+
function resolveTurnStartMs(
|
|
150
|
+
ownerEvents: CanonicalEvent[],
|
|
151
|
+
nowMs: number,
|
|
152
|
+
fallbackMs: number,
|
|
153
|
+
): number {
|
|
154
|
+
// Honor the cutoff: `now_ms` marks what is relevant yet, so a prompt after it
|
|
155
|
+
// cannot anchor this turn. In production the cutoff is the clock and nothing
|
|
156
|
+
// follows it, but a replay over recorded history passes a past cutoff, and an
|
|
157
|
+
// unclamped search would anchor on a later turn and hand back an empty window.
|
|
158
|
+
const prompts = ownerEvents.filter((e) => {
|
|
159
|
+
if (e.event_type !== "user_prompt.submit") return false;
|
|
160
|
+
const t = Date.parse(e.ts);
|
|
161
|
+
return Number.isFinite(t) && t <= nowMs;
|
|
162
|
+
});
|
|
163
|
+
if (prompts.length === 0) return fallbackMs;
|
|
164
|
+
|
|
165
|
+
let index = prompts.length - 1;
|
|
166
|
+
let hops = 0;
|
|
167
|
+
while (index > 0 && hops < MAX_REMEDIATION_WALK_BACK) {
|
|
168
|
+
const prompt = prompts[index];
|
|
169
|
+
if (!prompt || !isStopRemediationPrompt(prompt)) break;
|
|
170
|
+
index -= 1;
|
|
171
|
+
hops += 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const anchor = prompts[index];
|
|
175
|
+
const parsed = anchor ? Date.parse(anchor.ts) : Number.NaN;
|
|
176
|
+
return Number.isFinite(parsed) ? parsed : fallbackMs;
|
|
177
|
+
}
|
|
178
|
+
|
|
92
179
|
/**
|
|
93
180
|
* Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
|
|
94
181
|
* when all three pass). Soft-fails open when the event stream isn't
|
|
@@ -151,18 +238,20 @@ export function evaluateStopHook(coordRoot: string, req: StopHookRequest): Verdi
|
|
|
151
238
|
}
|
|
152
239
|
|
|
153
240
|
// Turn window: from the most recent user_prompt.submit (this owner) up to
|
|
154
|
-
// either the explicit now_ms or the last event we see
|
|
241
|
+
// either the explicit now_ms or the last event we see, walking back over any
|
|
242
|
+
// Harnery-authored Stop-remediation prompts so a repair turn is judged
|
|
243
|
+
// together with the turn it repairs (see `resolveTurnStartMs`). Fall back to a
|
|
155
244
|
// 5-minute window when no user_prompt.submit is in scope (fresh wiring,
|
|
156
245
|
// out-of-window prompt, etc.) so a stale stream doesn't silently pass.
|
|
157
246
|
const nowMs = req.now_ms ?? Date.now();
|
|
158
|
-
const
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
const
|
|
247
|
+
const startMs = req.turn_window
|
|
248
|
+
? req.turn_window.start_ms
|
|
249
|
+
: resolveTurnStartMs(ownerEvents, nowMs, nowMs - 5 * 60 * 1000);
|
|
250
|
+
const endMs = req.turn_window ? req.turn_window.end_ms : nowMs;
|
|
162
251
|
|
|
163
252
|
const inTurn = ownerEvents.filter((e) => {
|
|
164
253
|
const t = Date.parse(e.ts);
|
|
165
|
-
return Number.isFinite(t) && t >= startMs && t <=
|
|
254
|
+
return Number.isFinite(t) && t >= startMs && t <= endMs;
|
|
166
255
|
});
|
|
167
256
|
|
|
168
257
|
const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* fire-and-forget. Empty text → no-op (no JSON written).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.ts";
|
|
22
|
+
import { resolveBinName } from "../../config.ts";
|
|
21
23
|
import type { Adapter } from "../events/schema.ts";
|
|
22
24
|
|
|
23
25
|
export type SystemEvent = "SessionStart" | "UserPromptSubmit" | "SubagentStart";
|
|
@@ -61,14 +63,25 @@ export function emitDeny(adapter: Adapter, reason: string): void {
|
|
|
61
63
|
export function emitStopBlock(adapter: Adapter, verdict: { reason?: string; rule: string }): 0 | 2 {
|
|
62
64
|
if (adapter === "codex") return 0;
|
|
63
65
|
|
|
64
|
-
const
|
|
65
|
-
verdict.reason ?? "End-of-turn coordination ritual incomplete."
|
|
66
|
-
}\n[agent-hook stop]: rule=${verdict.rule}`;
|
|
66
|
+
const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
|
|
67
67
|
if (adapter === "cursor") {
|
|
68
|
+
// The marker leads the message so the Stop verdict can recognize the turn
|
|
69
|
+
// Cursor opens from it (`STOP_REMEDIATION_MARKER`), and so the operator can
|
|
70
|
+
// see in chat that the message came from Harnery rather than from them.
|
|
71
|
+
// Both commands are named because one message that repairs the whole ritual
|
|
72
|
+
// ends the chain in a single followup; the failing rule is context.
|
|
73
|
+
const bin = resolveBinName();
|
|
74
|
+
const message = [
|
|
75
|
+
`${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
|
|
76
|
+
reason,
|
|
77
|
+
`Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${bin} agents status\` as your last tool call.`,
|
|
78
|
+
].join("\n");
|
|
68
79
|
process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
|
|
69
80
|
return 0;
|
|
70
81
|
}
|
|
71
|
-
|
|
82
|
+
// Claude Code continues the SAME turn on exit 2, so the single failing rule is
|
|
83
|
+
// both accurate and sufficient there. Left unchanged deliberately.
|
|
84
|
+
process.stderr.write(`${reason}\n[agent-hook stop]: rule=${verdict.rule}\n`);
|
|
72
85
|
return 2;
|
|
73
86
|
}
|
|
74
87
|
|