harnery 0.32.0 → 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 +3 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +212 -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/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +45 -6
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -0
- 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/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +5 -3
- package/dist/core/agents/cli.js +5 -1
- package/dist/core/agents/coord-client.d.ts +11 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- 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 +6 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +46 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +8 -2
- package/dist/core/agents/rules/stop-hook.js +4 -4
- package/dist/core/agents/state/heartbeat-projector.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +54 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +15 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +32 -0
- 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/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- 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/cli.js +90 -6
- 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/events/schema.d.ts +30 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- 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/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/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/package.json +2 -2
- package/src/commander.ts +3 -1
- package/src/commands/agents.ts +248 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/doctor.ts +59 -7
- package/src/commands/init.ts +11 -0
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/core/agents/canonical-emit.ts +6 -4
- package/src/core/agents/cli.ts +6 -1
- package/src/core/agents/coord-client.ts +11 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +71 -6
- package/src/core/agents/render/session-context.ts +14 -2
- package/src/core/agents/rules/stop-hook.ts +5 -5
- package/src/core/agents/state/heartbeat-projector.ts +63 -0
- package/src/core/agents/state/heartbeat-writer.ts +51 -0
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/cli.ts +98 -7
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/events/schema.ts +44 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/instructions/bundle.ts +199 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Owner identity invariant: every action operates on `.harnery/active/<instance_id>.json`
|
|
13
13
|
* (the file IS the heartbeat). No mutations happen elsewhere.
|
|
14
14
|
*/
|
|
15
|
+
import { type AgentActivity, type SessionStateEvidenceEvent, type TaskState } from "./session-state.js";
|
|
15
16
|
export interface Heartbeat {
|
|
16
17
|
schema_version?: number;
|
|
17
18
|
instance_id: string;
|
|
@@ -27,6 +28,12 @@ export interface Heartbeat {
|
|
|
27
28
|
files_touched: string[];
|
|
28
29
|
task?: string;
|
|
29
30
|
task_updated_at?: string | null;
|
|
31
|
+
activity?: AgentActivity;
|
|
32
|
+
activity_updated_at?: string;
|
|
33
|
+
activity_source?: string;
|
|
34
|
+
task_state?: TaskState;
|
|
35
|
+
task_state_updated_at?: string;
|
|
36
|
+
task_state_reason?: string;
|
|
30
37
|
/** Session name built on the first non-empty set-task (never rebuilt). Its
|
|
31
38
|
* presence is the "this session has been named" signal the prompt-context
|
|
32
39
|
* nudge and the Stop-hook naming rule key on. */
|
|
@@ -50,6 +57,12 @@ export interface Heartbeat {
|
|
|
50
57
|
[extra: string]: unknown;
|
|
51
58
|
}
|
|
52
59
|
export declare function readHeartbeat(coordRoot: string, instanceId: string): Heartbeat | null;
|
|
60
|
+
/** Apply one evidence event to the live state fields without draining the
|
|
61
|
+
* global event stream. This keeps permission prompts current while avoiding
|
|
62
|
+
* unrelated projector side effects before a tool authorization verdict. */
|
|
63
|
+
export declare function stampSessionStateEvent(coordRoot: string, instanceId: string, event: Omit<SessionStateEvidenceEvent, "data"> & {
|
|
64
|
+
data: unknown;
|
|
65
|
+
}): Heartbeat | null;
|
|
53
66
|
export declare function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null;
|
|
54
67
|
/**
|
|
55
68
|
* Build the copy-pasteable session name from the coord identity + the agent's
|
|
@@ -60,6 +73,8 @@ export declare function buildSuggestedName(agentName: string, descriptionParts:
|
|
|
60
73
|
suggestedName: string;
|
|
61
74
|
description: string;
|
|
62
75
|
} | null;
|
|
76
|
+
/** Build the operator-facing title projected from an explicit task lifecycle. */
|
|
77
|
+
export declare function buildLifecycleSuggestedName(agentName: string, task: string | undefined, state: TaskState): string | null;
|
|
63
78
|
/** Stamp the sighting once the suggested name has been observed in assistant
|
|
64
79
|
* reply text; later turns skip the transcript scan. Records WHICH name was seen,
|
|
65
80
|
* because the skip is only valid for that one: if a later set-task mints a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { emit } from "../events/emit.js";
|
|
18
|
+
import { applySessionStateEvent, } from "./session-state.js";
|
|
18
19
|
/** Inline adapter normalizer (mirrors canonical-emit.normalizeAdapter), kept
|
|
19
20
|
* local so the writer's only cross-module dep is the in-process emit(). */
|
|
20
21
|
function adapterOf(platform) {
|
|
@@ -80,6 +81,26 @@ export function readHeartbeat(coordRoot, instanceId) {
|
|
|
80
81
|
return null;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
/** Apply one evidence event to the live state fields without draining the
|
|
85
|
+
* global event stream. This keeps permission prompts current while avoiding
|
|
86
|
+
* unrelated projector side effects before a tool authorization verdict. */
|
|
87
|
+
export function stampSessionStateEvent(coordRoot, instanceId, event) {
|
|
88
|
+
return mutate(coordRoot, instanceId, (heartbeat) => {
|
|
89
|
+
const data = typeof event.data === "object" && event.data !== null && !Array.isArray(event.data)
|
|
90
|
+
? event.data
|
|
91
|
+
: {};
|
|
92
|
+
const state = applySessionStateEvent(heartbeat, { ...event, data });
|
|
93
|
+
return {
|
|
94
|
+
...heartbeat,
|
|
95
|
+
activity: state.activity,
|
|
96
|
+
activity_updated_at: state.activity_updated_at,
|
|
97
|
+
activity_source: state.activity_source,
|
|
98
|
+
task_state: state.task_state,
|
|
99
|
+
task_state_updated_at: state.task_state_updated_at,
|
|
100
|
+
task_state_reason: state.task_state_reason,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
83
104
|
function mutate(coordRoot, instanceId, fn) {
|
|
84
105
|
const hb = readHeartbeat(coordRoot, instanceId);
|
|
85
106
|
if (!hb)
|
|
@@ -120,6 +141,17 @@ export function buildSuggestedName(agentName, descriptionParts) {
|
|
|
120
141
|
const name = agentName?.trim() || "unknown";
|
|
121
142
|
return { suggestedName: `Agent ${name} - ${description}`, description };
|
|
122
143
|
}
|
|
144
|
+
/** Build the operator-facing title projected from an explicit task lifecycle. */
|
|
145
|
+
export function buildLifecycleSuggestedName(agentName, task, state) {
|
|
146
|
+
const built = buildSuggestedName(agentName, task ? [task] : []);
|
|
147
|
+
if (!built)
|
|
148
|
+
return null;
|
|
149
|
+
if (state === "blocked")
|
|
150
|
+
return `[BLOCKED] - ${built.suggestedName}`;
|
|
151
|
+
if (state === "done")
|
|
152
|
+
return `[DONE] - ${built.suggestedName}`;
|
|
153
|
+
return built.suggestedName;
|
|
154
|
+
}
|
|
123
155
|
/** Stamp the sighting once the suggested name has been observed in assistant
|
|
124
156
|
* reply text; later turns skip the transcript scan. Records WHICH name was seen,
|
|
125
157
|
* because the skip is only valid for that one: if a later set-task mints a
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** What the agent is doing now. `unknown` is evidence-safe, not an error. */
|
|
2
|
+
export type AgentActivity = "unknown" | "working" | "needs_input" | "idle";
|
|
3
|
+
/** Whether the declared task remains open. Activity never changes this axis. */
|
|
4
|
+
export type TaskState = "active" | "blocked" | "done";
|
|
5
|
+
/** Raw fields stored in a live heartbeat and rebuilt from canonical events. */
|
|
6
|
+
export interface SessionStateFields {
|
|
7
|
+
activity?: AgentActivity;
|
|
8
|
+
activity_updated_at?: string;
|
|
9
|
+
activity_source?: string;
|
|
10
|
+
task_state?: TaskState;
|
|
11
|
+
task_state_updated_at?: string;
|
|
12
|
+
task_state_reason?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Reader-facing state with compatibility defaults applied. */
|
|
15
|
+
export interface SessionState extends SessionStateFields {
|
|
16
|
+
activity: AgentActivity;
|
|
17
|
+
task_state: TaskState;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionStateSelector {
|
|
20
|
+
instance_id?: string;
|
|
21
|
+
session_id?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SessionStateEvidenceEvent {
|
|
24
|
+
event_type: string;
|
|
25
|
+
ts: string;
|
|
26
|
+
data?: Record<string, unknown>;
|
|
27
|
+
instance_id?: string;
|
|
28
|
+
session_id?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Apply one canonical event to the two independent state axes.
|
|
32
|
+
*
|
|
33
|
+
* The table is deliberately evidence-only. Output chunks, post-tool events,
|
|
34
|
+
* narration, coordination commands, and elapsed time cannot clear an observed
|
|
35
|
+
* input wait. A command start only counts as progress when the session was
|
|
36
|
+
* already inside an open turn (`working` or `needs_input`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function applySessionStateEvent(current: Readonly<SessionStateFields>, event: SessionStateEvidenceEvent): SessionStateFields;
|
|
39
|
+
/**
|
|
40
|
+
* Rebuild both axes from canonical events after a heartbeat has disappeared.
|
|
41
|
+
* Events may be the whole ledger or an already-filtered session slice.
|
|
42
|
+
*/
|
|
43
|
+
export declare function foldSessionState(events: readonly SessionStateEvidenceEvent[], selector?: SessionStateSelector): SessionState;
|
|
44
|
+
//# sourceMappingURL=session-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-state.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/session-state.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAC;AAE3E,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtD,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACrC,KAAK,EAAE,yBAAyB,GAC/B,kBAAkB,CAkCpB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,yBAAyB,EAAE,EAC5C,QAAQ,GAAE,oBAAyB,GAClC,YAAY,CAcd"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const TERMINAL_ACTIVITY_EVENTS = new Set(["session.end", "subagent.stop", "turn.stop"]);
|
|
2
|
+
/**
|
|
3
|
+
* Apply one canonical event to the two independent state axes.
|
|
4
|
+
*
|
|
5
|
+
* The table is deliberately evidence-only. Output chunks, post-tool events,
|
|
6
|
+
* narration, coordination commands, and elapsed time cannot clear an observed
|
|
7
|
+
* input wait. A command start only counts as progress when the session was
|
|
8
|
+
* already inside an open turn (`working` or `needs_input`).
|
|
9
|
+
*/
|
|
10
|
+
export function applySessionStateEvent(current, event) {
|
|
11
|
+
const next = { ...current };
|
|
12
|
+
if (event.event_type === "session.start" || event.event_type === "subagent.start") {
|
|
13
|
+
setActivity(next, "idle", event);
|
|
14
|
+
}
|
|
15
|
+
else if (event.event_type === "user_prompt.submit" || event.event_type === "tool.pre_use") {
|
|
16
|
+
setActivity(next, "working", event);
|
|
17
|
+
}
|
|
18
|
+
else if (event.event_type === "interaction.input_requested") {
|
|
19
|
+
setActivity(next, "needs_input", event);
|
|
20
|
+
}
|
|
21
|
+
else if (event.event_type === "command.start" &&
|
|
22
|
+
(current.activity === "working" || current.activity === "needs_input")) {
|
|
23
|
+
setActivity(next, "working", event);
|
|
24
|
+
}
|
|
25
|
+
else if (TERMINAL_ACTIVITY_EVENTS.has(event.event_type)) {
|
|
26
|
+
setActivity(next, "idle", event);
|
|
27
|
+
}
|
|
28
|
+
if (event.event_type === "state.task_state") {
|
|
29
|
+
const data = event.data ?? {};
|
|
30
|
+
const state = field(data, "state");
|
|
31
|
+
if (state === "active" || state === "blocked" || state === "done") {
|
|
32
|
+
next.task_state = state;
|
|
33
|
+
next.task_state_updated_at = event.ts;
|
|
34
|
+
const reason = field(data, "reason");
|
|
35
|
+
if (typeof reason === "string" && reason.length > 0) {
|
|
36
|
+
next.task_state_reason = reason;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
delete next.task_state_reason;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rebuild both axes from canonical events after a heartbeat has disappeared.
|
|
47
|
+
* Events may be the whole ledger or an already-filtered session slice.
|
|
48
|
+
*/
|
|
49
|
+
export function foldSessionState(events, selector = {}) {
|
|
50
|
+
const ordered = events
|
|
51
|
+
.map((event, index) => ({ event, index }))
|
|
52
|
+
.filter(({ event }) => matches(event, selector))
|
|
53
|
+
.sort((a, b) => timestamp(a.event) - timestamp(b.event) || a.index - b.index);
|
|
54
|
+
let fields = {};
|
|
55
|
+
for (const { event } of ordered)
|
|
56
|
+
fields = applySessionStateEvent(fields, event);
|
|
57
|
+
return {
|
|
58
|
+
...fields,
|
|
59
|
+
activity: fields.activity ?? "unknown",
|
|
60
|
+
task_state: fields.task_state ?? "active",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function setActivity(target, activity, event) {
|
|
64
|
+
target.activity = activity;
|
|
65
|
+
target.activity_updated_at = event.ts;
|
|
66
|
+
target.activity_source = event.event_type;
|
|
67
|
+
}
|
|
68
|
+
function matches(event, selector) {
|
|
69
|
+
if (selector.instance_id && event.instance_id !== selector.instance_id)
|
|
70
|
+
return false;
|
|
71
|
+
if (selector.session_id && event.session_id !== selector.session_id)
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function timestamp(event) {
|
|
76
|
+
const parsed = Date.parse(event.ts);
|
|
77
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
78
|
+
}
|
|
79
|
+
function field(data, key) {
|
|
80
|
+
return data[key];
|
|
81
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhE,MAAM,WAAW,SAAS;IACxB,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,cAAc,CAAC;IAC3B,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+FAA+F;IAC/F,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,EAAE,SAAS,EAYzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,EAWpC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhE,MAAM,WAAW,SAAS;IACxB,6FAA6F;IAC7F,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,cAAc,CAAC;IAC3B,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+FAA+F;IAC/F,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,EAAE,SAAS,EAYzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,EAWpC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,EAWnC,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,EAAE,SAAS,EAI1C,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,UAWpC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,WAAW,CAqBxD,CAAC"}
|
|
@@ -42,6 +42,7 @@ export const CURSOR_EVENTS = [
|
|
|
42
42
|
export const CODEX_EVENTS = [
|
|
43
43
|
{ settingsKey: "SessionStart", subcommand: "session-start" },
|
|
44
44
|
{ settingsKey: "PreToolUse", subcommand: "pre-tool-use" },
|
|
45
|
+
{ settingsKey: "PermissionRequest", subcommand: "permission-request" },
|
|
45
46
|
{ settingsKey: "PostToolUse", subcommand: "post-tool-use" },
|
|
46
47
|
{ settingsKey: "UserPromptSubmit", subcommand: "user-prompt-submit" },
|
|
47
48
|
{ settingsKey: "SubagentStart", subcommand: "sub-agent-start" },
|
|
@@ -63,5 +63,5 @@ export declare function normalizeEventName(eventName: string): {
|
|
|
63
63
|
event_type: NormalizedEventType;
|
|
64
64
|
intra_turn: boolean;
|
|
65
65
|
} | null;
|
|
66
|
-
export type NormalizedEventType = "session.start" | "session.end" | "user_prompt.submit" | "turn.stop" | "subagent.start" | "subagent.stop" | "tool.pre_use" | "tool.post_use" | "tool.post_use_failure" | "context.compaction.started" | "context.compaction.completed";
|
|
66
|
+
export type NormalizedEventType = "session.start" | "session.end" | "user_prompt.submit" | "turn.stop" | "subagent.start" | "subagent.stop" | "tool.pre_use" | "interaction.input_requested" | "tool.post_use" | "tool.post_use_failure" | "context.compaction.started" | "context.compaction.completed";
|
|
67
67
|
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CA+ChF;AAQD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CA+ChF;AAQD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAmCjE;AAED,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,cAAc,GACd,6BAA6B,GAC7B,eAAe,GACf,uBAAuB,GACvB,4BAA4B,GAC5B,8BAA8B,CAAC"}
|
|
@@ -139,6 +139,8 @@ export function normalizeEventName(eventName) {
|
|
|
139
139
|
case "pre-tool-use":
|
|
140
140
|
case "before-shell-execution":
|
|
141
141
|
return { event_type: "tool.pre_use", intra_turn: true };
|
|
142
|
+
case "permission-request":
|
|
143
|
+
return { event_type: "interaction.input_requested", intra_turn: true };
|
|
142
144
|
case "post-tool-use":
|
|
143
145
|
return { event_type: "tool.post_use", intra_turn: true };
|
|
144
146
|
case "post-tool-use-failure":
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -20,12 +20,14 @@ import { spawnSync } from "node:child_process";
|
|
|
20
20
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { coordEnv } from "../../lib/env.js";
|
|
23
|
+
import { buildInstructionBundle } from "../../lib/instructions/bundle.js";
|
|
23
24
|
import { replayCodexJsonl } from "../agents/codex-replay.js";
|
|
24
25
|
import { coordBinPath } from "../agents/coord-bin.js";
|
|
25
26
|
import { consumeSince, writeCursor } from "../agents/events/consume.js";
|
|
26
27
|
import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
|
|
27
28
|
import { evaluateStopHook } from "../agents/rules/stop-hook.js";
|
|
28
29
|
import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
|
|
30
|
+
import { stampSessionStateEvent } from "../agents/state/heartbeat-writer.js";
|
|
29
31
|
import { writePidmapRow } from "../agents/state/pidmap.js";
|
|
30
32
|
import { shellMutationPaths } from "../agents/state/shell-mutation.js";
|
|
31
33
|
import { agentsRequireGitFinalization, resolveBinName } from "../config.js";
|
|
@@ -210,6 +212,7 @@ function buildEventData(eventType, ctx) {
|
|
|
210
212
|
if (adapterPid) {
|
|
211
213
|
writePidmapViaAgentCoord(ctx.coordRoot, adapterPid, ctx.instanceId, adapterPlatform);
|
|
212
214
|
}
|
|
215
|
+
const bundle = safeInstructionBundle(ctx.coordRoot, p?.cwd ?? process.cwd(), ctx.adapter);
|
|
213
216
|
return {
|
|
214
217
|
started_at: new Date().toISOString(),
|
|
215
218
|
cwd: p?.cwd ?? process.cwd(),
|
|
@@ -223,6 +226,14 @@ function buildEventData(eventType, ctx) {
|
|
|
223
226
|
name: assigned?.name,
|
|
224
227
|
kind: "session",
|
|
225
228
|
agent_id: ctx.instanceId,
|
|
229
|
+
...(bundle
|
|
230
|
+
? {
|
|
231
|
+
instruction_bundle_id: bundle.instruction_bundle_id,
|
|
232
|
+
instruction_source_id: bundle.canonical_source_id,
|
|
233
|
+
instruction_profile_root: bundle.profile_root,
|
|
234
|
+
instruction_component_count: bundle.components.length,
|
|
235
|
+
}
|
|
236
|
+
: {}),
|
|
226
237
|
// Recorded fork lineage rides the canonical event too, so full replay
|
|
227
238
|
// and derived readers converge on the same parent linkage that
|
|
228
239
|
// .name-history carries (the ADR 0017 dual-write pattern).
|
|
@@ -340,6 +351,15 @@ function buildEventData(eventType, ctx) {
|
|
|
340
351
|
...(clamped.truncated ? { truncated: true } : {}),
|
|
341
352
|
};
|
|
342
353
|
}
|
|
354
|
+
case "interaction.input_requested": {
|
|
355
|
+
const description = extractToolDescription(p?.tool_input);
|
|
356
|
+
const reason = description ? clampString(description, 500).value : undefined;
|
|
357
|
+
return {
|
|
358
|
+
request_kind: "permission",
|
|
359
|
+
tool_name: p?.tool_name,
|
|
360
|
+
...(reason ? { reason } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
343
363
|
case "tool.post_use": {
|
|
344
364
|
const toolName = p?.tool_name ?? "unknown";
|
|
345
365
|
const summary = summarizeOutput(p?.tool_response);
|
|
@@ -386,6 +406,15 @@ function buildEventData(eventType, ctx) {
|
|
|
386
406
|
}
|
|
387
407
|
}
|
|
388
408
|
}
|
|
409
|
+
function safeInstructionBundle(coordRoot, cwd, adapter) {
|
|
410
|
+
try {
|
|
411
|
+
return buildInstructionBundle({ coordRoot, cwd, adapter });
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
logError(coordRoot, err, { phase: "instruction-bundle-identity" });
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
389
418
|
function objectRecord(value) {
|
|
390
419
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
391
420
|
? value
|
|
@@ -488,6 +517,19 @@ async function main() {
|
|
|
488
517
|
owner_source: owner.source,
|
|
489
518
|
event_id: envelope.event_id,
|
|
490
519
|
});
|
|
520
|
+
// Activity is only useful while it is current. Stamp prompt and permission
|
|
521
|
+
// evidence directly from this envelope rather than draining the global
|
|
522
|
+
// projector: a drain here could apply a tool event before its authorization
|
|
523
|
+
// verdict. Tool progress is stamped later, after the guard allows it.
|
|
524
|
+
if (norm.event_type === "user_prompt.submit" ||
|
|
525
|
+
norm.event_type === "interaction.input_requested") {
|
|
526
|
+
try {
|
|
527
|
+
stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
logError(coordRoot, err, { phase: "activity-projection" });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
491
533
|
// Context telemetry is opportunistic and truthful: only persist a sample
|
|
492
534
|
// when the adapter payload actually exposes usage/window data. Identical
|
|
493
535
|
// measurements are de-duplicated before they reach the canonical stream.
|
|
@@ -811,6 +853,23 @@ async function main() {
|
|
|
811
853
|
bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
|
|
812
854
|
workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
|
|
813
855
|
});
|
|
856
|
+
const enforcementMode = stopEnforcementMode(verdict.rule);
|
|
857
|
+
emit(coordRoot, {
|
|
858
|
+
event_type: "stop.verdict",
|
|
859
|
+
instance_id: owner.instance_id,
|
|
860
|
+
session_id: sessionId,
|
|
861
|
+
turn_id: payload?.turn_id,
|
|
862
|
+
parent_turn_id: payload?.parent_turn_id,
|
|
863
|
+
adapter,
|
|
864
|
+
data: {
|
|
865
|
+
allow: verdict.allow,
|
|
866
|
+
rule: verdict.rule,
|
|
867
|
+
...(verdict.reason ? { reason: verdict.reason } : {}),
|
|
868
|
+
enforcement_mode: enforcementMode,
|
|
869
|
+
eligible: enforcementMode === "enforced",
|
|
870
|
+
nag_delivered: enforcementMode === "enforced" && !verdict.allow,
|
|
871
|
+
},
|
|
872
|
+
});
|
|
814
873
|
if (!verdict.allow) {
|
|
815
874
|
// Adapter-aware enforcement channel: Claude Code / Codex honor exit-2 +
|
|
816
875
|
// stderr as a turn block; Cursor ignores exit codes (fail-open) and
|
|
@@ -894,12 +953,21 @@ async function main() {
|
|
|
894
953
|
// apply_patch (codex) parses paths from the patch body and runs verdict
|
|
895
954
|
// per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
|
|
896
955
|
// (incl. Agent) yield no targets and pass through with no deny.
|
|
956
|
+
let guardAllowed = true;
|
|
897
957
|
try {
|
|
898
|
-
await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
|
|
958
|
+
guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
|
|
899
959
|
}
|
|
900
960
|
catch (err) {
|
|
901
961
|
logError(coordRoot, err, { phase: "pre-tool-use-guard" });
|
|
902
962
|
}
|
|
963
|
+
if (guardAllowed) {
|
|
964
|
+
try {
|
|
965
|
+
stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
logError(coordRoot, err, { phase: "activity-projection" });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
903
971
|
// Shell-mutation warn (warn-only, never blocks). Was the cursor
|
|
904
972
|
// beforeShellExecution + codex preToolUse-Bash shell-mutation-claim-log in
|
|
905
973
|
// the per-adapter shell adapters. Cursor sends the command at payload.command;
|
|
@@ -974,11 +1042,20 @@ async function main() {
|
|
|
974
1042
|
}
|
|
975
1043
|
return 0;
|
|
976
1044
|
}
|
|
1045
|
+
function stopEnforcementMode(rule) {
|
|
1046
|
+
if (rule === "stop-hook.codex_observe_only")
|
|
1047
|
+
return "observe_only";
|
|
1048
|
+
if (rule === "stop-hook.fail_open" || rule === "stop-hook.no_history")
|
|
1049
|
+
return "fail_open";
|
|
1050
|
+
if (rule === "stop-hook.bypass" || rule === "stop-hook.workflow_child")
|
|
1051
|
+
return "exempt";
|
|
1052
|
+
return "enforced";
|
|
1053
|
+
}
|
|
977
1054
|
async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapter) {
|
|
978
1055
|
const toolName = data.tool_name ?? "";
|
|
979
1056
|
const rawTargets = collectGuardTargets(toolName, data);
|
|
980
1057
|
if (rawTargets.length === 0)
|
|
981
|
-
return;
|
|
1058
|
+
return true;
|
|
982
1059
|
let targets;
|
|
983
1060
|
if (agentsRequireGitFinalization(coordRoot)) {
|
|
984
1061
|
const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
|
|
@@ -986,7 +1063,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
986
1063
|
if (denied && !denied.allow) {
|
|
987
1064
|
const { emitDeny } = await import("./adapter/output.js");
|
|
988
1065
|
emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
|
|
989
|
-
return;
|
|
1066
|
+
return false;
|
|
990
1067
|
}
|
|
991
1068
|
const allowed = decisions.filter((decision) => decision.allow);
|
|
992
1069
|
targets = allowed.map((decision) => ({
|
|
@@ -1001,14 +1078,15 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
1001
1078
|
.map((path) => ({ path }));
|
|
1002
1079
|
}
|
|
1003
1080
|
if (targets.length === 0)
|
|
1004
|
-
return;
|
|
1081
|
+
return true;
|
|
1005
1082
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
1006
1083
|
if (!existsSync(agentCoordBin)) {
|
|
1007
1084
|
if (agentsRequireGitFinalization(coordRoot)) {
|
|
1008
1085
|
const { emitDeny } = await import("./adapter/output.js");
|
|
1009
1086
|
emitDeny(adapter, "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.");
|
|
1087
|
+
return false;
|
|
1010
1088
|
}
|
|
1011
|
-
return;
|
|
1089
|
+
return true;
|
|
1012
1090
|
}
|
|
1013
1091
|
// For apply_patch (multi-file), collect siblings so the deny reason names
|
|
1014
1092
|
// them. For single-file tools the array has one entry.
|
|
@@ -1048,7 +1126,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
1048
1126
|
}
|
|
1049
1127
|
const { emitDeny } = await import("./adapter/output.js");
|
|
1050
1128
|
emitDeny(adapter, reason);
|
|
1051
|
-
return;
|
|
1129
|
+
return false;
|
|
1052
1130
|
}
|
|
1053
1131
|
// The verdict mutates the live heartbeat immediately, but that file is a
|
|
1054
1132
|
// projection and can be rebuilt at any later hook. Persist the acquisition
|
|
@@ -1068,6 +1146,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
1068
1146
|
},
|
|
1069
1147
|
});
|
|
1070
1148
|
}
|
|
1149
|
+
return true;
|
|
1071
1150
|
}
|
|
1072
1151
|
/** Canonicalize a path to monorepo-relative form. Absolute paths under
|
|
1073
1152
|
* coordRoot get the prefix stripped; relative paths pass through (assumed
|
|
@@ -1384,11 +1463,16 @@ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionI
|
|
|
1384
1463
|
// staleness nudges; Claude Code enforces those via its Stop transcript.
|
|
1385
1464
|
// Codex gets a fresh status-footer reminder on every prompt. Its Stop hook
|
|
1386
1465
|
// stays observe-only, so missing the footer never replaces the answer.
|
|
1466
|
+
// Claude Code + Cursor (the Stop-ENFORCING adapters) get a fresh per-turn
|
|
1467
|
+
// ritual reminder: satisfying the contract up front is far cheaper than
|
|
1468
|
+
// the bounce-and-retry the Stop hook otherwise forces.
|
|
1387
1469
|
args.push("--session-name-nudge");
|
|
1388
1470
|
if (adapter === "cursor" || adapter === "codex")
|
|
1389
1471
|
args.push("--task-nudge");
|
|
1390
1472
|
if (adapter === "codex")
|
|
1391
1473
|
args.push("--status-footer-nudge");
|
|
1474
|
+
if (adapter === "claude-code" || adapter === "cursor")
|
|
1475
|
+
args.push("--turn-ritual-nudge", adapter);
|
|
1392
1476
|
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1393
1477
|
if (result.status === 0 && result.stdout)
|
|
1394
1478
|
additionalContext = result.stdout.trim();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type CodexHookTrustStatus = "managed" | "trusted" | "untrusted" | "modified";
|
|
2
|
+
export type CodexHookAuthorizationStatus = "runnable" | "review_required" | "disabled" | "not_wired" | "error";
|
|
3
|
+
export interface CodexHookAuthorizationResult {
|
|
4
|
+
status: CodexHookAuthorizationStatus;
|
|
5
|
+
detail: string;
|
|
6
|
+
hookCount: number;
|
|
7
|
+
disabledCount: number;
|
|
8
|
+
trustCounts: Partial<Record<CodexHookTrustStatus, number>>;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexHookAuthorizationProbeOptions {
|
|
11
|
+
cwd: string;
|
|
12
|
+
codexBin?: string;
|
|
13
|
+
appServerArgs?: string[];
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert Codex app-server's `hooks/list` response into the one authorization
|
|
19
|
+
* state doctor needs. This is intentionally read-only: hashes are observed but
|
|
20
|
+
* never persisted, and only commands containing Harnery's `agent-hook` binary
|
|
21
|
+
* are included in the result.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assessCodexHookList(response: unknown, cwd: string): CodexHookAuthorizationResult;
|
|
24
|
+
/**
|
|
25
|
+
* Ask the active Codex CLI for hook authorization through its app-server
|
|
26
|
+
* protocol. The child is closed as soon as `hooks/list` responds. Harnery never
|
|
27
|
+
* calls the trust-write method and never reads Codex's private config format.
|
|
28
|
+
*/
|
|
29
|
+
export declare function probeCodexHookAuthorization(options: CodexHookAuthorizationProbeOptions): Promise<CodexHookAuthorizationResult>;
|
|
30
|
+
//# sourceMappingURL=codex-authorization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-authorization.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/codex-authorization.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;AAEpF,MAAM,MAAM,4BAA4B,GACpC,UAAU,GACV,iBAAiB,GACjB,UAAU,GACV,WAAW,GACX,OAAO,CAAC;AAEZ,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,4BAA4B,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,4BAA4B,CAqGhG;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,4BAA4B,CAAC,CAoFvC"}
|