harnery 0.31.5 → 0.32.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- 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/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.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +11 -4
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +27 -9
- 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/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- 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 +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -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/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/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 +22 -5
- 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 +169 -27
- 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 +27 -0
- 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/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/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/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 +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- 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/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +38 -9
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- 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 +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- 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/output.ts +9 -4
- package/src/core/hooks/adapter/parse.ts +24 -5
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- 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 +27 -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/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/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -47,7 +47,9 @@ const MAX_RECORD_BYTES = 256 * 1024;
|
|
|
47
47
|
const MAX_EVENTS_BYTES = 512 * 1024;
|
|
48
48
|
const MAX_REASON = 2_000;
|
|
49
49
|
const PROPOSAL_ROOT_RULE =
|
|
50
|
-
"proposal.root
|
|
50
|
+
"proposal.root is inferred by Harnery as the single final outcome that no other proposed item depends on; it must use a root-capable template and depend transitively on every other proposed item; it never preserves or repeats active_root, which is context only";
|
|
51
|
+
const PLANNER_ROOT_RULE =
|
|
52
|
+
"Dependencies point from later work to earlier prerequisites. Do not return proposal.root. Harnery infers it as the single final outcome that no other proposed item depends on; make that outcome use a root-capable template and depend transitively on every other proposed item.";
|
|
51
53
|
|
|
52
54
|
export interface RunGovernorPlannerInput {
|
|
53
55
|
coordRoot: string;
|
|
@@ -729,7 +731,7 @@ function plannerScript(
|
|
|
729
731
|
: "Return decision=apply with a replacement immutable root graph, or decision=attention when human judgment is required.",
|
|
730
732
|
"Never claim mission completion unless the frozen mission acceptance is met. Never request a workflow outside the frozen template catalog.",
|
|
731
733
|
"Dependencies may name an active work ID or an earlier key in your proposed work array.",
|
|
732
|
-
|
|
734
|
+
PLANNER_ROOT_RULE,
|
|
733
735
|
"Work keys are lowercase identifiers no longer than 32 characters. Keep titles within 200 characters, objectives within 4000 characters, and each acceptance criterion within 500 characters.",
|
|
734
736
|
`Plan id: ${planId}`,
|
|
735
737
|
`Planning trigger: ${trigger}`,
|
|
@@ -1331,12 +1333,6 @@ function planProposalSchema(
|
|
|
1331
1333
|
const applyProperties: Record<string, StageSchema> = {
|
|
1332
1334
|
decision: { type: "string", enum: ["apply"] },
|
|
1333
1335
|
rationale,
|
|
1334
|
-
root: {
|
|
1335
|
-
type: "string",
|
|
1336
|
-
minLength: 1,
|
|
1337
|
-
maxLength: 32,
|
|
1338
|
-
pattern: "^[a-z][a-z0-9-]{0,31}$",
|
|
1339
|
-
},
|
|
1340
1336
|
work: {
|
|
1341
1337
|
type: "array",
|
|
1342
1338
|
minItems: 1,
|
|
@@ -1344,7 +1340,7 @@ function planProposalSchema(
|
|
|
1344
1340
|
items: workItem,
|
|
1345
1341
|
},
|
|
1346
1342
|
};
|
|
1347
|
-
const applyRequired = ["decision", "rationale", "
|
|
1343
|
+
const applyRequired = ["decision", "rationale", "work"];
|
|
1348
1344
|
if (record.intent.mission) {
|
|
1349
1345
|
applyProperties.milestone = milestone;
|
|
1350
1346
|
applyRequired.push("milestone");
|
|
@@ -1390,10 +1386,10 @@ function normalizeProposal(
|
|
|
1390
1386
|
const proposedAt = preservedProposedAt ?? new Date().toISOString();
|
|
1391
1387
|
const decision = enumValue(value.decision, ["apply", "complete", "attention"], "plan decision");
|
|
1392
1388
|
const rationale = bounded(value.rationale, "plan rationale", MAX_REASON);
|
|
1393
|
-
const
|
|
1389
|
+
const terminalRoot = typeof value.root === "string" ? value.root.trim() : "";
|
|
1394
1390
|
if (!Array.isArray(value.work)) throw new Error("plan work must be an array");
|
|
1395
1391
|
if (decision === "attention" || decision === "complete") {
|
|
1396
|
-
if (
|
|
1392
|
+
if (terminalRoot || value.work.length > 0 || value.milestone !== undefined) {
|
|
1397
1393
|
throw new Error(`${decision} plan must not contain work or milestone data`);
|
|
1398
1394
|
}
|
|
1399
1395
|
if (decision === "complete" && (!record.intent.mission || trigger !== "milestone")) {
|
|
@@ -1473,7 +1469,16 @@ function normalizeProposal(
|
|
|
1473
1469
|
template,
|
|
1474
1470
|
};
|
|
1475
1471
|
});
|
|
1476
|
-
|
|
1472
|
+
const dependedOn = new Set(
|
|
1473
|
+
work.flatMap((item) => item.dependencies.filter((dependency) => seen.has(dependency))),
|
|
1474
|
+
);
|
|
1475
|
+
const terminalOutcomes = work.filter((item) => !dependedOn.has(item.key));
|
|
1476
|
+
if (terminalOutcomes.length !== 1) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`plan must have exactly one final outcome; found ${terminalOutcomes.map((item) => item.key).join(", ")}`,
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
const root = terminalOutcomes[0]!.key;
|
|
1477
1482
|
const rootSpec = work.find((item) => item.key === root);
|
|
1478
1483
|
if (!rootSpec || !policy.templates[rootSpec.template]?.root) {
|
|
1479
1484
|
throw new Error(`plan root ${root} must use a root-capable template`);
|
|
@@ -4,32 +4,23 @@ import type { Adapter } from "../events/schema.ts";
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the adapter firing the hook. The dispatcher binary is always
|
|
6
6
|
* invoked with `--adapter <name>` per the current wiring in each adapter's
|
|
7
|
-
* settings file.
|
|
8
|
-
*
|
|
9
|
-
* re-runs `harn init`. Returns null when neither flag is present; caller falls
|
|
10
|
-
* through to the env-based fallback or skips emission.
|
|
7
|
+
* settings file. Returns null when the flag is absent; caller falls through to
|
|
8
|
+
* the environment fallback or skips emission.
|
|
11
9
|
*/
|
|
12
10
|
export function detectAdapter(argv: readonly string[]): Adapter | null {
|
|
13
11
|
for (let i = 0; i < argv.length; i++) {
|
|
14
12
|
const a = argv[i]!;
|
|
15
|
-
if (a === "--adapter"
|
|
13
|
+
if (a === "--adapter") {
|
|
16
14
|
return validate(argv[i + 1]);
|
|
17
15
|
}
|
|
18
16
|
if (a.startsWith("--adapter=")) {
|
|
19
17
|
return validate(a.slice("--adapter=".length));
|
|
20
18
|
}
|
|
21
|
-
if (a.startsWith("--harness=")) {
|
|
22
|
-
return validate(a.slice("--harness=".length));
|
|
23
|
-
}
|
|
24
19
|
}
|
|
25
|
-
return validate(coordEnv("AGENT_COORD_ADAPTER")
|
|
20
|
+
return validate(coordEnv("AGENT_COORD_ADAPTER"));
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
function validate(v: string | undefined): Adapter | null {
|
|
29
24
|
if (v === "claude-code" || v === "cursor" || v === "codex") return v;
|
|
30
|
-
// Legacy: some adapters use `claude_code` (underscore). Map
|
|
31
|
-
// it through so the env-based fallback works for callers that haven't
|
|
32
|
-
// updated yet.
|
|
33
|
-
if (v === "claude_code") return "claude-code";
|
|
34
25
|
return null;
|
|
35
26
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.ts";
|
|
22
|
-
import { resolveBinName } from "../../config.ts";
|
|
22
|
+
import { endOfTurnStatusCommand, resolveBinName } from "../../config.ts";
|
|
23
23
|
import type { Adapter } from "../events/schema.ts";
|
|
24
24
|
|
|
25
25
|
export type SystemEvent = "SessionStart" | "UserPromptSubmit" | "SubagentStart";
|
|
@@ -60,7 +60,11 @@ export function emitDeny(adapter: Adapter, reason: string): void {
|
|
|
60
60
|
* We exit 0 so Cursor treats the run as a success and honors the output.
|
|
61
61
|
* (Confirmed against cursor.com/docs/agent/hooks.)
|
|
62
62
|
*/
|
|
63
|
-
export function emitStopBlock(
|
|
63
|
+
export function emitStopBlock(
|
|
64
|
+
adapter: Adapter,
|
|
65
|
+
verdict: { reason?: string; rule: string },
|
|
66
|
+
coordRoot?: string,
|
|
67
|
+
): 0 | 2 {
|
|
64
68
|
if (adapter === "codex") return 0;
|
|
65
69
|
|
|
66
70
|
const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
|
|
@@ -70,11 +74,12 @@ export function emitStopBlock(adapter: Adapter, verdict: { reason?: string; rule
|
|
|
70
74
|
// see in chat that the message came from Harnery rather than from them.
|
|
71
75
|
// Both commands are named because one message that repairs the whole ritual
|
|
72
76
|
// ends the chain in a single followup; the failing rule is context.
|
|
73
|
-
const bin = resolveBinName();
|
|
77
|
+
const bin = resolveBinName(coordRoot);
|
|
78
|
+
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
74
79
|
const message = [
|
|
75
80
|
`${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
|
|
76
81
|
reason,
|
|
77
|
-
`Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${
|
|
82
|
+
`Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${statusCommand}\` as your last tool call.`,
|
|
78
83
|
].join("\n");
|
|
79
84
|
process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
|
|
80
85
|
return 0;
|
|
@@ -43,7 +43,7 @@ export interface ParsedPayload {
|
|
|
43
43
|
* Parse the raw stdin payload string for any adapter. Returns null when JSON
|
|
44
44
|
* parse fails (Cursor occasionally fires hooks with no payload).
|
|
45
45
|
*/
|
|
46
|
-
export function parsePayload(raw: string,
|
|
46
|
+
export function parsePayload(raw: string, adapter: Adapter): ParsedPayload | null {
|
|
47
47
|
if (!raw || raw.trim().length === 0) return null;
|
|
48
48
|
let json: Record<string, unknown>;
|
|
49
49
|
try {
|
|
@@ -52,13 +52,26 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const sessionId = normalizeSessionId(adapter, pickStr(json, "session_id"));
|
|
56
|
+
const conversationId = normalizeSessionId(adapter, pickStr(json, "conversation_id"));
|
|
57
|
+
const parentSessionId = normalizeSessionId(adapter, pickStr(json, "parent_session_id"));
|
|
58
|
+
const normalizedRaw =
|
|
59
|
+
adapter === "cursor"
|
|
60
|
+
? {
|
|
61
|
+
...json,
|
|
62
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
63
|
+
...(conversationId ? { conversation_id: conversationId } : {}),
|
|
64
|
+
...(parentSessionId ? { parent_session_id: parentSessionId } : {}),
|
|
65
|
+
}
|
|
66
|
+
: json;
|
|
67
|
+
|
|
55
68
|
return {
|
|
56
69
|
hook_event_name: pickStr(json, "hook_event_name"),
|
|
57
|
-
session_id:
|
|
70
|
+
session_id: sessionId,
|
|
58
71
|
agent_id: pickStr(json, "agent_id"),
|
|
59
72
|
subagent_id: pickStr(json, "subagent_id"),
|
|
60
|
-
conversation_id:
|
|
61
|
-
parent_session_id:
|
|
73
|
+
conversation_id: conversationId,
|
|
74
|
+
parent_session_id: parentSessionId,
|
|
62
75
|
turn_id: pickStr(json, "turn_id"),
|
|
63
76
|
parent_turn_id: pickStr(json, "parent_turn_id"),
|
|
64
77
|
transcript_path: pickStr(json, "transcript_path"),
|
|
@@ -75,10 +88,16 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
|
|
|
75
88
|
clean_exit: pickBool(json, "clean_exit"),
|
|
76
89
|
exit_status: pickStr(json, "exit_status"),
|
|
77
90
|
reason: pickStr(json, "reason"),
|
|
78
|
-
raw:
|
|
91
|
+
raw: normalizedRaw,
|
|
79
92
|
};
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
/** Cursor Glass prefixes conversation ids with `bc-`; coordination uses the bare id everywhere. */
|
|
96
|
+
function normalizeSessionId(adapter: Adapter, value: string | undefined): string | undefined {
|
|
97
|
+
if (adapter !== "cursor" || !value?.startsWith("bc-") || value.length <= 3) return value;
|
|
98
|
+
return value.slice(3);
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* Pull the bash command string out of a Bash/Shell tool_input. Returns
|
|
84
103
|
* undefined for non-shell tools.
|
|
@@ -200,6 +200,49 @@ export function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[] {
|
|
|
200
200
|
return out;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
export interface AdapterWiringSummary {
|
|
204
|
+
/** Adapters carrying at least one harnery hook. */
|
|
205
|
+
wired: AdapterId[];
|
|
206
|
+
/**
|
|
207
|
+
* Adapters carrying none: no settings file at all, or a settings file with
|
|
208
|
+
* none of ours in it. An unparseable file lands in neither list, since
|
|
209
|
+
* `loadAdapterWiring` already reports that as drift.
|
|
210
|
+
*/
|
|
211
|
+
unwired: AdapterId[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Classify every known adapter as wired or unwired, without judging whether an
|
|
216
|
+
* unwired one is a problem.
|
|
217
|
+
*
|
|
218
|
+
* `loadAdapterWiring` stays deliberately silent about an adapter with zero
|
|
219
|
+
* harnery hooks, because a bare settings file is not an opt-out signal and
|
|
220
|
+
* nagging about it would false-warn every session. That silence is right in
|
|
221
|
+
* isolation and wrong once you also know the adapter's CLI is installed: then
|
|
222
|
+
* an agent can start a session here and register nothing. Splitting the fact
|
|
223
|
+
* from the judgement lets a caller holding both (doctor) tell "not used here"
|
|
224
|
+
* from "never wired" without changing what counts as drift.
|
|
225
|
+
*/
|
|
226
|
+
export function summarizeAdapterWiring(projectRoot: string): AdapterWiringSummary {
|
|
227
|
+
const wired: AdapterId[] = [];
|
|
228
|
+
const unwired: AdapterId[] = [];
|
|
229
|
+
for (const [id, spec] of Object.entries(ADAPTER_SPECS) as [AdapterId, AdapterSpec][]) {
|
|
230
|
+
const settingsPath = resolve(projectRoot, spec.settingsFile);
|
|
231
|
+
if (!existsSync(settingsPath)) {
|
|
232
|
+
unwired.push(id);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
let settings: SettingsFile;
|
|
236
|
+
try {
|
|
237
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8")) as SettingsFile;
|
|
238
|
+
} catch {
|
|
239
|
+
continue; // unparseable → already surfaced by loadAdapterWiring
|
|
240
|
+
}
|
|
241
|
+
(diffWiring(settings, spec).present.length > 0 ? wired : unwired).push(id);
|
|
242
|
+
}
|
|
243
|
+
return { wired, unwired };
|
|
244
|
+
}
|
|
245
|
+
|
|
203
246
|
/**
|
|
204
247
|
* Resolve the harnery package version for context in nudges/checks. Walks up
|
|
205
248
|
* from this module to the package root (works under Bun from `src/` and Node
|
package/src/core/hooks/cli.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agent-hook` CLI entry point
|
|
3
|
-
* alongside the legacy stream.
|
|
2
|
+
* `agent-hook` CLI entry point for canonical event emission.
|
|
4
3
|
*
|
|
5
4
|
* Flow:
|
|
6
5
|
* 1. Parse argv → event-name + adapter.
|
|
@@ -25,10 +24,17 @@ import { coordEnv } from "../../lib/env.ts";
|
|
|
25
24
|
import { replayCodexJsonl } from "../agents/codex-replay.ts";
|
|
26
25
|
import { coordBinPath } from "../agents/coord-bin.ts";
|
|
27
26
|
import { consumeSince, writeCursor } from "../agents/events/consume.ts";
|
|
27
|
+
import {
|
|
28
|
+
type ClaimFinalizationDecision,
|
|
29
|
+
type ClaimFinalizationDescriptor,
|
|
30
|
+
classifyWriteClaimFinalization,
|
|
31
|
+
formatWriteClaimFinalizationDenial,
|
|
32
|
+
} from "../agents/finalization.ts";
|
|
28
33
|
import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
|
|
29
34
|
import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
|
|
30
35
|
import { writePidmapRow } from "../agents/state/pidmap.ts";
|
|
31
36
|
import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
|
|
37
|
+
import { agentsRequireGitFinalization, resolveBinName } from "../config.ts";
|
|
32
38
|
import {
|
|
33
39
|
checkpointContext,
|
|
34
40
|
completeContextRecovery,
|
|
@@ -49,6 +55,12 @@ import {
|
|
|
49
55
|
type ParsedPayload,
|
|
50
56
|
parsePayload,
|
|
51
57
|
} from "./adapter/parse.ts";
|
|
58
|
+
import {
|
|
59
|
+
codexWslFileLinkTelemetry,
|
|
60
|
+
inspectCodexWslBridge,
|
|
61
|
+
isWslUncPath,
|
|
62
|
+
renderCodexWslFileLinkContext,
|
|
63
|
+
} from "./codex-wsl-bridge.ts";
|
|
52
64
|
import {
|
|
53
65
|
captureImages,
|
|
54
66
|
detectPresence,
|
|
@@ -69,7 +81,14 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
|
|
|
69
81
|
import { findCoordRoot } from "./resolve/coord-root.ts";
|
|
70
82
|
import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
|
|
71
83
|
import { resolveOwner } from "./resolve/owner.ts";
|
|
72
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
detectForkParent,
|
|
86
|
+
scanAssistantTextIncludes,
|
|
87
|
+
scanStatusBoxPresent,
|
|
88
|
+
scanTranscriptModel,
|
|
89
|
+
} from "./resolve/transcript.ts";
|
|
90
|
+
import { sessionNamePresence } from "./session-name-presence.ts";
|
|
91
|
+
import { unsafeCrossShellReason } from "./unsafe-cross-shell.ts";
|
|
73
92
|
|
|
74
93
|
interface Argv {
|
|
75
94
|
eventName: string | null;
|
|
@@ -80,11 +99,11 @@ function parseArgv(argv: string[]): Argv {
|
|
|
80
99
|
const out: Argv = { eventName: null, extra: [] };
|
|
81
100
|
for (let i = 0; i < argv.length; i++) {
|
|
82
101
|
const arg = argv[i]!;
|
|
83
|
-
if (arg === "--adapter"
|
|
102
|
+
if (arg === "--adapter") {
|
|
84
103
|
i++; // detectAdapter will re-parse; just consume the value here.
|
|
85
104
|
continue;
|
|
86
105
|
}
|
|
87
|
-
if (arg.startsWith("--adapter=")
|
|
106
|
+
if (arg.startsWith("--adapter=")) continue;
|
|
88
107
|
if (!out.eventName && !arg.startsWith("--")) {
|
|
89
108
|
out.eventName = arg;
|
|
90
109
|
} else {
|
|
@@ -160,11 +179,14 @@ function assignNameViaAgentCoord(
|
|
|
160
179
|
coordRoot: string,
|
|
161
180
|
instanceId: string,
|
|
162
181
|
kind: "session" | "subagent" | "transient",
|
|
182
|
+
forkedFrom?: string,
|
|
163
183
|
): { name: string; kind: string } | null {
|
|
164
184
|
const binary = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
165
185
|
if (!existsSync(binary)) return null;
|
|
166
186
|
try {
|
|
167
|
-
const
|
|
187
|
+
const args = ["assign-name", instanceId, kind];
|
|
188
|
+
if (forkedFrom) args.push("--forked-from", forkedFrom);
|
|
189
|
+
const result = spawnSync(binary, args, {
|
|
168
190
|
encoding: "utf8",
|
|
169
191
|
timeout: 2000,
|
|
170
192
|
env: childEnv(coordRoot),
|
|
@@ -238,13 +260,26 @@ function buildEventData(
|
|
|
238
260
|
case "session.start": {
|
|
239
261
|
const adapterPlatform =
|
|
240
262
|
ctx.adapter === "claude-code"
|
|
241
|
-
? "
|
|
263
|
+
? "claude-code"
|
|
242
264
|
: ctx.adapter === "cursor"
|
|
243
265
|
? "cursor"
|
|
244
266
|
: "codex";
|
|
267
|
+
// Recorded fork lineage is NOT detected here. On claude-code a fork
|
|
268
|
+
// never fires its own session.start — SessionStart fires under the
|
|
269
|
+
// PARENT's session id (source=resume) before the fork id is minted
|
|
270
|
+
// (verified 2026-08-05) — so detection at this point can only mislabel
|
|
271
|
+
// the resumed parent. The fork's new instance is caught by the
|
|
272
|
+
// tool.pre_use heal path instead. The forkedFrom plumbing below stays
|
|
273
|
+
// for adapters that DO report a parent at session start.
|
|
274
|
+
const forkedFrom: string | undefined = undefined;
|
|
245
275
|
// Assign (or recover) name + kind via agent-coord. Idempotent: resume
|
|
246
276
|
// returns the original name; new owner consumes a counter slot.
|
|
247
|
-
const assigned = assignNameViaAgentCoord(
|
|
277
|
+
const assigned = assignNameViaAgentCoord(
|
|
278
|
+
ctx.coordRoot,
|
|
279
|
+
ctx.instanceId,
|
|
280
|
+
"session",
|
|
281
|
+
forkedFrom,
|
|
282
|
+
);
|
|
248
283
|
// Write the adapter pid-map row so `harn agents whoami` ppid-walks find
|
|
249
284
|
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
250
285
|
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
@@ -268,6 +303,10 @@ function buildEventData(
|
|
|
268
303
|
name: assigned?.name,
|
|
269
304
|
kind: "session",
|
|
270
305
|
agent_id: ctx.instanceId,
|
|
306
|
+
// Recorded fork lineage rides the canonical event too, so full replay
|
|
307
|
+
// and derived readers converge on the same parent linkage that
|
|
308
|
+
// .name-history carries (the ADR 0017 dual-write pattern).
|
|
309
|
+
...(forkedFrom ? { forked_from: forkedFrom } : {}),
|
|
271
310
|
// Workflow-child linkage: `workflow run` children carry the run id in
|
|
272
311
|
// env (spawn adapters set it via buildChildEnv), so their sessions and
|
|
273
312
|
// heartbeats join back to the run transcript + /workflows web view.
|
|
@@ -298,6 +337,11 @@ function buildEventData(
|
|
|
298
337
|
}
|
|
299
338
|
|
|
300
339
|
case "turn.stop": {
|
|
340
|
+
const lastAssistantMessage = (p?.raw.last_assistant_message as string | undefined) ?? "";
|
|
341
|
+
const fileLinkTelemetry =
|
|
342
|
+
ctx.adapter === "codex"
|
|
343
|
+
? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
|
|
344
|
+
: null;
|
|
301
345
|
return {
|
|
302
346
|
// Backfill the model for adapters that omit it at session.start
|
|
303
347
|
// (Claude Code). The transcript is populated with assistant turns by
|
|
@@ -316,8 +360,27 @@ function buildEventData(
|
|
|
316
360
|
// the verdict now sees because agent-hook emits this turn.stop itself
|
|
317
361
|
// (the previous path passed those via the no-history fail-open).
|
|
318
362
|
status_box_present:
|
|
319
|
-
scanStatusBoxPresent(p?.transcript_path) ||
|
|
320
|
-
|
|
363
|
+
scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
|
|
364
|
+
// Shadow measurement for the rule-2/3 detector: the loose scan above
|
|
365
|
+
// matches the box prefix ANYWHERE in the transcript tail, including
|
|
366
|
+
// the tool_result row the status command itself writes, so it can
|
|
367
|
+
// report a paste that never happened. The strict variant counts
|
|
368
|
+
// assistant text only (same window, same scanner as the session-name
|
|
369
|
+
// detection). Telemetry-only: the Stop verdict still reads
|
|
370
|
+
// status_box_present. Once the divergence rate between the two fields
|
|
371
|
+
// is known, the gate either flips to strict or the rule gets
|
|
372
|
+
// reconsidered; either way this field decides it with data.
|
|
373
|
+
status_box_present_strict:
|
|
374
|
+
scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
|
|
375
|
+
lastAssistantMessage.includes("┌─ agent-"),
|
|
376
|
+
// Whether the session's suggested name is satisfied as of this turn,
|
|
377
|
+
// and which name that refers to. Reported on every stop while a name
|
|
378
|
+
// exists (the stop-hook.session_name verdict reads it per turn); the
|
|
379
|
+
// transcript scan itself stops once the name has been sighted.
|
|
380
|
+
...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) =>
|
|
381
|
+
scanAssistantTextIncludes(p?.transcript_path, name),
|
|
382
|
+
),
|
|
383
|
+
...(fileLinkTelemetry ?? {}),
|
|
321
384
|
stop_hook_active: p?.stop_hook_active,
|
|
322
385
|
};
|
|
323
386
|
}
|
|
@@ -805,6 +868,7 @@ async function main(): Promise<number> {
|
|
|
805
868
|
owner.instance_id,
|
|
806
869
|
sessionId,
|
|
807
870
|
adapter,
|
|
871
|
+
payload?.cwd,
|
|
808
872
|
recovery?.briefing ?? "",
|
|
809
873
|
);
|
|
810
874
|
if (injected && recovery) {
|
|
@@ -897,7 +961,7 @@ async function main(): Promise<number> {
|
|
|
897
961
|
// re-prompts only via a `followup_message` it auto-submits. emitStopBlock
|
|
898
962
|
// writes the right shape and returns the exit code to use.
|
|
899
963
|
const { emitStopBlock } = await import("./adapter/output.ts");
|
|
900
|
-
return emitStopBlock(adapter, verdict);
|
|
964
|
+
return emitStopBlock(adapter, verdict, coordRoot);
|
|
901
965
|
}
|
|
902
966
|
}
|
|
903
967
|
|
|
@@ -912,7 +976,21 @@ async function main(): Promise<number> {
|
|
|
912
976
|
// call was restored here afterward.
|
|
913
977
|
if (norm.event_type === "tool.pre_use") {
|
|
914
978
|
try {
|
|
915
|
-
|
|
979
|
+
// Recorded fork lineage, heal-path flavor. A forked CC conversation
|
|
980
|
+
// never fires its own session.start (SessionStart fires under the
|
|
981
|
+
// PARENT's session id with source=resume, before the fork id is
|
|
982
|
+
// minted; verified 2026-08-05), so the fork's new instance first
|
|
983
|
+
// materializes right here. Gate detection on "no heartbeat yet" so the
|
|
984
|
+
// transcript scan runs once per instance lifetime, not per tool call.
|
|
985
|
+
let forkedFrom: string | undefined;
|
|
986
|
+
if (
|
|
987
|
+
adapter === "claude-code" &&
|
|
988
|
+
payload?.transcript_path &&
|
|
989
|
+
!existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))
|
|
990
|
+
) {
|
|
991
|
+
forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
|
|
992
|
+
}
|
|
993
|
+
healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
|
|
916
994
|
refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
|
|
917
995
|
} catch (err) {
|
|
918
996
|
logError(coordRoot, err, { phase: "pre-tool-use-heal" });
|
|
@@ -933,6 +1011,29 @@ async function main(): Promise<number> {
|
|
|
933
1011
|
logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
|
|
934
1012
|
}
|
|
935
1013
|
|
|
1014
|
+
// Windows-native Codex + WSL UNC only: block the one cross-shell shape
|
|
1015
|
+
// proven to corrupt argument boundaries. Normal WSL argv calls, literal
|
|
1016
|
+
// bash -s scripts, native Linux/macOS sessions, and every other adapter
|
|
1017
|
+
// pass through. The host instructions name its concrete safe bridge.
|
|
1018
|
+
const unsafeShellReason = unsafeCrossShellReason({
|
|
1019
|
+
adapter,
|
|
1020
|
+
cwd: payload?.cwd,
|
|
1021
|
+
toolName: payload?.tool_name,
|
|
1022
|
+
toolInput: payload?.tool_input,
|
|
1023
|
+
});
|
|
1024
|
+
if (unsafeShellReason) {
|
|
1025
|
+
emit(coordRoot, {
|
|
1026
|
+
event_type: "decision.block",
|
|
1027
|
+
instance_id: owner.instance_id,
|
|
1028
|
+
session_id: sessionId,
|
|
1029
|
+
adapter,
|
|
1030
|
+
data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
|
|
1031
|
+
});
|
|
1032
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1033
|
+
emitDeny(adapter, unsafeShellReason);
|
|
1034
|
+
return 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
936
1037
|
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
937
1038
|
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
938
1039
|
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
@@ -1033,13 +1134,44 @@ async function runPreToolUseGuard(
|
|
|
1033
1134
|
adapter: Adapter,
|
|
1034
1135
|
): Promise<void> {
|
|
1035
1136
|
const toolName = (data.tool_name as string | undefined) ?? "";
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1137
|
+
const rawTargets = collectGuardTargets(toolName, data);
|
|
1138
|
+
if (rawTargets.length === 0) return;
|
|
1139
|
+
|
|
1140
|
+
let targets: Array<{ path: string; finalization?: ClaimFinalizationDescriptor }>;
|
|
1141
|
+
if (agentsRequireGitFinalization(coordRoot)) {
|
|
1142
|
+
const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
|
|
1143
|
+
const denied = decisions.find((decision) => !decision.allow);
|
|
1144
|
+
if (denied && !denied.allow) {
|
|
1145
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1146
|
+
emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const allowed = decisions.filter(
|
|
1150
|
+
(decision): decision is Extract<ClaimFinalizationDecision, { allow: true }> => decision.allow,
|
|
1151
|
+
);
|
|
1152
|
+
targets = allowed.map((decision) => ({
|
|
1153
|
+
path: decision.path,
|
|
1154
|
+
finalization: decision.descriptor,
|
|
1155
|
+
}));
|
|
1156
|
+
} else {
|
|
1157
|
+
targets = rawTargets
|
|
1158
|
+
.map((path) => canonicalize(coordRoot, path))
|
|
1159
|
+
.filter((path): path is string => path !== null)
|
|
1160
|
+
.map((path) => ({ path }));
|
|
1161
|
+
}
|
|
1039
1162
|
if (targets.length === 0) return;
|
|
1040
1163
|
|
|
1041
1164
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
1042
|
-
if (!existsSync(agentCoordBin))
|
|
1165
|
+
if (!existsSync(agentCoordBin)) {
|
|
1166
|
+
if (agentsRequireGitFinalization(coordRoot)) {
|
|
1167
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1168
|
+
emitDeny(
|
|
1169
|
+
adapter,
|
|
1170
|
+
"Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.",
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1043
1175
|
|
|
1044
1176
|
// For apply_patch (multi-file), collect siblings so the deny reason names
|
|
1045
1177
|
// them. For single-file tools the array has one entry.
|
|
@@ -1048,7 +1180,7 @@ async function runPreToolUseGuard(
|
|
|
1048
1180
|
rule: "claim",
|
|
1049
1181
|
instance_id: instanceId,
|
|
1050
1182
|
session_id: sessionId,
|
|
1051
|
-
path: target,
|
|
1183
|
+
path: target.path,
|
|
1052
1184
|
});
|
|
1053
1185
|
const result = spawnSync(agentCoordBin, ["verdict"], {
|
|
1054
1186
|
input: verdictReq,
|
|
@@ -1064,10 +1196,12 @@ async function runPreToolUseGuard(
|
|
|
1064
1196
|
continue;
|
|
1065
1197
|
}
|
|
1066
1198
|
if (parsed.allow === false) {
|
|
1067
|
-
let reason =
|
|
1199
|
+
let reason =
|
|
1200
|
+
parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
|
|
1068
1201
|
if (targets.length > 1) {
|
|
1069
1202
|
const siblings = targets
|
|
1070
|
-
.filter((
|
|
1203
|
+
.filter((candidate) => candidate !== target)
|
|
1204
|
+
.map((candidate) => candidate.path)
|
|
1071
1205
|
.slice(0, 3)
|
|
1072
1206
|
.join(", ");
|
|
1073
1207
|
if (siblings) {
|
|
@@ -1078,6 +1212,24 @@ async function runPreToolUseGuard(
|
|
|
1078
1212
|
emitDeny(adapter, reason);
|
|
1079
1213
|
return;
|
|
1080
1214
|
}
|
|
1215
|
+
|
|
1216
|
+
// The verdict mutates the live heartbeat immediately, but that file is a
|
|
1217
|
+
// projection and can be rebuilt at any later hook. Persist the acquisition
|
|
1218
|
+
// in the canonical stream too. This matters most for Codex apply_patch:
|
|
1219
|
+
// one tool call can claim several files, and tool.pre_use's clamped payload
|
|
1220
|
+
// is not a durable ownership representation for those targets.
|
|
1221
|
+
emit(coordRoot, {
|
|
1222
|
+
event_type: "claim.acquire",
|
|
1223
|
+
instance_id: instanceId,
|
|
1224
|
+
session_id: sessionId,
|
|
1225
|
+
adapter,
|
|
1226
|
+
source: "agent-hooks",
|
|
1227
|
+
data: {
|
|
1228
|
+
path: target.path,
|
|
1229
|
+
mode: "write",
|
|
1230
|
+
...(target.finalization ? { finalization: target.finalization } : {}),
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1081
1233
|
}
|
|
1082
1234
|
}
|
|
1083
1235
|
|
|
@@ -1142,14 +1294,18 @@ function healHeartbeatViaCli(
|
|
|
1142
1294
|
instanceId: string,
|
|
1143
1295
|
sessionId: string,
|
|
1144
1296
|
adapter: string,
|
|
1297
|
+
forkedFrom?: string,
|
|
1145
1298
|
): void {
|
|
1146
1299
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
1147
1300
|
if (!existsSync(agentCoordBin)) return;
|
|
1148
1301
|
// Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
|
|
1149
1302
|
// with the correct platform; without it, healHeartbeat defaults to
|
|
1150
|
-
//
|
|
1151
|
-
// heartbeat-writer.healHeartbeat.
|
|
1152
|
-
|
|
1303
|
+
// claude-code and the dashboard mislabels the agent. See
|
|
1304
|
+
// heartbeat-writer.healHeartbeat. --forked-from carries detected fork
|
|
1305
|
+
// lineage for an instance that never session.started (the CC fork flow).
|
|
1306
|
+
const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
|
|
1307
|
+
if (forkedFrom) args.push(`--forked-from=${forkedFrom}`);
|
|
1308
|
+
spawnSync(agentCoordBin, args, {
|
|
1153
1309
|
encoding: "utf8",
|
|
1154
1310
|
timeout: 2000,
|
|
1155
1311
|
env: childEnv(coordRoot),
|
|
@@ -1384,6 +1540,7 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
1384
1540
|
instanceId: string,
|
|
1385
1541
|
sessionId: string,
|
|
1386
1542
|
adapter: Adapter,
|
|
1543
|
+
workspaceCwd?: string,
|
|
1387
1544
|
recoveryBriefing = "",
|
|
1388
1545
|
): Promise<boolean> {
|
|
1389
1546
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
@@ -1416,6 +1573,12 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
1416
1573
|
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1417
1574
|
if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
|
|
1418
1575
|
}
|
|
1576
|
+
if (adapter === "codex") {
|
|
1577
|
+
const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
|
|
1578
|
+
if (fileLinkContext) {
|
|
1579
|
+
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1419
1582
|
if (recoveryBriefing) {
|
|
1420
1583
|
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1421
1584
|
}
|
|
@@ -1466,7 +1629,7 @@ function emitSubagentStartContext(
|
|
|
1466
1629
|
}
|
|
1467
1630
|
|
|
1468
1631
|
function adapterPlatform(adapter: Adapter): string {
|
|
1469
|
-
if (adapter === "claude-code") return "
|
|
1632
|
+
if (adapter === "claude-code") return "claude-code";
|
|
1470
1633
|
return adapter;
|
|
1471
1634
|
}
|
|
1472
1635
|
|
|
@@ -1527,6 +1690,21 @@ async function emitSessionStartSystemMessage(
|
|
|
1527
1690
|
if (recoveryBriefing) {
|
|
1528
1691
|
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1529
1692
|
}
|
|
1693
|
+
if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
|
|
1694
|
+
const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
|
|
1695
|
+
if (fileLinkContext) {
|
|
1696
|
+
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1697
|
+
}
|
|
1698
|
+
const bridge = inspectCodexWslBridge(process.env, { expected: true });
|
|
1699
|
+
if (bridge && !bridge.ok) {
|
|
1700
|
+
additionalContext = [
|
|
1701
|
+
additionalContext,
|
|
1702
|
+
`Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
|
|
1703
|
+
]
|
|
1704
|
+
.filter(Boolean)
|
|
1705
|
+
.join("\n\n");
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1530
1708
|
if (!additionalContext) return false;
|
|
1531
1709
|
|
|
1532
1710
|
const { emitContext } = await import("./adapter/output.ts");
|