harnery 0.31.6 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +5 -1
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +280 -94
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/claude-desktop.d.ts +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +74 -3
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +141 -19
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -5
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +4 -1
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +2 -3
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +6 -5
- package/dist/core/agents/cli.js +128 -89
- package/dist/core/agents/coord-client.d.ts +18 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/render/prompt-context.d.ts +12 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +70 -16
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +23 -4
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +98 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +86 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/agents/state/session-state.d.ts +44 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +81 -0
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/parse.d.ts +1 -1
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +2 -0
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +258 -32
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +57 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/presence/blob.d.ts +4 -0
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +12 -0
- package/dist/core/presence/index.js +15 -1
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/claude-desktop.d.ts +47 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +169 -1
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/bundle.d.ts +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +2 -2
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +5 -1
- package/src/commands/agents.ts +341 -113
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +155 -21
- package/src/commands/init.ts +28 -9
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +7 -6
- package/src/core/agents/cli.ts +143 -89
- package/src/core/agents/coord-client.ts +21 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +96 -16
- package/src/core/agents/render/session-context.ts +29 -4
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +108 -1
- package/src/core/agents/state/heartbeat-writer.ts +129 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +296 -27
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +71 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/bundle.ts +199 -0
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-aware Git completion checks for the end-of-turn status ritual.
|
|
3
|
+
*
|
|
4
|
+
* The heartbeat's durable `files_touched` ledger is the ownership boundary. We
|
|
5
|
+
* inspect only those paths, plus any enclosing submodule gitlinks, so a peer's
|
|
6
|
+
* active edits elsewhere in a shared checkout do not block this session.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
|
+
import { type AgentFinalizationDisposition, agentsFinalizationRoots } from "../config.ts";
|
|
13
|
+
import { readStreamTailBounded } from "./events/consume.ts";
|
|
14
|
+
|
|
15
|
+
const CLAIM_HISTORY_CAP_BYTES = 128 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
const GIT_DISCOVERY_VARS = [
|
|
18
|
+
"GIT_DIR",
|
|
19
|
+
"GIT_WORK_TREE",
|
|
20
|
+
"GIT_COMMON_DIR",
|
|
21
|
+
"GIT_INDEX_FILE",
|
|
22
|
+
"GIT_OBJECT_DIRECTORY",
|
|
23
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
24
|
+
"GIT_PREFIX",
|
|
25
|
+
"GIT_NAMESPACE",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
interface GitResult {
|
|
29
|
+
status: number;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RepoWork {
|
|
35
|
+
root: string;
|
|
36
|
+
paths: Set<string>;
|
|
37
|
+
gitlinks: Set<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GitFinalizationResult {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
claim_history_complete: boolean;
|
|
43
|
+
dirty_paths: string[];
|
|
44
|
+
unpushed_repos: string[];
|
|
45
|
+
host_output_paths: string[];
|
|
46
|
+
unsupported_paths: UnsupportedFinalizationPath[];
|
|
47
|
+
unverifiable_paths: string[];
|
|
48
|
+
unverifiable_repos: string[];
|
|
49
|
+
repos_checked: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type UnsupportedFinalizationReason =
|
|
53
|
+
| "outside_finalization_roots"
|
|
54
|
+
| "invalid_finalization_root"
|
|
55
|
+
| "ambiguous_finalization_roots"
|
|
56
|
+
| "path_escapes_finalization_root"
|
|
57
|
+
| "git_repository_unavailable"
|
|
58
|
+
| "output_path_is_git";
|
|
59
|
+
|
|
60
|
+
export interface UnsupportedFinalizationPath {
|
|
61
|
+
path: string;
|
|
62
|
+
reason: UnsupportedFinalizationReason;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ClaimFinalizationDescriptor {
|
|
66
|
+
disposition: AgentFinalizationDisposition;
|
|
67
|
+
root: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ClaimFinalizationDecision =
|
|
71
|
+
| {
|
|
72
|
+
allow: true;
|
|
73
|
+
path: string;
|
|
74
|
+
descriptor: ClaimFinalizationDescriptor;
|
|
75
|
+
authorityRoot: string;
|
|
76
|
+
repo?: { root: string; path: string };
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
allow: false;
|
|
80
|
+
path: string;
|
|
81
|
+
reason: UnsupportedFinalizationReason;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export interface SessionWriteClaims {
|
|
85
|
+
paths: string[];
|
|
86
|
+
complete: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read every write claim made by this session, including claims released after
|
|
91
|
+
* a commit. Current heartbeat claims describe dirty ownership; this history is
|
|
92
|
+
* what keeps the touched repository in scope until its commits are pushed.
|
|
93
|
+
*/
|
|
94
|
+
export function readSessionWriteClaims(
|
|
95
|
+
coordRoot: string,
|
|
96
|
+
instanceId: string,
|
|
97
|
+
sessionId: string,
|
|
98
|
+
): SessionWriteClaims {
|
|
99
|
+
const streamPath = join(coordRoot, ".harnery", "events.ndjson");
|
|
100
|
+
const { text, truncated } = readStreamTailBounded(streamPath, CLAIM_HISTORY_CAP_BYTES);
|
|
101
|
+
const paths = new Set<string>();
|
|
102
|
+
let sawSessionStart = false;
|
|
103
|
+
|
|
104
|
+
for (const line of text.split("\n")) {
|
|
105
|
+
if (line.trim().length === 0) continue;
|
|
106
|
+
try {
|
|
107
|
+
const event = JSON.parse(line) as {
|
|
108
|
+
event_type?: string;
|
|
109
|
+
instance_id?: string;
|
|
110
|
+
session_id?: string;
|
|
111
|
+
data?: { path?: unknown; mode?: unknown };
|
|
112
|
+
};
|
|
113
|
+
if (event.instance_id !== instanceId || event.session_id !== sessionId) continue;
|
|
114
|
+
if (event.event_type === "session.start") sawSessionStart = true;
|
|
115
|
+
if (
|
|
116
|
+
event.event_type === "claim.acquire" &&
|
|
117
|
+
event.data?.mode === "write" &&
|
|
118
|
+
typeof event.data.path === "string"
|
|
119
|
+
) {
|
|
120
|
+
paths.add(event.data.path);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore malformed and crash-truncated event lines, matching the
|
|
124
|
+
// canonical stream consumer's recovery behavior.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { paths: [...paths], complete: !truncated || sawSessionStart };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function git(args: string[], cwd: string): GitResult {
|
|
132
|
+
const env = { ...process.env };
|
|
133
|
+
for (const name of GIT_DISCOVERY_VARS) delete env[name];
|
|
134
|
+
const result = spawnSync("git", args, { cwd, env, encoding: "utf8", timeout: 5_000 });
|
|
135
|
+
return {
|
|
136
|
+
status: result.status ?? 1,
|
|
137
|
+
stdout: result.stdout ?? "",
|
|
138
|
+
stderr: result.stderr ?? result.error?.message ?? "",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function repoLabel(coordRoot: string, repoRoot: string): string {
|
|
143
|
+
const rel = relative(coordRoot, repoRoot).replaceAll("\\", "/");
|
|
144
|
+
return rel.length === 0 ? "." : rel;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
148
|
+
const rel = relative(root, candidate);
|
|
149
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function existingProbeDir(authorityRoot: string, absolutePath: string): string | null {
|
|
153
|
+
let current = absolutePath;
|
|
154
|
+
if (existsSync(current) && !statSync(current).isDirectory()) current = dirname(current);
|
|
155
|
+
while (!existsSync(current)) {
|
|
156
|
+
const parent = dirname(current);
|
|
157
|
+
if (parent === current || !pathInside(authorityRoot, parent)) return null;
|
|
158
|
+
current = parent;
|
|
159
|
+
}
|
|
160
|
+
return pathInside(authorityRoot, current) ? current : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolve existing ancestors through symlinks while preserving a missing tail. */
|
|
164
|
+
function physicalPath(absolutePath: string): string | null {
|
|
165
|
+
let existing = absolutePath;
|
|
166
|
+
while (!existsSync(existing)) {
|
|
167
|
+
const parent = dirname(existing);
|
|
168
|
+
if (parent === existing) return null;
|
|
169
|
+
existing = parent;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const physical = realpathSync.native(existing);
|
|
173
|
+
const tail = relative(existing, absolutePath);
|
|
174
|
+
return resolve(physical, tail);
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Translate a Windows-native WSL UNC claim back to the Linux path seen by the
|
|
182
|
+
* coordination process. Codex can report `\\\\wsl.localhost\\<distro>\\...`
|
|
183
|
+
* while Harnery runs inside that distro with a `/...` coord root.
|
|
184
|
+
*/
|
|
185
|
+
function resolveHeldPath(coordRoot: string, heldPath: string): string {
|
|
186
|
+
const wslUnc = /^\\\\(?:wsl\.localhost|wsl\$)\\[^\\]+\\(.+)$/i.exec(heldPath);
|
|
187
|
+
if (wslUnc) {
|
|
188
|
+
const linuxPath = `/${wslUnc[1]!.replaceAll("\\", "/")}`;
|
|
189
|
+
const slashRoot = coordRoot.replaceAll("\\", "/");
|
|
190
|
+
if (slashRoot.startsWith("/") || pathInside(coordRoot, linuxPath)) return linuxPath;
|
|
191
|
+
}
|
|
192
|
+
return resolve(coordRoot, heldPath);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function discoverRepo(
|
|
196
|
+
authorityRoot: string,
|
|
197
|
+
absolutePath: string,
|
|
198
|
+
): { root: string; path: string } | null {
|
|
199
|
+
const probeDir = existingProbeDir(authorityRoot, absolutePath);
|
|
200
|
+
if (!probeDir) return null;
|
|
201
|
+
const rootResult = git(["rev-parse", "--show-toplevel"], probeDir);
|
|
202
|
+
if (rootResult.status !== 0) return null;
|
|
203
|
+
const repoRoot = physicalPath(resolve(rootResult.stdout.trim()));
|
|
204
|
+
if (!repoRoot || !pathInside(authorityRoot, repoRoot) || !pathInside(repoRoot, absolutePath)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const repoPath = relative(repoRoot, absolutePath).replaceAll("\\", "/") || ".";
|
|
208
|
+
return { root: repoRoot, path: repoPath };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface FinalizationRoot {
|
|
212
|
+
configuredPath: string;
|
|
213
|
+
disposition: AgentFinalizationDisposition;
|
|
214
|
+
lexicalRoot: string;
|
|
215
|
+
physicalRoot: string | null;
|
|
216
|
+
invalid: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function rootIsGitRepository(root: string): boolean {
|
|
220
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) return false;
|
|
221
|
+
const result = git(["rev-parse", "--show-toplevel"], root);
|
|
222
|
+
if (result.status !== 0) return false;
|
|
223
|
+
const discovered = physicalPath(resolve(result.stdout.trim()));
|
|
224
|
+
const physicalRoot = physicalPath(root);
|
|
225
|
+
return discovered !== null && physicalRoot !== null && discovered === physicalRoot;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function outputRootIsInsideGit(root: string): boolean {
|
|
229
|
+
let probe = root;
|
|
230
|
+
while (!existsSync(probe)) {
|
|
231
|
+
const parent = dirname(probe);
|
|
232
|
+
if (parent === probe) return false;
|
|
233
|
+
probe = parent;
|
|
234
|
+
}
|
|
235
|
+
if (!statSync(probe).isDirectory()) probe = dirname(probe);
|
|
236
|
+
return git(["rev-parse", "--show-toplevel"], probe).status === 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function finalizationRoots(coordRoot: string): FinalizationRoot[] {
|
|
240
|
+
const root = resolve(coordRoot);
|
|
241
|
+
const configured = agentsFinalizationRoots(root).map((entry) => {
|
|
242
|
+
const lexicalRoot = resolveHeldPath(root, entry.path);
|
|
243
|
+
const physicalRoot = physicalPath(lexicalRoot);
|
|
244
|
+
const invalid =
|
|
245
|
+
physicalRoot === null ||
|
|
246
|
+
(entry.disposition === "git"
|
|
247
|
+
? !rootIsGitRepository(physicalRoot)
|
|
248
|
+
: outputRootIsInsideGit(physicalRoot));
|
|
249
|
+
return {
|
|
250
|
+
configuredPath: entry.path,
|
|
251
|
+
disposition: entry.disposition,
|
|
252
|
+
lexicalRoot,
|
|
253
|
+
physicalRoot,
|
|
254
|
+
invalid,
|
|
255
|
+
} satisfies FinalizationRoot;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const rootPhysical = physicalPath(root);
|
|
259
|
+
if (rootPhysical && rootIsGitRepository(rootPhysical)) {
|
|
260
|
+
configured.push({
|
|
261
|
+
configuredPath: ".",
|
|
262
|
+
disposition: "git",
|
|
263
|
+
lexicalRoot: root,
|
|
264
|
+
physicalRoot: rootPhysical,
|
|
265
|
+
invalid: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const current of configured) {
|
|
270
|
+
if (!current.physicalRoot) continue;
|
|
271
|
+
for (const peer of configured) {
|
|
272
|
+
if (current === peer || !peer.physicalRoot || current.disposition === peer.disposition) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
pathInside(current.physicalRoot, peer.physicalRoot) ||
|
|
277
|
+
pathInside(peer.physicalRoot, current.physicalRoot)
|
|
278
|
+
) {
|
|
279
|
+
current.invalid = true;
|
|
280
|
+
peer.invalid = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return configured;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolve one guarded write to the host's single supported end-turn
|
|
290
|
+
* disposition. Claim event data never grants authority: every call starts from
|
|
291
|
+
* the project-owned finalization roots and only then inspects the selected
|
|
292
|
+
* repository or output tree.
|
|
293
|
+
*/
|
|
294
|
+
export function classifyWriteClaimFinalization(
|
|
295
|
+
coordRoot: string,
|
|
296
|
+
heldPath: string,
|
|
297
|
+
): ClaimFinalizationDecision {
|
|
298
|
+
const root = resolve(coordRoot);
|
|
299
|
+
const absolutePath = resolveHeldPath(root, heldPath);
|
|
300
|
+
const roots = finalizationRoots(root);
|
|
301
|
+
const lexicalMatches = roots.filter((entry) => pathInside(entry.lexicalRoot, absolutePath));
|
|
302
|
+
if (lexicalMatches.length === 0) {
|
|
303
|
+
return { allow: false, path: heldPath, reason: "outside_finalization_roots" };
|
|
304
|
+
}
|
|
305
|
+
if (lexicalMatches.some((entry) => entry.invalid)) {
|
|
306
|
+
return { allow: false, path: heldPath, reason: "invalid_finalization_root" };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const physical = physicalPath(absolutePath);
|
|
310
|
+
if (!physical) {
|
|
311
|
+
return { allow: false, path: heldPath, reason: "path_escapes_finalization_root" };
|
|
312
|
+
}
|
|
313
|
+
const matches = lexicalMatches.filter(
|
|
314
|
+
(entry) => entry.physicalRoot && pathInside(entry.physicalRoot, physical),
|
|
315
|
+
);
|
|
316
|
+
if (matches.length === 0) {
|
|
317
|
+
return { allow: false, path: heldPath, reason: "path_escapes_finalization_root" };
|
|
318
|
+
}
|
|
319
|
+
const dispositions = new Set(matches.map((entry) => entry.disposition));
|
|
320
|
+
if (dispositions.size !== 1) {
|
|
321
|
+
return { allow: false, path: heldPath, reason: "ambiguous_finalization_roots" };
|
|
322
|
+
}
|
|
323
|
+
const selected = [...matches].sort(
|
|
324
|
+
(a, b) => (b.physicalRoot?.length ?? 0) - (a.physicalRoot?.length ?? 0),
|
|
325
|
+
)[0]!;
|
|
326
|
+
const normalizedPath = relative(root, absolutePath).replaceAll("\\", "/") || ".";
|
|
327
|
+
const descriptor: ClaimFinalizationDescriptor = {
|
|
328
|
+
disposition: selected.disposition,
|
|
329
|
+
root: selected.configuredPath,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (selected.disposition === "output") {
|
|
333
|
+
const probe = existingProbeDir(selected.physicalRoot!, physical);
|
|
334
|
+
if (probe && git(["rev-parse", "--show-toplevel"], probe).status === 0) {
|
|
335
|
+
return { allow: false, path: heldPath, reason: "output_path_is_git" };
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
allow: true,
|
|
339
|
+
path: normalizedPath,
|
|
340
|
+
descriptor,
|
|
341
|
+
authorityRoot: selected.physicalRoot!,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const repo = discoverRepo(selected.physicalRoot!, physical);
|
|
346
|
+
if (!repo) {
|
|
347
|
+
return { allow: false, path: heldPath, reason: "git_repository_unavailable" };
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
allow: true,
|
|
351
|
+
path: normalizedPath,
|
|
352
|
+
descriptor,
|
|
353
|
+
authorityRoot: selected.physicalRoot!,
|
|
354
|
+
repo,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function addRepoPath(
|
|
359
|
+
repos: Map<string, RepoWork>,
|
|
360
|
+
root: string,
|
|
361
|
+
path: string,
|
|
362
|
+
gitlink = false,
|
|
363
|
+
): void {
|
|
364
|
+
const work = repos.get(root) ?? {
|
|
365
|
+
root,
|
|
366
|
+
paths: new Set<string>(),
|
|
367
|
+
gitlinks: new Set<string>(),
|
|
368
|
+
};
|
|
369
|
+
work.paths.add(path);
|
|
370
|
+
if (gitlink) work.gitlinks.add(path);
|
|
371
|
+
repos.set(root, work);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function addSuperprojects(
|
|
375
|
+
authorityRoot: string,
|
|
376
|
+
repos: Map<string, RepoWork>,
|
|
377
|
+
leafRoot: string,
|
|
378
|
+
): void {
|
|
379
|
+
let childRoot = leafRoot;
|
|
380
|
+
for (;;) {
|
|
381
|
+
const result = git(["rev-parse", "--show-superproject-working-tree"], childRoot);
|
|
382
|
+
if (result.status !== 0 || result.stdout.trim().length === 0) return;
|
|
383
|
+
const parentRoot = physicalPath(resolve(result.stdout.trim()));
|
|
384
|
+
if (!parentRoot || !pathInside(authorityRoot, parentRoot) || parentRoot === childRoot) return;
|
|
385
|
+
const gitlink = relative(parentRoot, childRoot).replaceAll("\\", "/");
|
|
386
|
+
addRepoPath(repos, parentRoot, gitlink, true);
|
|
387
|
+
childRoot = parentRoot;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function repoHasRemote(repoRoot: string): boolean {
|
|
392
|
+
const result = git(["remote"], repoRoot);
|
|
393
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function repoSyncState(repoRoot: string): "synced" | "unpushed" | "unverifiable" {
|
|
397
|
+
if (!repoHasRemote(repoRoot)) return "synced";
|
|
398
|
+
if (git(["rev-parse", "--verify", "HEAD"], repoRoot).status !== 0) return "unverifiable";
|
|
399
|
+
|
|
400
|
+
const upstream = git(
|
|
401
|
+
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
402
|
+
repoRoot,
|
|
403
|
+
);
|
|
404
|
+
if (upstream.status === 0) {
|
|
405
|
+
const ahead = git(["rev-list", "--count", "@{upstream}..HEAD"], repoRoot);
|
|
406
|
+
if (ahead.status !== 0) return "unverifiable";
|
|
407
|
+
return Number.parseInt(ahead.stdout.trim(), 10) > 0 ? "unpushed" : "synced";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Detached submodules commonly have no upstream. A remote ref containing
|
|
411
|
+
// HEAD proves that commit is available elsewhere without requiring a branch.
|
|
412
|
+
const containing = git(
|
|
413
|
+
["branch", "--remotes", "--contains", "HEAD", "--format=%(refname:short)"],
|
|
414
|
+
repoRoot,
|
|
415
|
+
);
|
|
416
|
+
if (containing.status !== 0) return "unverifiable";
|
|
417
|
+
return containing.stdout.trim().length > 0 ? "synced" : "unpushed";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Compare gitlink commits without treating dirty contents inside the child as a pointer change. */
|
|
421
|
+
function gitlinkIsDirty(parentRoot: string, gitlink: string): boolean {
|
|
422
|
+
const head = git(["rev-parse", `HEAD:${gitlink}`], parentRoot);
|
|
423
|
+
const index = git(["ls-files", "--stage", "--", gitlink], parentRoot);
|
|
424
|
+
const child = git(["rev-parse", "HEAD"], join(parentRoot, gitlink));
|
|
425
|
+
if (head.status !== 0 || index.status !== 0 || child.status !== 0) return true;
|
|
426
|
+
const indexMatch = /^160000 ([0-9a-f]{40}) 0\t/.exec(index.stdout.trim());
|
|
427
|
+
if (!indexMatch) return true;
|
|
428
|
+
return head.stdout.trim() !== indexMatch[1] || child.stdout.trim() !== indexMatch[1];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Check that this session's held paths are committed and their repositories are pushed. */
|
|
432
|
+
export function checkGitFinalization(
|
|
433
|
+
coordRoot: string,
|
|
434
|
+
heldPaths: readonly string[],
|
|
435
|
+
options: { claimHistoryComplete?: boolean } = {},
|
|
436
|
+
): GitFinalizationResult {
|
|
437
|
+
const root = resolve(coordRoot);
|
|
438
|
+
const repos = new Map<string, RepoWork>();
|
|
439
|
+
const hostOutputPaths = new Set<string>();
|
|
440
|
+
const unsupportedPaths: UnsupportedFinalizationPath[] = [];
|
|
441
|
+
const unverifiablePaths: string[] = [];
|
|
442
|
+
|
|
443
|
+
for (const heldPath of heldPaths) {
|
|
444
|
+
const decision = classifyWriteClaimFinalization(root, heldPath);
|
|
445
|
+
if (!decision.allow) {
|
|
446
|
+
unsupportedPaths.push({ path: heldPath, reason: decision.reason });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (decision.descriptor.disposition === "output") {
|
|
450
|
+
hostOutputPaths.add(decision.path);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (!decision.repo) {
|
|
454
|
+
unsupportedPaths.push({ path: heldPath, reason: "git_repository_unavailable" });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
addRepoPath(repos, decision.repo.root, decision.repo.path);
|
|
458
|
+
addSuperprojects(decision.authorityRoot, repos, decision.repo.root);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const dirtyPaths = new Set<string>();
|
|
462
|
+
const unpushedRepos: string[] = [];
|
|
463
|
+
const unverifiableRepos: string[] = [];
|
|
464
|
+
|
|
465
|
+
for (const work of repos.values()) {
|
|
466
|
+
for (const repoPath of work.paths) {
|
|
467
|
+
if (work.gitlinks.has(repoPath)) {
|
|
468
|
+
if (gitlinkIsDirty(work.root, repoPath)) {
|
|
469
|
+
const label = repoLabel(root, work.root);
|
|
470
|
+
dirtyPaths.add(label === "." ? repoPath : `${label}/${repoPath}`);
|
|
471
|
+
}
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const status = git(
|
|
475
|
+
["status", "--porcelain=v1", "--untracked-files=all", "--", repoPath],
|
|
476
|
+
work.root,
|
|
477
|
+
);
|
|
478
|
+
if (status.status !== 0) {
|
|
479
|
+
unverifiablePaths.push(join(repoLabel(root, work.root), repoPath).replaceAll("\\", "/"));
|
|
480
|
+
} else if (status.stdout.length > 0) {
|
|
481
|
+
const label = repoLabel(root, work.root);
|
|
482
|
+
dirtyPaths.add(label === "." ? repoPath : `${label}/${repoPath}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const syncState = repoSyncState(work.root);
|
|
487
|
+
const label = repoLabel(root, work.root);
|
|
488
|
+
if (syncState === "unpushed") unpushedRepos.push(label);
|
|
489
|
+
if (syncState === "unverifiable") unverifiableRepos.push(label);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const result: GitFinalizationResult = {
|
|
493
|
+
ok: false,
|
|
494
|
+
claim_history_complete: options.claimHistoryComplete ?? true,
|
|
495
|
+
dirty_paths: [...dirtyPaths].sort(),
|
|
496
|
+
unpushed_repos: [...new Set(unpushedRepos)].sort(),
|
|
497
|
+
host_output_paths: [...hostOutputPaths].sort(),
|
|
498
|
+
unsupported_paths: unsupportedPaths.sort(
|
|
499
|
+
(a, b) => a.path.localeCompare(b.path) || a.reason.localeCompare(b.reason),
|
|
500
|
+
),
|
|
501
|
+
unverifiable_paths: [...new Set(unverifiablePaths)].sort(),
|
|
502
|
+
unverifiable_repos: [...new Set(unverifiableRepos)].sort(),
|
|
503
|
+
repos_checked: [...repos.keys()].map((repo) => repoLabel(root, repo)).sort(),
|
|
504
|
+
};
|
|
505
|
+
result.ok =
|
|
506
|
+
result.claim_history_complete &&
|
|
507
|
+
result.dirty_paths.length === 0 &&
|
|
508
|
+
result.unpushed_repos.length === 0 &&
|
|
509
|
+
result.unsupported_paths.length === 0 &&
|
|
510
|
+
result.unverifiable_paths.length === 0 &&
|
|
511
|
+
result.unverifiable_repos.length === 0;
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function formatGitFinalizationFailure(result: GitFinalizationResult, bin: string): string {
|
|
516
|
+
const lines = ["Owned work is not finalized; the status box was not issued."];
|
|
517
|
+
if (!result.claim_history_complete) {
|
|
518
|
+
lines.push("This session's write-claim history is older than the bounded event window.");
|
|
519
|
+
}
|
|
520
|
+
if (result.dirty_paths.length > 0) {
|
|
521
|
+
lines.push("Dirty owned paths:", ...result.dirty_paths.map((path) => ` - ${path}`));
|
|
522
|
+
}
|
|
523
|
+
if (result.unpushed_repos.length > 0) {
|
|
524
|
+
lines.push(
|
|
525
|
+
"Repositories with commits not on their remote:",
|
|
526
|
+
...result.unpushed_repos.map((repo) => ` - ${repo}`),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (result.unsupported_paths.length > 0) {
|
|
530
|
+
lines.push(
|
|
531
|
+
"Paths without an authorized end-turn disposition:",
|
|
532
|
+
...result.unsupported_paths.map(
|
|
533
|
+
({ path, reason }) => ` - ${path}: ${unsupportedReasonMessage(reason)}`,
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (result.unverifiable_paths.length > 0) {
|
|
538
|
+
lines.push(
|
|
539
|
+
"Paths whose Git state could not be verified:",
|
|
540
|
+
...result.unverifiable_paths.map((path) => ` - ${path}`),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
if (result.unverifiable_repos.length > 0) {
|
|
544
|
+
lines.push(
|
|
545
|
+
"Repositories whose remote state could not be verified:",
|
|
546
|
+
...result.unverifiable_repos.map((repo) => ` - ${repo}`),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (
|
|
550
|
+
result.dirty_paths.length > 0 ||
|
|
551
|
+
result.unpushed_repos.length > 0 ||
|
|
552
|
+
result.unverifiable_paths.length > 0 ||
|
|
553
|
+
result.unverifiable_repos.length > 0
|
|
554
|
+
) {
|
|
555
|
+
lines.push(
|
|
556
|
+
`Commit and push the owned Git work, then rerun \`${bin} agents status --end-turn\`.`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (result.unsupported_paths.length > 0) {
|
|
560
|
+
lines.push(
|
|
561
|
+
"For a legacy claim, add the containing Git repository or intentional non-Git output root to the project's `agents.finalizationRoots`, then rerun the guarded status. For new work, start the task from the target Git project or configure that root before editing.",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return lines.join("\n");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function unsupportedReasonMessage(reason: UnsupportedFinalizationReason): string {
|
|
568
|
+
switch (reason) {
|
|
569
|
+
case "outside_finalization_roots":
|
|
570
|
+
return "outside the coordination repository and project-configured finalization roots";
|
|
571
|
+
case "invalid_finalization_root":
|
|
572
|
+
return "the matching project-configured root is invalid or overlaps another disposition";
|
|
573
|
+
case "ambiguous_finalization_roots":
|
|
574
|
+
return "more than one finalization disposition matches this path";
|
|
575
|
+
case "path_escapes_finalization_root":
|
|
576
|
+
return "the resolved path escapes its configured root";
|
|
577
|
+
case "git_repository_unavailable":
|
|
578
|
+
return "the configured Git root does not contain a verifiable repository for this path";
|
|
579
|
+
case "output_path_is_git":
|
|
580
|
+
return "the configured non-Git output path is inside a Git repository";
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function formatWriteClaimFinalizationDenial(
|
|
585
|
+
decision: Extract<ClaimFinalizationDecision, { allow: false }>,
|
|
586
|
+
bin: string,
|
|
587
|
+
): string {
|
|
588
|
+
return (
|
|
589
|
+
`Harnery denied the write to ${decision.path} before mutation: ` +
|
|
590
|
+
`${unsupportedReasonMessage(decision.reason)}. ` +
|
|
591
|
+
"Start the task from the target Git project, or add its Git repository root " +
|
|
592
|
+
"(or an intentional non-Git output root) to the project's `agents.finalizationRoots` " +
|
|
593
|
+
`before editing. Then retry the tool and finish with \`${bin} agents status --end-turn\`.`
|
|
594
|
+
);
|
|
595
|
+
}
|