harnery 0.31.6 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +0 -9
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +70 -95
  6. package/dist/commands/browse-session.d.ts +21 -0
  7. package/dist/commands/browse-session.d.ts.map +1 -0
  8. package/dist/commands/browse-session.js +157 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +205 -37
  11. package/dist/commands/checkpoint.js +1 -1
  12. package/dist/commands/deinit.d.ts.map +1 -1
  13. package/dist/commands/deinit.js +4 -0
  14. package/dist/commands/docs.d.ts.map +1 -1
  15. package/dist/commands/docs.js +40 -0
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +100 -17
  18. package/dist/commands/init.d.ts +1 -2
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +16 -5
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +2 -3
  23. package/dist/core/agents/canonical-emit.d.ts +1 -2
  24. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  25. package/dist/core/agents/canonical-emit.js +1 -2
  26. package/dist/core/agents/cli.js +123 -88
  27. package/dist/core/agents/coord-client.d.ts +7 -3
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +5 -4
  30. package/dist/core/agents/finalization.d.ts +68 -0
  31. package/dist/core/agents/finalization.d.ts.map +1 -0
  32. package/dist/core/agents/finalization.js +443 -0
  33. package/dist/core/agents/git-hook.d.ts +51 -0
  34. package/dist/core/agents/git-hook.d.ts.map +1 -0
  35. package/dist/core/agents/git-hook.js +118 -0
  36. package/dist/core/agents/render/prompt-context.d.ts +6 -5
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +26 -13
  39. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/session-context.js +15 -2
  41. package/dist/core/agents/rules/claim-conflict.js +3 -3
  42. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +21 -5
  45. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  46. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  47. package/dist/core/agents/rules/stop-hook.js +59 -23
  48. package/dist/core/agents/session-events.d.ts +8 -16
  49. package/dist/core/agents/session-events.d.ts.map +1 -1
  50. package/dist/core/agents/session-events.js +12 -26
  51. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
  52. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  53. package/dist/core/agents/state/heartbeat-projector.js +44 -1
  54. package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
  55. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/heartbeat-writer.js +54 -17
  57. package/dist/core/agents/state/names.d.ts +22 -1
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +40 -2
  60. package/dist/core/config.d.ts +30 -2
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +74 -11
  63. package/dist/core/governor/planning.d.ts.map +1 -1
  64. package/dist/core/governor/planning.js +12 -13
  65. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  66. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  67. package/dist/core/hooks/adapter/detect.js +4 -14
  68. package/dist/core/hooks/adapter/output.d.ts +1 -1
  69. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  70. package/dist/core/hooks/adapter/output.js +5 -4
  71. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +32 -0
  74. package/dist/core/hooks/cli.d.ts +1 -2
  75. package/dist/core/hooks/cli.d.ts.map +1 -1
  76. package/dist/core/hooks/cli.js +169 -27
  77. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  78. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  79. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  80. package/dist/core/hooks/effects/index.d.ts +1 -1
  81. package/dist/core/hooks/effects/index.js +3 -3
  82. package/dist/core/hooks/events/schema.d.ts +27 -0
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  85. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  86. package/dist/core/hooks/resolve/owner.js +15 -0
  87. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  88. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/transcript.js +176 -1
  90. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  91. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  92. package/dist/core/hooks/session-name-presence.js +43 -0
  93. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  94. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  95. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  96. package/dist/core/work/state.d.ts +4 -5
  97. package/dist/core/work/state.d.ts.map +1 -1
  98. package/dist/core/work/state.js +5 -8
  99. package/dist/core/workflow/index.d.ts +1 -1
  100. package/dist/core/workflow/index.d.ts.map +1 -1
  101. package/dist/core/workflow/proof.d.ts +2 -2
  102. package/dist/core/workflow/proof.d.ts.map +1 -1
  103. package/dist/core/workflow/proof.js +2 -2
  104. package/dist/core/workflow/run-state.d.ts +2 -2
  105. package/dist/core/workflow/run-state.d.ts.map +1 -1
  106. package/dist/core/workflow/run-state.js +2 -2
  107. package/dist/core/workflow/types.d.ts +3 -3
  108. package/dist/core/workflow/types.d.ts.map +1 -1
  109. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  110. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  111. package/dist/core/workflow/workspaces/execution.js +5 -0
  112. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  113. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  115. package/dist/core/workflow/workspaces/local-git.js +1 -2
  116. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  117. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  118. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  119. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  120. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  121. package/dist/core/workflow/workspaces/validate.js +0 -2
  122. package/dist/lib/browser/client.d.ts +118 -1
  123. package/dist/lib/browser/client.d.ts.map +1 -1
  124. package/dist/lib/browser/client.js +435 -6
  125. package/dist/lib/browser/geometry.d.ts.map +1 -1
  126. package/dist/lib/browser/geometry.js +180 -37
  127. package/dist/lib/browser/index.d.ts +5 -1
  128. package/dist/lib/browser/index.d.ts.map +1 -1
  129. package/dist/lib/browser/index.js +5 -1
  130. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  131. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  132. package/dist/lib/browser/netscape-cookies.js +31 -0
  133. package/dist/lib/browser/proxy.d.ts +24 -0
  134. package/dist/lib/browser/proxy.d.ts.map +1 -0
  135. package/dist/lib/browser/proxy.js +84 -0
  136. package/dist/lib/browser/runts.d.ts +6 -0
  137. package/dist/lib/browser/runts.d.ts.map +1 -1
  138. package/dist/lib/browser/runts.js +21 -4
  139. package/dist/lib/browser/session-control.d.ts +112 -0
  140. package/dist/lib/browser/session-control.d.ts.map +1 -0
  141. package/dist/lib/browser/session-control.js +670 -0
  142. package/dist/lib/docs-links.d.ts +108 -0
  143. package/dist/lib/docs-links.d.ts.map +1 -0
  144. package/dist/lib/docs-links.js +555 -0
  145. package/dist/lib/exec.d.ts +1 -1
  146. package/dist/lib/exec.js +1 -1
  147. package/dist/lib/identities/assume.d.ts +4 -2
  148. package/dist/lib/identities/assume.d.ts.map +1 -1
  149. package/dist/lib/identities/assume.js +15 -2
  150. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  151. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  152. package/dist/lib/instructions/git-hooks.js +238 -0
  153. package/dist/lib/instructions/splice.d.ts +10 -4
  154. package/dist/lib/instructions/splice.d.ts.map +1 -1
  155. package/dist/lib/instructions/splice.js +19 -11
  156. package/package.json +1 -1
  157. package/schemas/config.schema.json +32 -0
  158. package/src/commander.ts +2 -0
  159. package/src/commands/agents.ts +96 -115
  160. package/src/commands/browse-session.ts +245 -0
  161. package/src/commands/browse.ts +287 -44
  162. package/src/commands/checkpoint.ts +1 -1
  163. package/src/commands/deinit.ts +7 -1
  164. package/src/commands/docs.ts +56 -0
  165. package/src/commands/doctor.ts +100 -18
  166. package/src/commands/init.ts +17 -9
  167. package/src/commands/tunnel.ts +2 -5
  168. package/src/core/agents/canonical-emit.ts +1 -2
  169. package/src/core/agents/cli.ts +137 -88
  170. package/src/core/agents/coord-client.ts +10 -5
  171. package/src/core/agents/finalization.ts +595 -0
  172. package/src/core/agents/git-hook.ts +126 -0
  173. package/src/core/agents/render/prompt-context.ts +28 -13
  174. package/src/core/agents/render/session-context.ts +15 -2
  175. package/src/core/agents/rules/claim-conflict.ts +3 -3
  176. package/src/core/agents/rules/commit-conflict.ts +35 -8
  177. package/src/core/agents/rules/stop-hook.ts +71 -23
  178. package/src/core/agents/session-events.ts +14 -41
  179. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  180. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  181. package/src/core/agents/state/names.ts +54 -2
  182. package/src/core/config.ts +92 -12
  183. package/src/core/governor/planning.ts +17 -12
  184. package/src/core/hooks/adapter/detect.ts +4 -13
  185. package/src/core/hooks/adapter/output.ts +9 -4
  186. package/src/core/hooks/adapter/wiring.ts +43 -0
  187. package/src/core/hooks/cli.ts +201 -23
  188. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  189. package/src/core/hooks/effects/index.ts +5 -5
  190. package/src/core/hooks/events/schema.ts +27 -0
  191. package/src/core/hooks/resolve/owner.ts +22 -1
  192. package/src/core/hooks/resolve/transcript.ts +164 -1
  193. package/src/core/hooks/session-name-presence.ts +52 -0
  194. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  195. package/src/core/work/state.ts +8 -12
  196. package/src/core/workflow/engine.ts +2 -2
  197. package/src/core/workflow/index.ts +0 -1
  198. package/src/core/workflow/proof.ts +4 -4
  199. package/src/core/workflow/run-state.ts +5 -5
  200. package/src/core/workflow/types.ts +3 -3
  201. package/src/core/workflow/workspaces/execution.ts +11 -4
  202. package/src/core/workflow/workspaces/index.ts +0 -3
  203. package/src/core/workflow/workspaces/local-git.ts +1 -2
  204. package/src/core/workflow/workspaces/paths.ts +2 -2
  205. package/src/core/workflow/workspaces/types.ts +0 -9
  206. package/src/core/workflow/workspaces/validate.ts +1 -5
  207. package/src/lib/browser/client.ts +528 -6
  208. package/src/lib/browser/geometry.ts +197 -37
  209. package/src/lib/browser/index.ts +39 -0
  210. package/src/lib/browser/netscape-cookies.ts +39 -0
  211. package/src/lib/browser/proxy.ts +105 -0
  212. package/src/lib/browser/runts.ts +27 -3
  213. package/src/lib/browser/session-control.ts +892 -0
  214. package/src/lib/docs-links.ts +674 -0
  215. package/src/lib/exec.ts +1 -1
  216. package/src/lib/identities/assume.ts +22 -1
  217. package/src/lib/instructions/git-hooks.ts +259 -0
  218. package/src/lib/instructions/splice.ts +41 -11
@@ -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
+ }
@@ -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. Before the first set-task in a human-facing session, tells
16
- * every adapter to reproduce set-task's suggested session name in a fenced
17
- * code block. Cursor/Codex additionally get the existing unset/stale-task
18
- * reminder because their Stop hooks do not enforce task declarations as
19
- * reliably as Claude Code's.
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.
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAoCH,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;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA0CnE"}
1
+ {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAqCH,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;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA0CnE"}
@@ -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. Before the first set-task in a human-facing session, tells
16
- * every adapter to reproduce set-task's suggested session name in a fenced
17
- * code block. Cursor/Codex additionally get the existing unset/stale-task
18
- * reminder because their Stop hooks do not enforce task declarations as
19
- * reliably as Claude Code's.
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.
@@ -32,7 +33,7 @@ import { createHash } from "node:crypto";
32
33
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
33
34
  import { join } from "node:path";
34
35
  import { coordEnv } from "../../../lib/env.js";
35
- import { resolveBinName } from "../../config.js";
36
+ import { endOfTurnStatusCommand, resolveBinName } from "../../config.js";
36
37
  import { readRemoteMachines } from "../../presence/index.js";
37
38
  import { formatPendingCouncils } from "./session-context.js";
38
39
  /**
@@ -81,9 +82,10 @@ function renderStatusFooterReminder(coordRoot, selfInstanceId) {
81
82
  const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
82
83
  if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
83
84
  return "";
84
- const bin = resolveBinName(coordRoot);
85
+ const statusCommand = endOfTurnStatusCommand(coordRoot);
85
86
  return ("Codex status footer: complete the user's request first. " +
86
- `Then run \`${bin} agents status\` as your final shell call and append its stdout verbatim in a fenced code block at the bottom of the same substantive reply. ` +
87
+ `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. ` +
88
+ "If it fails, finish the owned Git work and rerun it before replying. " +
87
89
  "Keep the answer intact. If the footer is missed, do not retry or replace the reply; the Stop hook is observe-only.");
88
90
  }
89
91
  /**
@@ -114,13 +116,15 @@ function computeFocusNudgeIfChanged(coordRoot, selfInstanceId, opts) {
114
116
  let needsNudge = false;
115
117
  let message = "";
116
118
  let nudgeKind = "";
117
- if (opts.sessionNameNudge && !hb.task_updated_at) {
119
+ if (opts.sessionNameNudge && !hb.suggested_session_name) {
120
+ // Keyed on "a name was ever produced", not task_updated_at: a bare clear
121
+ // as the first declaration must not end the naming window.
118
122
  needsNudge = true;
119
123
  nudgeKind = "session-name";
120
124
  message =
121
- `New session: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
125
+ `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
122
126
  "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. " +
123
- "Then continue with the task; that first `set-task` also satisfies this turn's focus declaration.";
127
+ "Then continue with the task; that `set-task` also satisfies this turn's focus declaration.";
124
128
  }
125
129
  else if (opts.taskNudge && !taskValue) {
126
130
  needsNudge = true;
@@ -145,8 +149,17 @@ function computeFocusNudgeIfChanged(coordRoot, selfInstanceId, opts) {
145
149
  clearHashFile(hashFile);
146
150
  return "";
147
151
  }
148
- // Dedup on kind + task state so the first-session reminder does not turn into
149
- // a generic "task unset" reminder on the next prompt before set-task runs.
152
+ // The session-name reminder is deliberately NOT deduped: it re-emits on
153
+ // every prompt until a name is produced, because the "still unnamed" state
154
+ // is the failure state — deduping it erases the only reminder after one
155
+ // ignored prompt (the miss mode operators reported). Bounded in practice:
156
+ // Stop rule 3/3 forces a set-task on the first tool-using turn.
157
+ if (nudgeKind === "session-name") {
158
+ clearHashFile(hashFile);
159
+ return message;
160
+ }
161
+ // Dedup the task-unset/stale reminders on kind + task state so a repeat
162
+ // prompt in the same state stays quiet.
150
163
  const state = `${nudgeKind}|task=${taskValue}|updated=${hb.task_updated_at ?? ""}|threshold=${threshold}`;
151
164
  const newHash = sha256Hex16(state);
152
165
  const oldHash = safeRead(hashFile);
@@ -1 +1 @@
1
- {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAqB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiE7D;AA+ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
1
+ {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAsB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6E7D;AA+ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { resolveBinName, resolveHooksSetupHint } from "../../config.js";
13
13
  import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.js";
14
14
  import { readRemoteMachines } from "../../presence/index.js";
15
+ import { readForkParent } from "../state/names.js";
15
16
  /**
16
17
  * Build the combined SessionStart systemMessage. Returns the additionalContext
17
18
  * string (or "" if there's nothing to say).
@@ -26,7 +27,19 @@ export function renderSessionContext(opts) {
26
27
  const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
27
28
  if (agentName) {
28
29
  const suffix = platformLabel ? ` (${platformLabel})` : "";
29
- const selfLine = `You are agent-${agentName}${suffix}.`;
30
+ // The authority clause guards forked/branched chats. An adapter that forks a
31
+ // conversation copies the parent's transcript, so the fork's context still
32
+ // asserts the parent's name, claims, and task. When recorded fork lineage
33
+ // (.name-history forked_from) names the parent, say so specifically — the
34
+ // fork is about to read a transcript full of that exact name. Otherwise
35
+ // fall back to the generic clause, which needs no fork detection.
36
+ const forkParent = readForkParent(coordRoot, instanceId);
37
+ const authority = forkParent?.name
38
+ ? `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.`
39
+ : forkParent
40
+ ? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
41
+ : "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
42
+ const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
30
43
  messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
31
44
  }
32
45
  else if (peerTable) {
@@ -48,7 +61,7 @@ export function renderSessionContext(opts) {
48
61
  const hint = resolveHooksSetupHint(coordRoot);
49
62
  const fix = hint
50
63
  ? `Run \`${hint}\` to install them.`
51
- : "Wire each repo's pre-commit hook to invoke `agent-coord verdict --rule=commit` (harnery's commit guard).";
64
+ : `Run \`${resolveBinName(coordRoot)} init\` to install the harnery-managed git-hook regions (commit guard + claim pruning).`;
52
65
  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")}`;
53
66
  messages.push(wiringSummary);
54
67
  }
@@ -232,8 +232,8 @@ function isFresh(lastHeartbeat) {
232
232
  * - diff shows non-empty output (genuinely dirty)
233
233
  */
234
234
  function isFileCommittedClean(coordRoot, relPath) {
235
- // Tolerate either path form: files_touched can hold absolute-under-coordRoot
236
- // entries (legacy file-tracking) or canonical monorepo-relative ones.
235
+ // Tool payloads can provide either absolute-under-coordRoot or canonical
236
+ // monorepo-relative paths.
237
237
  const rel = relPath.startsWith(`${coordRoot}/`) ? relPath.slice(coordRoot.length + 1) : relPath;
238
238
  const abs = join(coordRoot, rel);
239
239
  if (!existsSync(abs))
@@ -269,7 +269,7 @@ function pruneClaimFromPeer(coordRoot, instanceId, relPath) {
269
269
  const body = JSON.parse(readFileSync(path, "utf8"));
270
270
  const files = body.files_touched ?? [];
271
271
  // Normalize both sides: files_touched can hold absolute-under-coordRoot
272
- // entries (legacy projections) as well as canonical relative ones — an
272
+ // entries as well as canonical relative ones — an
273
273
  // exact-string filter silently no-ops on the mixed-form case and the
274
274
  // stale claim never heals.
275
275
  const norm = (p) => p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
@@ -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, resolves its own instance_id
4
- * via pid-map, and prints the verdict messages + exits with
5
- * verdict.exit_code.
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.
@@ -30,9 +30,16 @@ interface Conflict {
30
30
  short_name: string;
31
31
  }
32
32
  export interface CommitVerdictRequest {
33
- instance_id: string;
34
- /** Group key. Subagents inherit parent's session_id. */
35
- session_id: string;
33
+ /**
34
+ * Committer identity. Optional: when absent (or the legacy
35
+ * `__unattributed__` sentinel), the verdict resolves it via
36
+ * `resolveOwner()` — env → pid-map walk → validated Codex thread id —
37
+ * from its own process context. Callers should stop hand-rolling
38
+ * identity resolution and omit this.
39
+ */
40
+ instance_id?: string;
41
+ /** Group key. Subagents inherit parent's session_id. Same optionality. */
42
+ session_id?: string;
36
43
  /** Canonical monorepo-relative paths. */
37
44
  staged_paths: string[];
38
45
  /** Paths in `staged_paths` that resolve to submodule gitlinks (mode 160000)
@@ -45,6 +52,8 @@ export interface CommitVerdictResult {
45
52
  allow: boolean;
46
53
  exit_code: number;
47
54
  rule: string;
55
+ /** The committer identity the verdict actually used (resolved or passed). */
56
+ instance_id: string;
48
57
  /** Conflict details (for the caller to print). */
49
58
  conflicts: Conflict[];
50
59
  /** Path-specific gates that fired (for `coord_log` lines). */
@@ -1 +1 @@
1
- {"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAsGhG"}
1
+ {"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAiBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAuHhG"}
@@ -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, resolves its own instance_id
4
- * via pid-map, and prints the verdict messages + exits with
5
- * verdict.exit_code.
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.
@@ -23,13 +23,25 @@ import { spawnSync } from "node:child_process";
23
23
  import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
24
24
  import { basename, dirname, join } from "node:path";
25
25
  import { coordFreshnessSeconds } from "../../config.js";
26
+ import { resolveOwner } from "../../hooks/resolve/owner.js";
26
27
  import { instanceHasLivePid } from "../state/pidmap.js";
27
28
  export function evaluateCommit(coordRoot, req) {
29
+ // Resolve the committer here, not in the calling hook. The bash shims used
30
+ // to carry their own pid-map walk (a pre-extraction fossil of resolveOwner);
31
+ // a hand-rolled copy can only fall behind as tiers are added, so the rule
32
+ // resolves identity from its own process context and callers just omit it.
33
+ const passed = req.instance_id?.trim();
34
+ const me = passed && passed !== "__unattributed__"
35
+ ? passed
36
+ : (resolveOwner({ payload: null, coordRoot })?.instance_id ?? "__unattributed__");
37
+ const passedSession = req.session_id?.trim();
38
+ const mySession = passed && passedSession ? passedSession : me;
28
39
  if (req.staged_paths.length === 0) {
29
40
  return {
30
41
  allow: true,
31
42
  exit_code: 0,
32
43
  rule: "commit.pass",
44
+ instance_id: me,
33
45
  conflicts: [],
34
46
  log_lines: [],
35
47
  message: "",
@@ -41,10 +53,10 @@ export function evaluateCommit(coordRoot, req) {
41
53
  const gitlinkSet = new Set(req.staged_gitlinks ?? []);
42
54
  const conflicts = [];
43
55
  for (const peer of peers) {
44
- if (peer.instance_id === req.instance_id)
56
+ if (peer.instance_id === me)
45
57
  continue;
46
58
  const peerSession = peer.session_id ?? peer.instance_id;
47
- if (req.session_id && peerSession === req.session_id)
59
+ if (mySession !== "__unattributed__" && peerSession === mySession)
48
60
  continue; // same group
49
61
  const ts = peer.last_heartbeat ? Date.parse(peer.last_heartbeat) : 0;
50
62
  if (!Number.isFinite(ts) || ts < cutoffMs)
@@ -71,6 +83,7 @@ export function evaluateCommit(coordRoot, req) {
71
83
  allow: true,
72
84
  exit_code: 0,
73
85
  rule: "commit.pass",
86
+ instance_id: me,
74
87
  conflicts: [],
75
88
  log_lines: [],
76
89
  message: "",
@@ -81,6 +94,7 @@ export function evaluateCommit(coordRoot, req) {
81
94
  allow: true,
82
95
  exit_code: 0,
83
96
  rule: "commit.bypass",
97
+ instance_id: me,
84
98
  conflicts,
85
99
  log_lines: conflicts.map((c) => `COMMIT_BYPASSED path=${c.staged_path} owner=${c.short_name}`),
86
100
  message: "⚠ Multi-Agent coordination: bypass active; staged paths claimed by other agents will be committed anyway:",
@@ -95,6 +109,7 @@ export function evaluateCommit(coordRoot, req) {
95
109
  allow: true,
96
110
  exit_code: 0,
97
111
  rule: "commit.suppressed",
112
+ instance_id: me,
98
113
  conflicts,
99
114
  log_lines: conflicts.map((c) => `COMMIT_SUPPRESSED path=${c.staged_path} owner=${c.short_name} reason=self_attribution`),
100
115
  message: "⚠ Multi-Agent coordination: self-attribution detected; staged paths\n" +
@@ -108,6 +123,7 @@ export function evaluateCommit(coordRoot, req) {
108
123
  allow: false,
109
124
  exit_code: 1,
110
125
  rule: "commit.conflict",
126
+ instance_id: me,
111
127
  conflicts,
112
128
  log_lines: conflicts.map((c) => `COMMIT_BLOCKED path=${c.staged_path} owner=${c.short_name}`),
113
129
  message: "✗ Commit blocked by multi-agent coordination (E guard).\n\n" +
@@ -11,6 +11,9 @@
11
11
  * stop currently firing carries that field via the in-flight event
12
12
  * agent-hook emits before the stop hook runs).
13
13
  * Rule 3/3: `state.task_set` event with matching turn boundary exists.
14
+ * Session-naming rule (at most once per session, CC semantics only): the turn
15
+ * whose set-task produced `suggested_session_name` must carry an
16
+ * in-window `turn.stop` with `session_name_present: true`.
14
17
  *
15
18
  * Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
16
19
  * the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
@@ -1 +1 @@
1
- {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAwEnE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CAqHvF"}
1
+ {"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAUH,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;iFAM6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA+BD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AA4DnE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CA8JvF"}
@@ -11,6 +11,9 @@
11
11
  * stop currently firing carries that field via the in-flight event
12
12
  * agent-hook emits before the stop hook runs).
13
13
  * Rule 3/3: `state.task_set` event with matching turn boundary exists.
14
+ * Session-naming rule (at most once per session, CC semantics only): the turn
15
+ * whose set-task produced `suggested_session_name` must carry an
16
+ * in-window `turn.stop` with `session_name_present: true`.
14
17
  *
15
18
  * Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
16
19
  * the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
@@ -24,7 +27,7 @@
24
27
  */
25
28
  import { existsSync, readFileSync } from "node:fs";
26
29
  import { join } from "node:path";
27
- import { resolveBinName } from "../../config.js";
30
+ import { agentsRequireGitFinalization, endOfTurnStatusCommand, resolveBinName, } from "../../config.js";
28
31
  function ackSignalFor(adapter) {
29
32
  return adapter === "cursor" ? "status_checked" : "status_box_present";
30
33
  }
@@ -39,17 +42,6 @@ const RECENT_EVENT_WINDOW_LINES = 5_000;
39
42
  * trailing marker could be truncated away on a long reason.
40
43
  */
41
44
  export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
42
- /**
43
- * The pre-marker message shape, still recognized on purpose.
44
- *
45
- * A followup already sitting in the stream was written by whichever build was
46
- * running when it fired. An agent whose session spans an upgrade would otherwise
47
- * keep alternating until the first new-format followup appeared, which is the
48
- * exact failure this change removes. Recognizing the older trailing form closes
49
- * that rollout window at no cost: it only ever appears in a `prompt_text`
50
- * because Harnery put it there.
51
- */
52
- const STOP_REMEDIATION_LEGACY_MARKER = "[agent-hook stop]: rule=";
53
45
  /** How far back the remediation walk may travel. Adapters cap their own
54
46
  * followup chains (Cursor's `loop_limit`), and each prompt can appear once per
55
47
  * observing adapter, so this only guards a pathological stream. */
@@ -58,7 +50,7 @@ function isStopRemediationPrompt(event) {
58
50
  const text = event.data.prompt_text;
59
51
  if (typeof text !== "string")
60
52
  return false;
61
- return text.includes(STOP_REMEDIATION_MARKER) || text.includes(STOP_REMEDIATION_LEGACY_MARKER);
53
+ return text.includes(STOP_REMEDIATION_MARKER);
62
54
  }
63
55
  /**
64
56
  * Start of the window this Stop verdict judges.
@@ -157,7 +149,7 @@ export function evaluateStopHook(coordRoot, req) {
157
149
  allow: true,
158
150
  exit_code: 0,
159
151
  rule: "stop-hook.no_history",
160
- reason: "no canonical events for this owner; defer to legacy or skip",
152
+ reason: "no canonical events for this owner; nothing to evaluate",
161
153
  };
162
154
  }
163
155
  // Turn window: from the most recent user_prompt.submit (this owner) up to
@@ -176,7 +168,11 @@ export function evaluateStopHook(coordRoot, req) {
176
168
  return Number.isFinite(t) && t >= startMs && t <= endMs;
177
169
  });
178
170
  const toolPreUseInTurn = inTurn.some((e) => e.event_type === "tool.pre_use");
179
- const statusChecked = inTurn.some((e) => e.event_type === "state.status_checked");
171
+ const statusEvents = inTurn.filter((e) => e.event_type === "state.status_checked");
172
+ const statusChecked = statusEvents.length > 0;
173
+ const requiredStatusChecked = agentsRequireGitFinalization(coordRoot)
174
+ ? statusEvents.some((e) => e.data.git_finalization_checked === true)
175
+ : statusChecked;
180
176
  const taskSet = inTurn.some((e) => e.event_type === "state.task_set");
181
177
  // Rule 2/3: status_box_present on the most recent turn.stop for this owner.
182
178
  // The turn.stop event fires from agent-hook stop, which is wired BEFORE
@@ -189,8 +185,12 @@ export function evaluateStopHook(coordRoot, req) {
189
185
  // on Claude Code / Codex it's the transcript-scanned `status_box_present`.
190
186
  // The matching block helper carries the right "how to fix" message.
191
187
  const ackSignal = ackSignalFor(req.adapter);
192
- const ackPresent = ackSignal === "status_checked" ? statusChecked : boxPresent;
193
- const ackBlock = ackSignal === "status_checked" ? rule13Block : rule23Block;
188
+ const ackPresent = ackSignal === "status_checked"
189
+ ? toolPreUseInTurn
190
+ ? requiredStatusChecked
191
+ : statusChecked
192
+ : boxPresent;
193
+ const ackBlock = ackSignal === "status_checked" ? () => rule13Block(coordRoot) : () => rule23Block(coordRoot);
194
194
  // Pure-prose-turn exemption: only the ack signal applies. Parity
195
195
  // across adapters: CC requires the box; Cursor requires status_checked.
196
196
  if (!toolPreUseInTurn) {
@@ -204,34 +204,70 @@ export function evaluateStopHook(coordRoot, req) {
204
204
  reason: "no tool calls this turn; rules 1/3 + 3/3 skipped",
205
205
  };
206
206
  }
207
- if (!statusChecked)
208
- return rule13Block();
207
+ if (!requiredStatusChecked)
208
+ return rule13Block(coordRoot);
209
209
  // On Cursor `ackPresent === statusChecked` (already true here), so this is a
210
210
  // no-op and rule 2/3 is not enforced; on CC/Codex it's the box-paste check.
211
211
  if (!ackPresent)
212
212
  return ackBlock();
213
213
  if (!taskSet)
214
214
  return rule33Block();
215
+ // Session-naming rule (at most once per session, evaluated last so
216
+ // remediation stays monotonic in the established order): the turn whose
217
+ // set-task produced the suggested session name must show that name in the
218
+ // reply. Claude Code semantics only — Cursor's Stop payload carries neither
219
+ // a transcript nor the assistant message, so the fence is undetectable there
220
+ // (the same reason rule 2/3 collapses into 1/3 on Cursor), and Codex
221
+ // returned observe-only above. Detection rides `turn.stop`'s
222
+ // `session_name_present` (assistant-text-only transcript scan), mirroring
223
+ // how rule 2/3 rides `status_box_present`.
224
+ if (ackSignal === "status_box_present") {
225
+ const namingTaskSet = inTurn.find((e) => e.event_type === "state.task_set" &&
226
+ e.data.first_of_session === true &&
227
+ typeof e.data.suggested_session_name === "string" &&
228
+ e.data.suggested_session_name.length > 0);
229
+ if (namingTaskSet) {
230
+ // ANY in-window turn.stop with the flag satisfies the rule, not just the
231
+ // latest: once seen, `session_name_seen_at` is stamped and later
232
+ // turn.stops OMIT the field, so a later block on a different rule (whose
233
+ // repair fires a fresh turn.stop) must not un-satisfy this one.
234
+ const namePresent = inTurn.some((e) => e.event_type === "turn.stop" && e.data.session_name_present === true);
235
+ if (!namePresent) {
236
+ return sessionNameBlock(String(namingTaskSet.data.suggested_session_name));
237
+ }
238
+ }
239
+ }
215
240
  return {
216
241
  allow: true,
217
242
  exit_code: 0,
218
243
  rule: "stop-hook.pass",
219
244
  };
220
245
  }
221
- function rule13Block() {
246
+ function rule13Block(coordRoot) {
247
+ const missingEvidence = agentsRequireGitFinalization(coordRoot)
248
+ ? "no Git-finalized state.status_checked event found in this turn"
249
+ : "no state.status_checked event found in this turn";
222
250
  return {
223
251
  allow: false,
224
252
  exit_code: 2,
225
253
  rule: "stop-hook.rule_1_3",
226
- reason: `End-of-turn rule (1/3): no state.status_checked event found in this turn; run \`${resolveBinName()} agents status\` as your last tool call.`,
254
+ reason: `End-of-turn rule (1/3): ${missingEvidence}; run \`${endOfTurnStatusCommand(coordRoot)}\` as your last tool call.`,
227
255
  };
228
256
  }
229
- function rule23Block() {
257
+ function rule23Block(coordRoot) {
230
258
  return {
231
259
  allow: false,
232
260
  exit_code: 2,
233
261
  rule: "stop-hook.rule_2_3",
234
- reason: `End-of-turn rule (2/3): turn.stop did not see the agent-status box in your reply text. Paste the \`${resolveBinName()} agents status\` output verbatim as a fenced code block (the \`┌─ agent-\` prefix is the detection signal).`,
262
+ reason: `End-of-turn rule (2/3): turn.stop did not see the agent-status box in your reply text. Paste the \`${endOfTurnStatusCommand(coordRoot)}\` output verbatim as a fenced code block (the \`┌─ agent-\` prefix is the detection signal).`,
263
+ };
264
+ }
265
+ function sessionNameBlock(name) {
266
+ return {
267
+ allow: false,
268
+ exit_code: 2,
269
+ rule: "stop-hook.session_name",
270
+ reason: `Session-naming rule: this turn's set-task produced the session name, but your reply doesn't show it. Reproduce it by itself in a fenced code block so the operator can one-click-copy it as the session/tab title: ${name}`,
235
271
  };
236
272
  }
237
273
  function rule33Block() {