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