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,443 @@
|
|
|
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
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
|
+
import { agentsFinalizationRoots } from "../config.js";
|
|
12
|
+
import { readStreamTailBounded } from "./events/consume.js";
|
|
13
|
+
const CLAIM_HISTORY_CAP_BYTES = 128 * 1024 * 1024;
|
|
14
|
+
const GIT_DISCOVERY_VARS = [
|
|
15
|
+
"GIT_DIR",
|
|
16
|
+
"GIT_WORK_TREE",
|
|
17
|
+
"GIT_COMMON_DIR",
|
|
18
|
+
"GIT_INDEX_FILE",
|
|
19
|
+
"GIT_OBJECT_DIRECTORY",
|
|
20
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
21
|
+
"GIT_PREFIX",
|
|
22
|
+
"GIT_NAMESPACE",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Read every write claim made by this session, including claims released after
|
|
26
|
+
* a commit. Current heartbeat claims describe dirty ownership; this history is
|
|
27
|
+
* what keeps the touched repository in scope until its commits are pushed.
|
|
28
|
+
*/
|
|
29
|
+
export function readSessionWriteClaims(coordRoot, instanceId, sessionId) {
|
|
30
|
+
const streamPath = join(coordRoot, ".harnery", "events.ndjson");
|
|
31
|
+
const { text, truncated } = readStreamTailBounded(streamPath, CLAIM_HISTORY_CAP_BYTES);
|
|
32
|
+
const paths = new Set();
|
|
33
|
+
let sawSessionStart = false;
|
|
34
|
+
for (const line of text.split("\n")) {
|
|
35
|
+
if (line.trim().length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
try {
|
|
38
|
+
const event = JSON.parse(line);
|
|
39
|
+
if (event.instance_id !== instanceId || event.session_id !== sessionId)
|
|
40
|
+
continue;
|
|
41
|
+
if (event.event_type === "session.start")
|
|
42
|
+
sawSessionStart = true;
|
|
43
|
+
if (event.event_type === "claim.acquire" &&
|
|
44
|
+
event.data?.mode === "write" &&
|
|
45
|
+
typeof event.data.path === "string") {
|
|
46
|
+
paths.add(event.data.path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ignore malformed and crash-truncated event lines, matching the
|
|
51
|
+
// canonical stream consumer's recovery behavior.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { paths: [...paths], complete: !truncated || sawSessionStart };
|
|
55
|
+
}
|
|
56
|
+
function git(args, cwd) {
|
|
57
|
+
const env = { ...process.env };
|
|
58
|
+
for (const name of GIT_DISCOVERY_VARS)
|
|
59
|
+
delete env[name];
|
|
60
|
+
const result = spawnSync("git", args, { cwd, env, encoding: "utf8", timeout: 5_000 });
|
|
61
|
+
return {
|
|
62
|
+
status: result.status ?? 1,
|
|
63
|
+
stdout: result.stdout ?? "",
|
|
64
|
+
stderr: result.stderr ?? result.error?.message ?? "",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function repoLabel(coordRoot, repoRoot) {
|
|
68
|
+
const rel = relative(coordRoot, repoRoot).replaceAll("\\", "/");
|
|
69
|
+
return rel.length === 0 ? "." : rel;
|
|
70
|
+
}
|
|
71
|
+
function pathInside(root, candidate) {
|
|
72
|
+
const rel = relative(root, candidate);
|
|
73
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
74
|
+
}
|
|
75
|
+
function existingProbeDir(authorityRoot, absolutePath) {
|
|
76
|
+
let current = absolutePath;
|
|
77
|
+
if (existsSync(current) && !statSync(current).isDirectory())
|
|
78
|
+
current = dirname(current);
|
|
79
|
+
while (!existsSync(current)) {
|
|
80
|
+
const parent = dirname(current);
|
|
81
|
+
if (parent === current || !pathInside(authorityRoot, parent))
|
|
82
|
+
return null;
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
return pathInside(authorityRoot, current) ? current : null;
|
|
86
|
+
}
|
|
87
|
+
/** Resolve existing ancestors through symlinks while preserving a missing tail. */
|
|
88
|
+
function physicalPath(absolutePath) {
|
|
89
|
+
let existing = absolutePath;
|
|
90
|
+
while (!existsSync(existing)) {
|
|
91
|
+
const parent = dirname(existing);
|
|
92
|
+
if (parent === existing)
|
|
93
|
+
return null;
|
|
94
|
+
existing = parent;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const physical = realpathSync.native(existing);
|
|
98
|
+
const tail = relative(existing, absolutePath);
|
|
99
|
+
return resolve(physical, tail);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Translate a Windows-native WSL UNC claim back to the Linux path seen by the
|
|
107
|
+
* coordination process. Codex can report `\\\\wsl.localhost\\<distro>\\...`
|
|
108
|
+
* while Harnery runs inside that distro with a `/...` coord root.
|
|
109
|
+
*/
|
|
110
|
+
function resolveHeldPath(coordRoot, heldPath) {
|
|
111
|
+
const wslUnc = /^\\\\(?:wsl\.localhost|wsl\$)\\[^\\]+\\(.+)$/i.exec(heldPath);
|
|
112
|
+
if (wslUnc) {
|
|
113
|
+
const linuxPath = `/${wslUnc[1].replaceAll("\\", "/")}`;
|
|
114
|
+
const slashRoot = coordRoot.replaceAll("\\", "/");
|
|
115
|
+
if (slashRoot.startsWith("/") || pathInside(coordRoot, linuxPath))
|
|
116
|
+
return linuxPath;
|
|
117
|
+
}
|
|
118
|
+
return resolve(coordRoot, heldPath);
|
|
119
|
+
}
|
|
120
|
+
function discoverRepo(authorityRoot, absolutePath) {
|
|
121
|
+
const probeDir = existingProbeDir(authorityRoot, absolutePath);
|
|
122
|
+
if (!probeDir)
|
|
123
|
+
return null;
|
|
124
|
+
const rootResult = git(["rev-parse", "--show-toplevel"], probeDir);
|
|
125
|
+
if (rootResult.status !== 0)
|
|
126
|
+
return null;
|
|
127
|
+
const repoRoot = physicalPath(resolve(rootResult.stdout.trim()));
|
|
128
|
+
if (!repoRoot || !pathInside(authorityRoot, repoRoot) || !pathInside(repoRoot, absolutePath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const repoPath = relative(repoRoot, absolutePath).replaceAll("\\", "/") || ".";
|
|
132
|
+
return { root: repoRoot, path: repoPath };
|
|
133
|
+
}
|
|
134
|
+
function rootIsGitRepository(root) {
|
|
135
|
+
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
136
|
+
return false;
|
|
137
|
+
const result = git(["rev-parse", "--show-toplevel"], root);
|
|
138
|
+
if (result.status !== 0)
|
|
139
|
+
return false;
|
|
140
|
+
const discovered = physicalPath(resolve(result.stdout.trim()));
|
|
141
|
+
const physicalRoot = physicalPath(root);
|
|
142
|
+
return discovered !== null && physicalRoot !== null && discovered === physicalRoot;
|
|
143
|
+
}
|
|
144
|
+
function outputRootIsInsideGit(root) {
|
|
145
|
+
let probe = root;
|
|
146
|
+
while (!existsSync(probe)) {
|
|
147
|
+
const parent = dirname(probe);
|
|
148
|
+
if (parent === probe)
|
|
149
|
+
return false;
|
|
150
|
+
probe = parent;
|
|
151
|
+
}
|
|
152
|
+
if (!statSync(probe).isDirectory())
|
|
153
|
+
probe = dirname(probe);
|
|
154
|
+
return git(["rev-parse", "--show-toplevel"], probe).status === 0;
|
|
155
|
+
}
|
|
156
|
+
function finalizationRoots(coordRoot) {
|
|
157
|
+
const root = resolve(coordRoot);
|
|
158
|
+
const configured = agentsFinalizationRoots(root).map((entry) => {
|
|
159
|
+
const lexicalRoot = resolveHeldPath(root, entry.path);
|
|
160
|
+
const physicalRoot = physicalPath(lexicalRoot);
|
|
161
|
+
const invalid = physicalRoot === null ||
|
|
162
|
+
(entry.disposition === "git"
|
|
163
|
+
? !rootIsGitRepository(physicalRoot)
|
|
164
|
+
: outputRootIsInsideGit(physicalRoot));
|
|
165
|
+
return {
|
|
166
|
+
configuredPath: entry.path,
|
|
167
|
+
disposition: entry.disposition,
|
|
168
|
+
lexicalRoot,
|
|
169
|
+
physicalRoot,
|
|
170
|
+
invalid,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const rootPhysical = physicalPath(root);
|
|
174
|
+
if (rootPhysical && rootIsGitRepository(rootPhysical)) {
|
|
175
|
+
configured.push({
|
|
176
|
+
configuredPath: ".",
|
|
177
|
+
disposition: "git",
|
|
178
|
+
lexicalRoot: root,
|
|
179
|
+
physicalRoot: rootPhysical,
|
|
180
|
+
invalid: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (const current of configured) {
|
|
184
|
+
if (!current.physicalRoot)
|
|
185
|
+
continue;
|
|
186
|
+
for (const peer of configured) {
|
|
187
|
+
if (current === peer || !peer.physicalRoot || current.disposition === peer.disposition) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (pathInside(current.physicalRoot, peer.physicalRoot) ||
|
|
191
|
+
pathInside(peer.physicalRoot, current.physicalRoot)) {
|
|
192
|
+
current.invalid = true;
|
|
193
|
+
peer.invalid = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return configured;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve one guarded write to the host's single supported end-turn
|
|
201
|
+
* disposition. Claim event data never grants authority: every call starts from
|
|
202
|
+
* the project-owned finalization roots and only then inspects the selected
|
|
203
|
+
* repository or output tree.
|
|
204
|
+
*/
|
|
205
|
+
export function classifyWriteClaimFinalization(coordRoot, heldPath) {
|
|
206
|
+
const root = resolve(coordRoot);
|
|
207
|
+
const absolutePath = resolveHeldPath(root, heldPath);
|
|
208
|
+
const roots = finalizationRoots(root);
|
|
209
|
+
const lexicalMatches = roots.filter((entry) => pathInside(entry.lexicalRoot, absolutePath));
|
|
210
|
+
if (lexicalMatches.length === 0) {
|
|
211
|
+
return { allow: false, path: heldPath, reason: "outside_finalization_roots" };
|
|
212
|
+
}
|
|
213
|
+
if (lexicalMatches.some((entry) => entry.invalid)) {
|
|
214
|
+
return { allow: false, path: heldPath, reason: "invalid_finalization_root" };
|
|
215
|
+
}
|
|
216
|
+
const physical = physicalPath(absolutePath);
|
|
217
|
+
if (!physical) {
|
|
218
|
+
return { allow: false, path: heldPath, reason: "path_escapes_finalization_root" };
|
|
219
|
+
}
|
|
220
|
+
const matches = lexicalMatches.filter((entry) => entry.physicalRoot && pathInside(entry.physicalRoot, physical));
|
|
221
|
+
if (matches.length === 0) {
|
|
222
|
+
return { allow: false, path: heldPath, reason: "path_escapes_finalization_root" };
|
|
223
|
+
}
|
|
224
|
+
const dispositions = new Set(matches.map((entry) => entry.disposition));
|
|
225
|
+
if (dispositions.size !== 1) {
|
|
226
|
+
return { allow: false, path: heldPath, reason: "ambiguous_finalization_roots" };
|
|
227
|
+
}
|
|
228
|
+
const selected = [...matches].sort((a, b) => (b.physicalRoot?.length ?? 0) - (a.physicalRoot?.length ?? 0))[0];
|
|
229
|
+
const normalizedPath = relative(root, absolutePath).replaceAll("\\", "/") || ".";
|
|
230
|
+
const descriptor = {
|
|
231
|
+
disposition: selected.disposition,
|
|
232
|
+
root: selected.configuredPath,
|
|
233
|
+
};
|
|
234
|
+
if (selected.disposition === "output") {
|
|
235
|
+
const probe = existingProbeDir(selected.physicalRoot, physical);
|
|
236
|
+
if (probe && git(["rev-parse", "--show-toplevel"], probe).status === 0) {
|
|
237
|
+
return { allow: false, path: heldPath, reason: "output_path_is_git" };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
allow: true,
|
|
241
|
+
path: normalizedPath,
|
|
242
|
+
descriptor,
|
|
243
|
+
authorityRoot: selected.physicalRoot,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const repo = discoverRepo(selected.physicalRoot, physical);
|
|
247
|
+
if (!repo) {
|
|
248
|
+
return { allow: false, path: heldPath, reason: "git_repository_unavailable" };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
allow: true,
|
|
252
|
+
path: normalizedPath,
|
|
253
|
+
descriptor,
|
|
254
|
+
authorityRoot: selected.physicalRoot,
|
|
255
|
+
repo,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function addRepoPath(repos, root, path, gitlink = false) {
|
|
259
|
+
const work = repos.get(root) ?? {
|
|
260
|
+
root,
|
|
261
|
+
paths: new Set(),
|
|
262
|
+
gitlinks: new Set(),
|
|
263
|
+
};
|
|
264
|
+
work.paths.add(path);
|
|
265
|
+
if (gitlink)
|
|
266
|
+
work.gitlinks.add(path);
|
|
267
|
+
repos.set(root, work);
|
|
268
|
+
}
|
|
269
|
+
function addSuperprojects(authorityRoot, repos, leafRoot) {
|
|
270
|
+
let childRoot = leafRoot;
|
|
271
|
+
for (;;) {
|
|
272
|
+
const result = git(["rev-parse", "--show-superproject-working-tree"], childRoot);
|
|
273
|
+
if (result.status !== 0 || result.stdout.trim().length === 0)
|
|
274
|
+
return;
|
|
275
|
+
const parentRoot = physicalPath(resolve(result.stdout.trim()));
|
|
276
|
+
if (!parentRoot || !pathInside(authorityRoot, parentRoot) || parentRoot === childRoot)
|
|
277
|
+
return;
|
|
278
|
+
const gitlink = relative(parentRoot, childRoot).replaceAll("\\", "/");
|
|
279
|
+
addRepoPath(repos, parentRoot, gitlink, true);
|
|
280
|
+
childRoot = parentRoot;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function repoHasRemote(repoRoot) {
|
|
284
|
+
const result = git(["remote"], repoRoot);
|
|
285
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
286
|
+
}
|
|
287
|
+
function repoSyncState(repoRoot) {
|
|
288
|
+
if (!repoHasRemote(repoRoot))
|
|
289
|
+
return "synced";
|
|
290
|
+
if (git(["rev-parse", "--verify", "HEAD"], repoRoot).status !== 0)
|
|
291
|
+
return "unverifiable";
|
|
292
|
+
const upstream = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], repoRoot);
|
|
293
|
+
if (upstream.status === 0) {
|
|
294
|
+
const ahead = git(["rev-list", "--count", "@{upstream}..HEAD"], repoRoot);
|
|
295
|
+
if (ahead.status !== 0)
|
|
296
|
+
return "unverifiable";
|
|
297
|
+
return Number.parseInt(ahead.stdout.trim(), 10) > 0 ? "unpushed" : "synced";
|
|
298
|
+
}
|
|
299
|
+
// Detached submodules commonly have no upstream. A remote ref containing
|
|
300
|
+
// HEAD proves that commit is available elsewhere without requiring a branch.
|
|
301
|
+
const containing = git(["branch", "--remotes", "--contains", "HEAD", "--format=%(refname:short)"], repoRoot);
|
|
302
|
+
if (containing.status !== 0)
|
|
303
|
+
return "unverifiable";
|
|
304
|
+
return containing.stdout.trim().length > 0 ? "synced" : "unpushed";
|
|
305
|
+
}
|
|
306
|
+
/** Compare gitlink commits without treating dirty contents inside the child as a pointer change. */
|
|
307
|
+
function gitlinkIsDirty(parentRoot, gitlink) {
|
|
308
|
+
const head = git(["rev-parse", `HEAD:${gitlink}`], parentRoot);
|
|
309
|
+
const index = git(["ls-files", "--stage", "--", gitlink], parentRoot);
|
|
310
|
+
const child = git(["rev-parse", "HEAD"], join(parentRoot, gitlink));
|
|
311
|
+
if (head.status !== 0 || index.status !== 0 || child.status !== 0)
|
|
312
|
+
return true;
|
|
313
|
+
const indexMatch = /^160000 ([0-9a-f]{40}) 0\t/.exec(index.stdout.trim());
|
|
314
|
+
if (!indexMatch)
|
|
315
|
+
return true;
|
|
316
|
+
return head.stdout.trim() !== indexMatch[1] || child.stdout.trim() !== indexMatch[1];
|
|
317
|
+
}
|
|
318
|
+
/** Check that this session's held paths are committed and their repositories are pushed. */
|
|
319
|
+
export function checkGitFinalization(coordRoot, heldPaths, options = {}) {
|
|
320
|
+
const root = resolve(coordRoot);
|
|
321
|
+
const repos = new Map();
|
|
322
|
+
const hostOutputPaths = new Set();
|
|
323
|
+
const unsupportedPaths = [];
|
|
324
|
+
const unverifiablePaths = [];
|
|
325
|
+
for (const heldPath of heldPaths) {
|
|
326
|
+
const decision = classifyWriteClaimFinalization(root, heldPath);
|
|
327
|
+
if (!decision.allow) {
|
|
328
|
+
unsupportedPaths.push({ path: heldPath, reason: decision.reason });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (decision.descriptor.disposition === "output") {
|
|
332
|
+
hostOutputPaths.add(decision.path);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!decision.repo) {
|
|
336
|
+
unsupportedPaths.push({ path: heldPath, reason: "git_repository_unavailable" });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
addRepoPath(repos, decision.repo.root, decision.repo.path);
|
|
340
|
+
addSuperprojects(decision.authorityRoot, repos, decision.repo.root);
|
|
341
|
+
}
|
|
342
|
+
const dirtyPaths = new Set();
|
|
343
|
+
const unpushedRepos = [];
|
|
344
|
+
const unverifiableRepos = [];
|
|
345
|
+
for (const work of repos.values()) {
|
|
346
|
+
for (const repoPath of work.paths) {
|
|
347
|
+
if (work.gitlinks.has(repoPath)) {
|
|
348
|
+
if (gitlinkIsDirty(work.root, repoPath)) {
|
|
349
|
+
const label = repoLabel(root, work.root);
|
|
350
|
+
dirtyPaths.add(label === "." ? repoPath : `${label}/${repoPath}`);
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const status = git(["status", "--porcelain=v1", "--untracked-files=all", "--", repoPath], work.root);
|
|
355
|
+
if (status.status !== 0) {
|
|
356
|
+
unverifiablePaths.push(join(repoLabel(root, work.root), repoPath).replaceAll("\\", "/"));
|
|
357
|
+
}
|
|
358
|
+
else if (status.stdout.length > 0) {
|
|
359
|
+
const label = repoLabel(root, work.root);
|
|
360
|
+
dirtyPaths.add(label === "." ? repoPath : `${label}/${repoPath}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const syncState = repoSyncState(work.root);
|
|
364
|
+
const label = repoLabel(root, work.root);
|
|
365
|
+
if (syncState === "unpushed")
|
|
366
|
+
unpushedRepos.push(label);
|
|
367
|
+
if (syncState === "unverifiable")
|
|
368
|
+
unverifiableRepos.push(label);
|
|
369
|
+
}
|
|
370
|
+
const result = {
|
|
371
|
+
ok: false,
|
|
372
|
+
claim_history_complete: options.claimHistoryComplete ?? true,
|
|
373
|
+
dirty_paths: [...dirtyPaths].sort(),
|
|
374
|
+
unpushed_repos: [...new Set(unpushedRepos)].sort(),
|
|
375
|
+
host_output_paths: [...hostOutputPaths].sort(),
|
|
376
|
+
unsupported_paths: unsupportedPaths.sort((a, b) => a.path.localeCompare(b.path) || a.reason.localeCompare(b.reason)),
|
|
377
|
+
unverifiable_paths: [...new Set(unverifiablePaths)].sort(),
|
|
378
|
+
unverifiable_repos: [...new Set(unverifiableRepos)].sort(),
|
|
379
|
+
repos_checked: [...repos.keys()].map((repo) => repoLabel(root, repo)).sort(),
|
|
380
|
+
};
|
|
381
|
+
result.ok =
|
|
382
|
+
result.claim_history_complete &&
|
|
383
|
+
result.dirty_paths.length === 0 &&
|
|
384
|
+
result.unpushed_repos.length === 0 &&
|
|
385
|
+
result.unsupported_paths.length === 0 &&
|
|
386
|
+
result.unverifiable_paths.length === 0 &&
|
|
387
|
+
result.unverifiable_repos.length === 0;
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
export function formatGitFinalizationFailure(result, bin) {
|
|
391
|
+
const lines = ["Owned work is not finalized; the status box was not issued."];
|
|
392
|
+
if (!result.claim_history_complete) {
|
|
393
|
+
lines.push("This session's write-claim history is older than the bounded event window.");
|
|
394
|
+
}
|
|
395
|
+
if (result.dirty_paths.length > 0) {
|
|
396
|
+
lines.push("Dirty owned paths:", ...result.dirty_paths.map((path) => ` - ${path}`));
|
|
397
|
+
}
|
|
398
|
+
if (result.unpushed_repos.length > 0) {
|
|
399
|
+
lines.push("Repositories with commits not on their remote:", ...result.unpushed_repos.map((repo) => ` - ${repo}`));
|
|
400
|
+
}
|
|
401
|
+
if (result.unsupported_paths.length > 0) {
|
|
402
|
+
lines.push("Paths without an authorized end-turn disposition:", ...result.unsupported_paths.map(({ path, reason }) => ` - ${path}: ${unsupportedReasonMessage(reason)}`));
|
|
403
|
+
}
|
|
404
|
+
if (result.unverifiable_paths.length > 0) {
|
|
405
|
+
lines.push("Paths whose Git state could not be verified:", ...result.unverifiable_paths.map((path) => ` - ${path}`));
|
|
406
|
+
}
|
|
407
|
+
if (result.unverifiable_repos.length > 0) {
|
|
408
|
+
lines.push("Repositories whose remote state could not be verified:", ...result.unverifiable_repos.map((repo) => ` - ${repo}`));
|
|
409
|
+
}
|
|
410
|
+
if (result.dirty_paths.length > 0 ||
|
|
411
|
+
result.unpushed_repos.length > 0 ||
|
|
412
|
+
result.unverifiable_paths.length > 0 ||
|
|
413
|
+
result.unverifiable_repos.length > 0) {
|
|
414
|
+
lines.push(`Commit and push the owned Git work, then rerun \`${bin} agents status --end-turn\`.`);
|
|
415
|
+
}
|
|
416
|
+
if (result.unsupported_paths.length > 0) {
|
|
417
|
+
lines.push("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.");
|
|
418
|
+
}
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
}
|
|
421
|
+
function unsupportedReasonMessage(reason) {
|
|
422
|
+
switch (reason) {
|
|
423
|
+
case "outside_finalization_roots":
|
|
424
|
+
return "outside the coordination repository and project-configured finalization roots";
|
|
425
|
+
case "invalid_finalization_root":
|
|
426
|
+
return "the matching project-configured root is invalid or overlaps another disposition";
|
|
427
|
+
case "ambiguous_finalization_roots":
|
|
428
|
+
return "more than one finalization disposition matches this path";
|
|
429
|
+
case "path_escapes_finalization_root":
|
|
430
|
+
return "the resolved path escapes its configured root";
|
|
431
|
+
case "git_repository_unavailable":
|
|
432
|
+
return "the configured Git root does not contain a verifiable repository for this path";
|
|
433
|
+
case "output_path_is_git":
|
|
434
|
+
return "the configured non-Git output path is inside a Git repository";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
export function formatWriteClaimFinalizationDenial(decision, bin) {
|
|
438
|
+
return (`Harnery denied the write to ${decision.path} before mutation: ` +
|
|
439
|
+
`${unsupportedReasonMessage(decision.reason)}. ` +
|
|
440
|
+
"Start the task from the target Git project, or add its Git repository root " +
|
|
441
|
+
"(or an intentional non-Git output root) to the project's `agents.finalizationRoots` " +
|
|
442
|
+
`before editing. Then retry the tool and finish with \`${bin} agents status --end-turn\`.`);
|
|
443
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-plumbing collection for `agent-coord git-hook <event>` — the in-process
|
|
3
|
+
* replacement for the bash that host pre-commit / post-commit / post-checkout
|
|
4
|
+
* hooks used to carry.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is a port of hook logic that lived in host repos as shell:
|
|
7
|
+
* staged-path collection with rename handling, submodule canonicalization,
|
|
8
|
+
* the gitlink probe, and the clean-in-worktree checks that gate claim pruning.
|
|
9
|
+
* Owning it here means a harnery upgrade upgrades the behavior; the host hook
|
|
10
|
+
* file keeps only a stable managed region that invokes `git-hook <event>`.
|
|
11
|
+
*
|
|
12
|
+
* All functions take an explicit cwd and return data — no printing, no
|
|
13
|
+
* process.exit — so they unit-test against a temp repo.
|
|
14
|
+
*/
|
|
15
|
+
export type GitHookEvent = "pre-commit" | "post-commit" | "post-checkout";
|
|
16
|
+
/**
|
|
17
|
+
* The coordination root for a hook firing in `cwd`: the superproject when the
|
|
18
|
+
* repo is a submodule, else the repo's own toplevel. Pinning to the
|
|
19
|
+
* superproject matters because some submodules carry a config-only `.harnery/`
|
|
20
|
+
* that a cwd walk would land on, running the check against an empty claims dir.
|
|
21
|
+
*/
|
|
22
|
+
export declare function discoverCoordRoot(cwd: string): string | null;
|
|
23
|
+
/** Monorepo-relative prefix when `cwd` is inside a submodule ("" in the parent). */
|
|
24
|
+
export declare function submodulePrefix(cwd: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Parse `--name-status -M` output into repo-local paths. Renames/copies emit
|
|
27
|
+
* BOTH sides so the source's claim releases too; plain statuses emit one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseNameStatus(output: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* pre-commit: staged paths (canonical, monorepo-relative) plus which of them
|
|
32
|
+
* are submodule gitlinks — a parent-repo pointer bump is not a claim on the
|
|
33
|
+
* submodule's contents, and the conflict rule's prefix matcher needs to know.
|
|
34
|
+
*/
|
|
35
|
+
export declare function collectStaged(cwd: string): {
|
|
36
|
+
staged: string[];
|
|
37
|
+
gitlinks: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* post-commit: paths the just-landed HEAD commit touched that are now clean in
|
|
41
|
+
* the worktree. A path with unstaged residue keeps its claim — the agent is
|
|
42
|
+
* still mid-work on it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function collectCommitted(cwd: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* post-checkout: paths the ref move rewrote that are now clean — a checkout
|
|
47
|
+
* that discarded an agent's edits should release its claims so a peer's later
|
|
48
|
+
* Edit isn't falsely blocked. `oldRef`/`newRef` are the hook's $1/$2.
|
|
49
|
+
*/
|
|
50
|
+
export declare function collectCheckoutRemoved(cwd: string, oldRef: string, newRef: string): string[];
|
|
51
|
+
//# sourceMappingURL=git-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-hook.d.ts","sourceRoot":"","sources":["../../../src/core/agents/git-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,eAAe,CAAC;AAO1E;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAcxD;AAgBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAWnF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAStD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM5F"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-plumbing collection for `agent-coord git-hook <event>` — the in-process
|
|
3
|
+
* replacement for the bash that host pre-commit / post-commit / post-checkout
|
|
4
|
+
* hooks used to carry.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is a port of hook logic that lived in host repos as shell:
|
|
7
|
+
* staged-path collection with rename handling, submodule canonicalization,
|
|
8
|
+
* the gitlink probe, and the clean-in-worktree checks that gate claim pruning.
|
|
9
|
+
* Owning it here means a harnery upgrade upgrades the behavior; the host hook
|
|
10
|
+
* file keeps only a stable managed region that invokes `git-hook <event>`.
|
|
11
|
+
*
|
|
12
|
+
* All functions take an explicit cwd and return data — no printing, no
|
|
13
|
+
* process.exit — so they unit-test against a temp repo.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
function git(args, cwd) {
|
|
17
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5000 });
|
|
18
|
+
return r.status === 0 ? (r.stdout ?? "") : "";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The coordination root for a hook firing in `cwd`: the superproject when the
|
|
22
|
+
* repo is a submodule, else the repo's own toplevel. Pinning to the
|
|
23
|
+
* superproject matters because some submodules carry a config-only `.harnery/`
|
|
24
|
+
* that a cwd walk would land on, running the check against an empty claims dir.
|
|
25
|
+
*/
|
|
26
|
+
export function discoverCoordRoot(cwd) {
|
|
27
|
+
const superproject = git(["rev-parse", "--show-superproject-working-tree"], cwd).trim();
|
|
28
|
+
if (superproject)
|
|
29
|
+
return superproject;
|
|
30
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], cwd).trim();
|
|
31
|
+
return toplevel || null;
|
|
32
|
+
}
|
|
33
|
+
/** Monorepo-relative prefix when `cwd` is inside a submodule ("" in the parent). */
|
|
34
|
+
export function submodulePrefix(cwd) {
|
|
35
|
+
const superproject = git(["rev-parse", "--show-superproject-working-tree"], cwd).trim();
|
|
36
|
+
if (!superproject)
|
|
37
|
+
return "";
|
|
38
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], cwd).trim();
|
|
39
|
+
if (!toplevel.startsWith(`${superproject}/`))
|
|
40
|
+
return "";
|
|
41
|
+
return toplevel.slice(superproject.length + 1);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse `--name-status -M` output into repo-local paths. Renames/copies emit
|
|
45
|
+
* BOTH sides so the source's claim releases too; plain statuses emit one.
|
|
46
|
+
*/
|
|
47
|
+
export function parseNameStatus(output) {
|
|
48
|
+
const paths = [];
|
|
49
|
+
for (const line of output.split("\n")) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
const parts = line.split("\t");
|
|
53
|
+
const status = parts[0] ?? "";
|
|
54
|
+
if (/^[RC]/.test(status)) {
|
|
55
|
+
if (parts[1])
|
|
56
|
+
paths.push(parts[1]);
|
|
57
|
+
if (parts[2])
|
|
58
|
+
paths.push(parts[2]);
|
|
59
|
+
}
|
|
60
|
+
else if (parts[1]) {
|
|
61
|
+
paths.push(parts[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
function canonicalize(paths, prefix) {
|
|
67
|
+
return prefix ? paths.map((p) => `${prefix}/${p}`) : [...paths];
|
|
68
|
+
}
|
|
69
|
+
/** Is this repo-local path a submodule gitlink (mode 160000) in the index? */
|
|
70
|
+
function isGitlink(path, cwd) {
|
|
71
|
+
return git(["ls-files", "--stage", "--", path], cwd).startsWith("160000 ");
|
|
72
|
+
}
|
|
73
|
+
/** Is this repo-local path clean in the worktree (no unstaged residue)? */
|
|
74
|
+
function isCleanInWorktree(path, cwd) {
|
|
75
|
+
return git(["status", "--porcelain", "--", path], cwd).trim() === "";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* pre-commit: staged paths (canonical, monorepo-relative) plus which of them
|
|
79
|
+
* are submodule gitlinks — a parent-repo pointer bump is not a claim on the
|
|
80
|
+
* submodule's contents, and the conflict rule's prefix matcher needs to know.
|
|
81
|
+
*/
|
|
82
|
+
export function collectStaged(cwd) {
|
|
83
|
+
const out = git(["diff", "--cached", "--name-status", "-M"], cwd);
|
|
84
|
+
const local = parseNameStatus(out);
|
|
85
|
+
const prefix = submodulePrefix(cwd);
|
|
86
|
+
const staged = canonicalize(local, prefix);
|
|
87
|
+
const gitlinks = [];
|
|
88
|
+
for (let i = 0; i < local.length; i++) {
|
|
89
|
+
const l = local[i];
|
|
90
|
+
if (isGitlink(l, cwd))
|
|
91
|
+
gitlinks.push(staged[i]);
|
|
92
|
+
}
|
|
93
|
+
return { staged, gitlinks };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* post-commit: paths the just-landed HEAD commit touched that are now clean in
|
|
97
|
+
* the worktree. A path with unstaged residue keeps its claim — the agent is
|
|
98
|
+
* still mid-work on it.
|
|
99
|
+
*/
|
|
100
|
+
export function collectCommitted(cwd) {
|
|
101
|
+
// --root: a repo's first commit has no parent; without it diff-tree emits
|
|
102
|
+
// nothing and the root commit's claims never prune (a bash-era blind spot).
|
|
103
|
+
const out = git(["diff-tree", "--no-commit-id", "--name-status", "-M", "-r", "--root", "HEAD"], cwd);
|
|
104
|
+
const local = parseNameStatus(out).filter((p) => isCleanInWorktree(p, cwd));
|
|
105
|
+
return canonicalize(local, submodulePrefix(cwd));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* post-checkout: paths the ref move rewrote that are now clean — a checkout
|
|
109
|
+
* that discarded an agent's edits should release its claims so a peer's later
|
|
110
|
+
* Edit isn't falsely blocked. `oldRef`/`newRef` are the hook's $1/$2.
|
|
111
|
+
*/
|
|
112
|
+
export function collectCheckoutRemoved(cwd, oldRef, newRef) {
|
|
113
|
+
const from = oldRef || "0000000000000000000000000000000000000000";
|
|
114
|
+
const to = newRef || "HEAD";
|
|
115
|
+
const out = git(["diff", "--name-status", "-M", from, to], cwd);
|
|
116
|
+
const local = parseNameStatus(out).filter((p) => isCleanInWorktree(p, cwd));
|
|
117
|
+
return canonicalize(local, submodulePrefix(cwd));
|
|
118
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/agents/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/agents/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC"}
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* + sorted(files_touched) + platform, sorted by instance_id).
|
|
13
13
|
* 2. Council pending: pending open-council IDs hashed; re-emits when the
|
|
14
14
|
* ID set changes.
|
|
15
|
-
* 3. Focus nudge.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* 3. Focus nudge. Until the session has produced its suggested name (first
|
|
16
|
+
* non-empty set-task), tells every adapter — on every prompt, undeduped —
|
|
17
|
+
* to reproduce set-task's suggested session name in a fenced code block.
|
|
18
|
+
* Cursor/Codex additionally get the existing unset/stale-task reminder
|
|
19
|
+
* because their Stop hooks do not enforce task declarations as reliably
|
|
20
|
+
* as Claude Code's.
|
|
20
21
|
* 4. Codex status footer: a non-deduplicated reminder to put the live status
|
|
21
22
|
* box after the substantive answer. Stop stays observe-only, so missing the
|
|
22
23
|
* footer can never trigger a replacement response.
|
|
@@ -42,6 +43,12 @@ export interface PromptContextOpts {
|
|
|
42
43
|
/** When true, append the non-deduplicated Codex status-footer reminder.
|
|
43
44
|
* The caller enables this only for human-facing Codex sessions. */
|
|
44
45
|
statusFooterNudge?: boolean;
|
|
46
|
+
/** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
|
|
47
|
+
* status check). When set, append a fresh per-prompt reminder of that
|
|
48
|
+
* contract so the model satisfies it before ending the turn instead of
|
|
49
|
+
* being bounced into a retry. Not hash-deduped, same rationale as the
|
|
50
|
+
* status footer. */
|
|
51
|
+
turnRitualNudge?: string;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Build the combined UserPromptSubmit additionalContext string. Returns "" when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA0CH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqDnE"}
|