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,126 @@
|
|
|
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
|
+
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
export type GitHookEvent = "pre-commit" | "post-commit" | "post-checkout";
|
|
19
|
+
|
|
20
|
+
function git(args: string[], cwd: string): string {
|
|
21
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5000 });
|
|
22
|
+
return r.status === 0 ? (r.stdout ?? "") : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The coordination root for a hook firing in `cwd`: the superproject when the
|
|
27
|
+
* repo is a submodule, else the repo's own toplevel. Pinning to the
|
|
28
|
+
* superproject matters because some submodules carry a config-only `.harnery/`
|
|
29
|
+
* that a cwd walk would land on, running the check against an empty claims dir.
|
|
30
|
+
*/
|
|
31
|
+
export function discoverCoordRoot(cwd: string): string | null {
|
|
32
|
+
const superproject = git(["rev-parse", "--show-superproject-working-tree"], cwd).trim();
|
|
33
|
+
if (superproject) return superproject;
|
|
34
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], cwd).trim();
|
|
35
|
+
return toplevel || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Monorepo-relative prefix when `cwd` is inside a submodule ("" in the parent). */
|
|
39
|
+
export function submodulePrefix(cwd: string): string {
|
|
40
|
+
const superproject = git(["rev-parse", "--show-superproject-working-tree"], cwd).trim();
|
|
41
|
+
if (!superproject) return "";
|
|
42
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], cwd).trim();
|
|
43
|
+
if (!toplevel.startsWith(`${superproject}/`)) return "";
|
|
44
|
+
return toplevel.slice(superproject.length + 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse `--name-status -M` output into repo-local paths. Renames/copies emit
|
|
49
|
+
* BOTH sides so the source's claim releases too; plain statuses emit one.
|
|
50
|
+
*/
|
|
51
|
+
export function parseNameStatus(output: string): string[] {
|
|
52
|
+
const paths: string[] = [];
|
|
53
|
+
for (const line of output.split("\n")) {
|
|
54
|
+
if (!line.trim()) continue;
|
|
55
|
+
const parts = line.split("\t");
|
|
56
|
+
const status = parts[0] ?? "";
|
|
57
|
+
if (/^[RC]/.test(status)) {
|
|
58
|
+
if (parts[1]) paths.push(parts[1]);
|
|
59
|
+
if (parts[2]) paths.push(parts[2]);
|
|
60
|
+
} else if (parts[1]) {
|
|
61
|
+
paths.push(parts[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function canonicalize(paths: string[], prefix: string): string[] {
|
|
68
|
+
return prefix ? paths.map((p) => `${prefix}/${p}`) : [...paths];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Is this repo-local path a submodule gitlink (mode 160000) in the index? */
|
|
72
|
+
function isGitlink(path: string, cwd: string): boolean {
|
|
73
|
+
return git(["ls-files", "--stage", "--", path], cwd).startsWith("160000 ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Is this repo-local path clean in the worktree (no unstaged residue)? */
|
|
77
|
+
function isCleanInWorktree(path: string, cwd: string): boolean {
|
|
78
|
+
return git(["status", "--porcelain", "--", path], cwd).trim() === "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* pre-commit: staged paths (canonical, monorepo-relative) plus which of them
|
|
83
|
+
* are submodule gitlinks — a parent-repo pointer bump is not a claim on the
|
|
84
|
+
* submodule's contents, and the conflict rule's prefix matcher needs to know.
|
|
85
|
+
*/
|
|
86
|
+
export function collectStaged(cwd: string): { staged: string[]; gitlinks: string[] } {
|
|
87
|
+
const out = git(["diff", "--cached", "--name-status", "-M"], cwd);
|
|
88
|
+
const local = parseNameStatus(out);
|
|
89
|
+
const prefix = submodulePrefix(cwd);
|
|
90
|
+
const staged = canonicalize(local, prefix);
|
|
91
|
+
const gitlinks: string[] = [];
|
|
92
|
+
for (let i = 0; i < local.length; i++) {
|
|
93
|
+
const l = local[i]!;
|
|
94
|
+
if (isGitlink(l, cwd)) gitlinks.push(staged[i]!);
|
|
95
|
+
}
|
|
96
|
+
return { staged, gitlinks };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* post-commit: paths the just-landed HEAD commit touched that are now clean in
|
|
101
|
+
* the worktree. A path with unstaged residue keeps its claim — the agent is
|
|
102
|
+
* still mid-work on it.
|
|
103
|
+
*/
|
|
104
|
+
export function collectCommitted(cwd: string): string[] {
|
|
105
|
+
// --root: a repo's first commit has no parent; without it diff-tree emits
|
|
106
|
+
// nothing and the root commit's claims never prune (a bash-era blind spot).
|
|
107
|
+
const out = git(
|
|
108
|
+
["diff-tree", "--no-commit-id", "--name-status", "-M", "-r", "--root", "HEAD"],
|
|
109
|
+
cwd,
|
|
110
|
+
);
|
|
111
|
+
const local = parseNameStatus(out).filter((p) => isCleanInWorktree(p, cwd));
|
|
112
|
+
return canonicalize(local, submodulePrefix(cwd));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* post-checkout: paths the ref move rewrote that are now clean — a checkout
|
|
117
|
+
* that discarded an agent's edits should release its claims so a peer's later
|
|
118
|
+
* Edit isn't falsely blocked. `oldRef`/`newRef` are the hook's $1/$2.
|
|
119
|
+
*/
|
|
120
|
+
export function collectCheckoutRemoved(cwd: string, oldRef: string, newRef: string): string[] {
|
|
121
|
+
const from = oldRef || "0000000000000000000000000000000000000000";
|
|
122
|
+
const to = newRef || "HEAD";
|
|
123
|
+
const out = git(["diff", "--name-status", "-M", from, to], cwd);
|
|
124
|
+
const local = parseNameStatus(out).filter((p) => isCleanInWorktree(p, cwd));
|
|
125
|
+
return canonicalize(local, submodulePrefix(cwd));
|
|
126
|
+
}
|
package/src/core/agents/index.ts
CHANGED
|
@@ -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,8 +43,9 @@ import {
|
|
|
42
43
|
import { join } from "node:path";
|
|
43
44
|
|
|
44
45
|
import { coordEnv } from "../../../lib/env.ts";
|
|
45
|
-
import { resolveBinName } from "../../config.ts";
|
|
46
|
+
import { endOfTurnStatusCommand, resolveBinName } from "../../config.ts";
|
|
46
47
|
import { type RemoteMachine, readRemoteMachines } from "../../presence/index.ts";
|
|
48
|
+
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
47
49
|
import { formatPendingCouncils } from "./session-context.ts";
|
|
48
50
|
|
|
49
51
|
interface HeartbeatRow {
|
|
@@ -58,7 +60,12 @@ interface HeartbeatRow {
|
|
|
58
60
|
files_touched?: string[];
|
|
59
61
|
platform?: string;
|
|
60
62
|
task?: string;
|
|
63
|
+
activity?: AgentActivity;
|
|
64
|
+
task_state?: TaskState;
|
|
65
|
+
task_state_reason?: string;
|
|
61
66
|
task_updated_at?: string;
|
|
67
|
+
suggested_session_name?: string;
|
|
68
|
+
session_name_seen_for?: string;
|
|
62
69
|
turn_summary?: string;
|
|
63
70
|
workflow_run_id?: string;
|
|
64
71
|
}
|
|
@@ -77,6 +84,12 @@ export interface PromptContextOpts {
|
|
|
77
84
|
/** When true, append the non-deduplicated Codex status-footer reminder.
|
|
78
85
|
* The caller enables this only for human-facing Codex sessions. */
|
|
79
86
|
statusFooterNudge?: boolean;
|
|
87
|
+
/** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
|
|
88
|
+
* status check). When set, append a fresh per-prompt reminder of that
|
|
89
|
+
* contract so the model satisfies it before ending the turn instead of
|
|
90
|
+
* being bounced into a retry. Not hash-deduped, same rationale as the
|
|
91
|
+
* status footer. */
|
|
92
|
+
turnRitualNudge?: string;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
@@ -96,6 +109,7 @@ export function renderPromptContext(opts: PromptContextOpts): string {
|
|
|
96
109
|
sessionNameNudge,
|
|
97
110
|
taskNudge,
|
|
98
111
|
statusFooterNudge,
|
|
112
|
+
turnRitualNudge,
|
|
99
113
|
} = opts;
|
|
100
114
|
const sections: string[] = [];
|
|
101
115
|
|
|
@@ -128,17 +142,50 @@ export function renderPromptContext(opts: PromptContextOpts): string {
|
|
|
128
142
|
if (statusMsg) sections.push(statusMsg);
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
// 5. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
|
|
146
|
+
// on every prompt. Reminding before the reply is drafted is far cheaper than
|
|
147
|
+
// the bounce-and-retry the Stop hook otherwise forces: enforcement-only
|
|
148
|
+
// compliance was measured missing on a double-digit share of turns. Not
|
|
149
|
+
// hash-deduped for the same reason as the Codex footer.
|
|
150
|
+
if (turnRitualNudge) {
|
|
151
|
+
const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, turnRitualNudge);
|
|
152
|
+
if (ritualMsg) sections.push(ritualMsg);
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
return sections.join("\n\n");
|
|
132
156
|
}
|
|
133
157
|
|
|
134
|
-
function
|
|
158
|
+
function renderTurnRitualReminder(
|
|
159
|
+
coordRoot: string,
|
|
160
|
+
selfInstanceId: string,
|
|
161
|
+
adapter: string,
|
|
162
|
+
): string {
|
|
135
163
|
const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
|
|
136
164
|
if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) return "";
|
|
137
165
|
|
|
138
166
|
const bin = resolveBinName(coordRoot);
|
|
167
|
+
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
168
|
+
// Claude Code renders the status box only if the reply carries it; Cursor
|
|
169
|
+
// shows the box inline on its own, so it is never asked for a second copy.
|
|
170
|
+
const paste =
|
|
171
|
+
adapter === "claude-code"
|
|
172
|
+
? " and paste its output verbatim in a fenced code block at the end of your reply"
|
|
173
|
+
: "";
|
|
174
|
+
return (
|
|
175
|
+
`Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
|
|
176
|
+
"A turn that ends without these is bounced by the Stop hook and costs a retry."
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderStatusFooterReminder(coordRoot: string, selfInstanceId: string): string {
|
|
181
|
+
const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
|
|
182
|
+
if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) return "";
|
|
183
|
+
|
|
184
|
+
const statusCommand = endOfTurnStatusCommand(coordRoot);
|
|
139
185
|
return (
|
|
140
186
|
"Codex status footer: complete the user's request first. " +
|
|
141
|
-
`Then run \`${
|
|
187
|
+
`Then run \`${statusCommand}\` as your final shell call and append its stdout verbatim in a fenced code block at the bottom of the same substantive reply. ` +
|
|
188
|
+
"If it fails, finish the owned Git work and rerun it before replying. " +
|
|
142
189
|
"Keep the answer intact. If the footer is missed, do not retry or replace the reply; the Stop hook is observe-only."
|
|
143
190
|
);
|
|
144
191
|
}
|
|
@@ -176,13 +223,21 @@ function computeFocusNudgeIfChanged(
|
|
|
176
223
|
let message = "";
|
|
177
224
|
let nudgeKind = "";
|
|
178
225
|
|
|
179
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
opts.sessionNameNudge &&
|
|
228
|
+
(!hb.suggested_session_name ||
|
|
229
|
+
(hb.session_name_seen_for !== undefined &&
|
|
230
|
+
hb.suggested_session_name !== hb.session_name_seen_for))
|
|
231
|
+
) {
|
|
232
|
+
// Keyed on "a name was ever produced", not task_updated_at: a bare clear
|
|
233
|
+
// as the first declaration must not end the naming window.
|
|
180
234
|
needsNudge = true;
|
|
181
235
|
nudgeKind = "session-name";
|
|
182
|
-
message =
|
|
183
|
-
`
|
|
184
|
-
|
|
185
|
-
|
|
236
|
+
message = hb.suggested_session_name
|
|
237
|
+
? `This session's lifecycle changed its suggested name. Reproduce this value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title: ${hb.suggested_session_name}`
|
|
238
|
+
: `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
|
|
239
|
+
"When it returns `first_of_session: true`, reproduce its `suggested_session_name` value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title. " +
|
|
240
|
+
"Then continue with the task; that `set-task` also satisfies this turn's focus declaration.";
|
|
186
241
|
} else if (opts.taskNudge && !taskValue) {
|
|
187
242
|
needsNudge = true;
|
|
188
243
|
nudgeKind = "task-unset";
|
|
@@ -207,8 +262,18 @@ function computeFocusNudgeIfChanged(
|
|
|
207
262
|
return "";
|
|
208
263
|
}
|
|
209
264
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
265
|
+
// The session-name reminder is deliberately NOT deduped: it re-emits on
|
|
266
|
+
// every prompt until a name is produced, because the "still unnamed" state
|
|
267
|
+
// is the failure state — deduping it erases the only reminder after one
|
|
268
|
+
// ignored prompt (the miss mode operators reported). Bounded in practice:
|
|
269
|
+
// Stop rule 3/3 forces a set-task on the first tool-using turn.
|
|
270
|
+
if (nudgeKind === "session-name") {
|
|
271
|
+
clearHashFile(hashFile);
|
|
272
|
+
return message;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Dedup the task-unset/stale reminders on kind + task state so a repeat
|
|
276
|
+
// prompt in the same state stays quiet.
|
|
212
277
|
const state = `${nudgeKind}|task=${taskValue}|updated=${hb.task_updated_at ?? ""}|threshold=${threshold}`;
|
|
213
278
|
const newHash = sha256Hex16(state);
|
|
214
279
|
const oldHash = safeRead(hashFile);
|
|
@@ -254,6 +319,9 @@ function computePeerTableIfChanged(
|
|
|
254
319
|
started_at: p.started_at ?? null,
|
|
255
320
|
files_touched: Array.from(p.files_touched ?? []).sort(),
|
|
256
321
|
platform: p.platform ?? null,
|
|
322
|
+
activity: p.activity ?? "unknown",
|
|
323
|
+
task_state: p.task_state ?? "active",
|
|
324
|
+
task_state_reason: p.task_state_reason ?? null,
|
|
257
325
|
}))
|
|
258
326
|
.sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
|
|
259
327
|
// Remote basis: machine + per-agent identity/task/files (published_at is
|
|
@@ -264,6 +332,9 @@ function computePeerTableIfChanged(
|
|
|
264
332
|
instance_id: a.instance_id,
|
|
265
333
|
name: a.name ?? null,
|
|
266
334
|
task: a.task ?? null,
|
|
335
|
+
activity: a.activity,
|
|
336
|
+
task_state: a.task_state,
|
|
337
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
267
338
|
files_touched: Array.from(a.files_touched ?? []).sort(),
|
|
268
339
|
})),
|
|
269
340
|
}));
|
|
@@ -478,6 +549,7 @@ function renderSubtable(
|
|
|
478
549
|
|
|
479
550
|
function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
|
|
480
551
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
552
|
+
const statePart = formatState(r);
|
|
481
553
|
// Fall back started_at → last_heartbeat; if neither is a valid timestamp,
|
|
482
554
|
// show "age unknown" rather than the epoch-derived "20608d ago" ghost.
|
|
483
555
|
const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
|
|
@@ -488,7 +560,15 @@ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number
|
|
|
488
560
|
// Prefer a short instance_id over a bare "unknown" so an incomplete row is
|
|
489
561
|
// still identifiable.
|
|
490
562
|
const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
|
|
491
|
-
return ` - agent-${label}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
563
|
+
return ` - agent-${label}${taskPart} (${statePart}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function formatState(r: HeartbeatRow): string {
|
|
567
|
+
const activity = r.activity ?? "unknown";
|
|
568
|
+
const lifecycle = r.task_state ?? "active";
|
|
569
|
+
const reason =
|
|
570
|
+
lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
571
|
+
return `activity=${activity}, lifecycle=${lifecycle}${reason}`;
|
|
492
572
|
}
|
|
493
573
|
|
|
494
574
|
function fmtFiles(files: string[]): string {
|
|
@@ -13,6 +13,8 @@ import { join } from "node:path";
|
|
|
13
13
|
import { resolveBinName, resolveHooksSetupHint } from "../../config.ts";
|
|
14
14
|
import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.ts";
|
|
15
15
|
import { readRemoteMachines } from "../../presence/index.ts";
|
|
16
|
+
import { readForkParent } from "../state/names.ts";
|
|
17
|
+
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
16
18
|
|
|
17
19
|
interface HeartbeatRow {
|
|
18
20
|
instance_id?: string;
|
|
@@ -26,6 +28,9 @@ interface HeartbeatRow {
|
|
|
26
28
|
files_touched?: string[];
|
|
27
29
|
platform?: string;
|
|
28
30
|
task?: string;
|
|
31
|
+
activity?: AgentActivity;
|
|
32
|
+
task_state?: TaskState;
|
|
33
|
+
task_state_reason?: string;
|
|
29
34
|
turn_summary?: string;
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -54,7 +59,19 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
54
59
|
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
55
60
|
if (agentName) {
|
|
56
61
|
const suffix = platformLabel ? ` (${platformLabel})` : "";
|
|
57
|
-
|
|
62
|
+
// The authority clause guards forked/branched chats. An adapter that forks a
|
|
63
|
+
// conversation copies the parent's transcript, so the fork's context still
|
|
64
|
+
// asserts the parent's name, claims, and task. When recorded fork lineage
|
|
65
|
+
// (.name-history forked_from) names the parent, say so specifically — the
|
|
66
|
+
// fork is about to read a transcript full of that exact name. Otherwise
|
|
67
|
+
// fall back to the generic clause, which needs no fork detection.
|
|
68
|
+
const forkParent = readForkParent(coordRoot, instanceId);
|
|
69
|
+
const authority = forkParent?.name
|
|
70
|
+
? `This conversation was branched from agent-${forkParent.name}'s session: earlier context showing agent-${forkParent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`
|
|
71
|
+
: forkParent
|
|
72
|
+
? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
|
|
73
|
+
: "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
74
|
+
const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
|
|
58
75
|
messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
|
|
59
76
|
} else if (peerTable) {
|
|
60
77
|
messages.push(peerTable);
|
|
@@ -79,7 +96,7 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
79
96
|
const hint = resolveHooksSetupHint(coordRoot);
|
|
80
97
|
const fix = hint
|
|
81
98
|
? `Run \`${hint}\` to install them.`
|
|
82
|
-
:
|
|
99
|
+
: `Run \`${resolveBinName(coordRoot)} init\` to install the harnery-managed git-hook regions (commit guard + claim pruning).`;
|
|
83
100
|
const wiringSummary = `Coordination hooks are NOT wired: the E-guard will not block conflicting commits, and post-commit claim pruning will not run. ${fix} Detected:\n${wiringIssues.map((i) => ` - ${i}`).join("\n")}`;
|
|
84
101
|
messages.push(wiringSummary);
|
|
85
102
|
}
|
|
@@ -122,8 +139,12 @@ function formatRemoteMachines(coordRoot: string): string {
|
|
|
122
139
|
const files = a.files_touched?.length
|
|
123
140
|
? `holds: ${a.files_touched.slice(0, 3).join(", ")}${a.files_touched.length > 3 ? `, +${a.files_touched.length - 3} more` : ""}`
|
|
124
141
|
: "nothing held";
|
|
142
|
+
const reason =
|
|
143
|
+
a.task_state === "blocked" && a.task_state_reason
|
|
144
|
+
? `: ${a.task_state_reason.slice(0, 80)}`
|
|
145
|
+
: "";
|
|
125
146
|
lines.push(
|
|
126
|
-
` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (${files})`,
|
|
147
|
+
` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (activity=${a.activity}, lifecycle=${a.task_state}${reason}, ${files})`,
|
|
127
148
|
);
|
|
128
149
|
}
|
|
129
150
|
}
|
|
@@ -217,11 +238,15 @@ function renderSubtable(
|
|
|
217
238
|
|
|
218
239
|
function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
|
|
219
240
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
241
|
+
const activity = r.activity ?? "unknown";
|
|
242
|
+
const lifecycle = r.task_state ?? "active";
|
|
243
|
+
const reason =
|
|
244
|
+
lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
220
245
|
const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
|
|
221
246
|
const filesPart = fmtFiles(r.display_files);
|
|
222
247
|
const lastActivity = fmtLastActivity(r, nowSec);
|
|
223
248
|
const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
|
|
224
|
-
return ` - agent-${r.name ?? "unknown"}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
249
|
+
return ` - agent-${r.name ?? "unknown"}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
225
250
|
}
|
|
226
251
|
|
|
227
252
|
function fmtFiles(files: string[]): string {
|
|
@@ -271,8 +271,8 @@ function isFresh(lastHeartbeat: string): boolean {
|
|
|
271
271
|
* - diff shows non-empty output (genuinely dirty)
|
|
272
272
|
*/
|
|
273
273
|
function isFileCommittedClean(coordRoot: string, relPath: string): boolean {
|
|
274
|
-
//
|
|
275
|
-
//
|
|
274
|
+
// Tool payloads can provide either absolute-under-coordRoot or canonical
|
|
275
|
+
// monorepo-relative paths.
|
|
276
276
|
const rel = relPath.startsWith(`${coordRoot}/`) ? relPath.slice(coordRoot.length + 1) : relPath;
|
|
277
277
|
const abs = join(coordRoot, rel);
|
|
278
278
|
if (!existsSync(abs)) return false;
|
|
@@ -305,7 +305,7 @@ function pruneClaimFromPeer(coordRoot: string, instanceId: string, relPath: stri
|
|
|
305
305
|
const body = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
306
306
|
const files = (body.files_touched as string[] | undefined) ?? [];
|
|
307
307
|
// Normalize both sides: files_touched can hold absolute-under-coordRoot
|
|
308
|
-
// entries
|
|
308
|
+
// entries as well as canonical relative ones — an
|
|
309
309
|
// exact-string filter silently no-ops on the mixed-form case and the
|
|
310
310
|
// stale claim never heals.
|
|
311
311
|
const norm = (p: string): string =>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pre-commit E-guard verdict. The caller (typically a bash pre-commit hook)
|
|
3
|
-
* sends a JSON request with the staged paths
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* sends a JSON request with the staged paths and prints the verdict
|
|
4
|
+
* messages + exits with verdict.exit_code. Committer identity is resolved
|
|
5
|
+
* HERE via `resolveOwner()` unless the request explicitly carries one.
|
|
6
6
|
*
|
|
7
7
|
* Three outcomes:
|
|
8
8
|
* 1. allow (no conflicts): exit 0.
|
|
@@ -24,6 +24,7 @@ import { spawnSync } from "node:child_process";
|
|
|
24
24
|
import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
25
25
|
import { basename, dirname, join } from "node:path";
|
|
26
26
|
import { coordFreshnessSeconds } from "../../config.ts";
|
|
27
|
+
import { resolveOwner } from "../../hooks/resolve/owner.ts";
|
|
27
28
|
import { instanceHasLivePid } from "../state/pidmap.ts";
|
|
28
29
|
|
|
29
30
|
interface PeerHeartbeat {
|
|
@@ -46,9 +47,16 @@ interface Conflict {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export interface CommitVerdictRequest {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Committer identity. Optional: when absent (or the legacy
|
|
52
|
+
* `__unattributed__` sentinel), the verdict resolves it via
|
|
53
|
+
* `resolveOwner()` — env → pid-map walk → validated Codex thread id —
|
|
54
|
+
* from its own process context. Callers should stop hand-rolling
|
|
55
|
+
* identity resolution and omit this.
|
|
56
|
+
*/
|
|
57
|
+
instance_id?: string;
|
|
58
|
+
/** Group key. Subagents inherit parent's session_id. Same optionality. */
|
|
59
|
+
session_id?: string;
|
|
52
60
|
/** Canonical monorepo-relative paths. */
|
|
53
61
|
staged_paths: string[];
|
|
54
62
|
/** Paths in `staged_paths` that resolve to submodule gitlinks (mode 160000)
|
|
@@ -62,6 +70,8 @@ export interface CommitVerdictResult {
|
|
|
62
70
|
allow: boolean;
|
|
63
71
|
exit_code: number;
|
|
64
72
|
rule: string;
|
|
73
|
+
/** The committer identity the verdict actually used (resolved or passed). */
|
|
74
|
+
instance_id: string;
|
|
65
75
|
/** Conflict details (for the caller to print). */
|
|
66
76
|
conflicts: Conflict[];
|
|
67
77
|
/** Path-specific gates that fired (for `coord_log` lines). */
|
|
@@ -73,11 +83,24 @@ export interface CommitVerdictResult {
|
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): CommitVerdictResult {
|
|
86
|
+
// Resolve the committer here, not in the calling hook. The bash shims used
|
|
87
|
+
// to carry their own pid-map walk (a pre-extraction fossil of resolveOwner);
|
|
88
|
+
// a hand-rolled copy can only fall behind as tiers are added, so the rule
|
|
89
|
+
// resolves identity from its own process context and callers just omit it.
|
|
90
|
+
const passed = req.instance_id?.trim();
|
|
91
|
+
const me =
|
|
92
|
+
passed && passed !== "__unattributed__"
|
|
93
|
+
? passed
|
|
94
|
+
: (resolveOwner({ payload: null, coordRoot })?.instance_id ?? "__unattributed__");
|
|
95
|
+
const passedSession = req.session_id?.trim();
|
|
96
|
+
const mySession = passed && passedSession ? passedSession : me;
|
|
97
|
+
|
|
76
98
|
if (req.staged_paths.length === 0) {
|
|
77
99
|
return {
|
|
78
100
|
allow: true,
|
|
79
101
|
exit_code: 0,
|
|
80
102
|
rule: "commit.pass",
|
|
103
|
+
instance_id: me,
|
|
81
104
|
conflicts: [],
|
|
82
105
|
log_lines: [],
|
|
83
106
|
message: "",
|
|
@@ -91,9 +114,9 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
91
114
|
|
|
92
115
|
const conflicts: Conflict[] = [];
|
|
93
116
|
for (const peer of peers) {
|
|
94
|
-
if (peer.instance_id ===
|
|
117
|
+
if (peer.instance_id === me) continue;
|
|
95
118
|
const peerSession = peer.session_id ?? peer.instance_id;
|
|
96
|
-
if (
|
|
119
|
+
if (mySession !== "__unattributed__" && peerSession === mySession) continue; // same group
|
|
97
120
|
const ts = peer.last_heartbeat ? Date.parse(peer.last_heartbeat) : 0;
|
|
98
121
|
if (!Number.isFinite(ts) || ts < cutoffMs) continue; // stale
|
|
99
122
|
const files = peer.files_touched ?? [];
|
|
@@ -118,6 +141,7 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
118
141
|
allow: true,
|
|
119
142
|
exit_code: 0,
|
|
120
143
|
rule: "commit.pass",
|
|
144
|
+
instance_id: me,
|
|
121
145
|
conflicts: [],
|
|
122
146
|
log_lines: [],
|
|
123
147
|
message: "",
|
|
@@ -129,6 +153,7 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
129
153
|
allow: true,
|
|
130
154
|
exit_code: 0,
|
|
131
155
|
rule: "commit.bypass",
|
|
156
|
+
instance_id: me,
|
|
132
157
|
conflicts,
|
|
133
158
|
log_lines: conflicts.map(
|
|
134
159
|
(c) => `COMMIT_BYPASSED path=${c.staged_path} owner=${c.short_name}`,
|
|
@@ -149,6 +174,7 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
149
174
|
allow: true,
|
|
150
175
|
exit_code: 0,
|
|
151
176
|
rule: "commit.suppressed",
|
|
177
|
+
instance_id: me,
|
|
152
178
|
conflicts,
|
|
153
179
|
log_lines: conflicts.map(
|
|
154
180
|
(c) =>
|
|
@@ -167,6 +193,7 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
167
193
|
allow: false,
|
|
168
194
|
exit_code: 1,
|
|
169
195
|
rule: "commit.conflict",
|
|
196
|
+
instance_id: me,
|
|
170
197
|
conflicts,
|
|
171
198
|
log_lines: conflicts.map((c) => `COMMIT_BLOCKED path=${c.staged_path} owner=${c.short_name}`),
|
|
172
199
|
message:
|