harnery 0.31.5 → 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 (222) 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 +11 -4
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +27 -9
  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/parse.d.ts +1 -1
  72. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/parse.js +22 -5
  74. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +32 -0
  77. package/dist/core/hooks/cli.d.ts +1 -2
  78. package/dist/core/hooks/cli.d.ts.map +1 -1
  79. package/dist/core/hooks/cli.js +169 -27
  80. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  81. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  82. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  83. package/dist/core/hooks/effects/index.d.ts +1 -1
  84. package/dist/core/hooks/effects/index.js +3 -3
  85. package/dist/core/hooks/events/schema.d.ts +27 -0
  86. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  87. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  88. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/owner.js +15 -0
  90. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  91. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  92. package/dist/core/hooks/resolve/transcript.js +176 -1
  93. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  94. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  95. package/dist/core/hooks/session-name-presence.js +43 -0
  96. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  97. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  98. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  99. package/dist/core/work/state.d.ts +4 -5
  100. package/dist/core/work/state.d.ts.map +1 -1
  101. package/dist/core/work/state.js +5 -8
  102. package/dist/core/workflow/index.d.ts +1 -1
  103. package/dist/core/workflow/index.d.ts.map +1 -1
  104. package/dist/core/workflow/proof.d.ts +2 -2
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +2 -2
  107. package/dist/core/workflow/run-state.d.ts +2 -2
  108. package/dist/core/workflow/run-state.d.ts.map +1 -1
  109. package/dist/core/workflow/run-state.js +2 -2
  110. package/dist/core/workflow/types.d.ts +3 -3
  111. package/dist/core/workflow/types.d.ts.map +1 -1
  112. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  113. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/execution.js +5 -0
  115. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  116. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  117. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  118. package/dist/core/workflow/workspaces/local-git.js +1 -2
  119. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  120. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  121. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  122. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  123. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  124. package/dist/core/workflow/workspaces/validate.js +0 -2
  125. package/dist/lib/browser/client.d.ts +118 -1
  126. package/dist/lib/browser/client.d.ts.map +1 -1
  127. package/dist/lib/browser/client.js +435 -6
  128. package/dist/lib/browser/geometry.d.ts.map +1 -1
  129. package/dist/lib/browser/geometry.js +180 -37
  130. package/dist/lib/browser/index.d.ts +5 -1
  131. package/dist/lib/browser/index.d.ts.map +1 -1
  132. package/dist/lib/browser/index.js +5 -1
  133. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  134. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  135. package/dist/lib/browser/netscape-cookies.js +31 -0
  136. package/dist/lib/browser/proxy.d.ts +24 -0
  137. package/dist/lib/browser/proxy.d.ts.map +1 -0
  138. package/dist/lib/browser/proxy.js +84 -0
  139. package/dist/lib/browser/runts.d.ts +6 -0
  140. package/dist/lib/browser/runts.d.ts.map +1 -1
  141. package/dist/lib/browser/runts.js +21 -4
  142. package/dist/lib/browser/session-control.d.ts +112 -0
  143. package/dist/lib/browser/session-control.d.ts.map +1 -0
  144. package/dist/lib/browser/session-control.js +670 -0
  145. package/dist/lib/docs-links.d.ts +108 -0
  146. package/dist/lib/docs-links.d.ts.map +1 -0
  147. package/dist/lib/docs-links.js +555 -0
  148. package/dist/lib/exec.d.ts +1 -1
  149. package/dist/lib/exec.js +1 -1
  150. package/dist/lib/identities/assume.d.ts +4 -2
  151. package/dist/lib/identities/assume.d.ts.map +1 -1
  152. package/dist/lib/identities/assume.js +15 -2
  153. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  154. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  155. package/dist/lib/instructions/git-hooks.js +238 -0
  156. package/dist/lib/instructions/splice.d.ts +10 -4
  157. package/dist/lib/instructions/splice.d.ts.map +1 -1
  158. package/dist/lib/instructions/splice.js +19 -11
  159. package/package.json +1 -1
  160. package/schemas/config.schema.json +32 -0
  161. package/src/commander.ts +2 -0
  162. package/src/commands/agents.ts +96 -115
  163. package/src/commands/browse-session.ts +245 -0
  164. package/src/commands/browse.ts +287 -44
  165. package/src/commands/checkpoint.ts +1 -1
  166. package/src/commands/deinit.ts +7 -1
  167. package/src/commands/docs.ts +56 -0
  168. package/src/commands/doctor.ts +100 -18
  169. package/src/commands/init.ts +17 -9
  170. package/src/commands/tunnel.ts +2 -5
  171. package/src/core/agents/canonical-emit.ts +1 -2
  172. package/src/core/agents/cli.ts +137 -88
  173. package/src/core/agents/coord-client.ts +38 -9
  174. package/src/core/agents/finalization.ts +595 -0
  175. package/src/core/agents/git-hook.ts +126 -0
  176. package/src/core/agents/render/prompt-context.ts +28 -13
  177. package/src/core/agents/render/session-context.ts +15 -2
  178. package/src/core/agents/rules/claim-conflict.ts +3 -3
  179. package/src/core/agents/rules/commit-conflict.ts +35 -8
  180. package/src/core/agents/rules/stop-hook.ts +71 -23
  181. package/src/core/agents/session-events.ts +14 -41
  182. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  183. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  184. package/src/core/agents/state/names.ts +54 -2
  185. package/src/core/config.ts +92 -12
  186. package/src/core/governor/planning.ts +17 -12
  187. package/src/core/hooks/adapter/detect.ts +4 -13
  188. package/src/core/hooks/adapter/output.ts +9 -4
  189. package/src/core/hooks/adapter/parse.ts +24 -5
  190. package/src/core/hooks/adapter/wiring.ts +43 -0
  191. package/src/core/hooks/cli.ts +201 -23
  192. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  193. package/src/core/hooks/effects/index.ts +5 -5
  194. package/src/core/hooks/events/schema.ts +27 -0
  195. package/src/core/hooks/resolve/owner.ts +22 -1
  196. package/src/core/hooks/resolve/transcript.ts +164 -1
  197. package/src/core/hooks/session-name-presence.ts +52 -0
  198. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  199. package/src/core/work/state.ts +8 -12
  200. package/src/core/workflow/engine.ts +2 -2
  201. package/src/core/workflow/index.ts +0 -1
  202. package/src/core/workflow/proof.ts +4 -4
  203. package/src/core/workflow/run-state.ts +5 -5
  204. package/src/core/workflow/types.ts +3 -3
  205. package/src/core/workflow/workspaces/execution.ts +11 -4
  206. package/src/core/workflow/workspaces/index.ts +0 -3
  207. package/src/core/workflow/workspaces/local-git.ts +1 -2
  208. package/src/core/workflow/workspaces/paths.ts +2 -2
  209. package/src/core/workflow/workspaces/types.ts +0 -9
  210. package/src/core/workflow/workspaces/validate.ts +1 -5
  211. package/src/lib/browser/client.ts +528 -6
  212. package/src/lib/browser/geometry.ts +197 -37
  213. package/src/lib/browser/index.ts +39 -0
  214. package/src/lib/browser/netscape-cookies.ts +39 -0
  215. package/src/lib/browser/proxy.ts +105 -0
  216. package/src/lib/browser/runts.ts +27 -3
  217. package/src/lib/browser/session-control.ts +892 -0
  218. package/src/lib/docs-links.ts +674 -0
  219. package/src/lib/exec.ts +1 -1
  220. package/src/lib/identities/assume.ts +22 -1
  221. package/src/lib/instructions/git-hooks.ts +259 -0
  222. package/src/lib/instructions/splice.ts +41 -11
@@ -224,10 +224,10 @@ export function parsePidmapRowOwner(row) {
224
224
  const tab = trimmed.indexOf("\t");
225
225
  return tab >= 0 ? trimmed.slice(0, tab) : trimmed;
226
226
  }
227
- /** Parse platform from a pid-map row; legacy rows default to `claude_code`. */
227
+ /** Parse platform from a pid-map row; legacy rows default to `claude-code`. */
228
228
  export function parsePidmapRowPlatform(row) {
229
229
  const platform = row.trim().split("\t")[1]?.trim();
230
- return platform || "claude_code";
230
+ return platform || "claude-code";
231
231
  }
232
232
  /** Parse the start token from a pid-map row; rows written before it carry none. */
233
233
  export function parsePidmapRowStartToken(row) {
@@ -336,7 +336,7 @@ function readPidmapRow(pidmapDir, pid) {
336
336
  *
337
337
  * Pid-map files are `instance_id` or `instance_id\tplatform` (Cursor Phase 1).
338
338
  * Prefer a row whose platform matches `HARNERY_AGENT_COORD_PLATFORM` (default
339
- * `claude_code`); otherwise return the first owner seen on the walk.
339
+ * `claude-code`); otherwise return the first owner seen on the walk.
340
340
  *
341
341
  * Subagents intentionally do not write pid-map entries; a Bash-tool ppid-walk
342
342
  * from inside a subagent therefore resolves to the *parent's* pid-map entry.
@@ -400,7 +400,7 @@ export function resolveOwnerByPidmap(root) {
400
400
  const pidmapDir = resolve(root, ".harnery", "pid-map");
401
401
  if (!existsSync(pidmapDir))
402
402
  return { owner: null, source: "none" };
403
- const prefer = process.env.HARNERY_AGENT_COORD_PLATFORM?.trim() || "claude_code";
403
+ const prefer = process.env.HARNERY_AGENT_COORD_PLATFORM?.trim() || "claude-code";
404
404
  let fallbackOwner = null;
405
405
  let pid = process.pid;
406
406
  for (let hop = 0; hop < 20; hop++) {
@@ -439,6 +439,7 @@ const SESSION_ID_ENV_VARS = [
439
439
  "CURSOR_SESSION_ID",
440
440
  "CURSOR_CONVERSATION_ID",
441
441
  "CODEX_SESSION_ID",
442
+ "CODEX_THREAD_ID",
442
443
  ];
443
444
  /** Read normalized candidates from the first non-empty adapter session-id env var. */
444
445
  function sessionIdsFromEnv() {
@@ -482,7 +483,10 @@ function shouldPreferSessionEnv() {
482
483
  * `instance_id`, or null if there's no session-id env var or no live heartbeat
483
484
  * carries it. "Live" reuses the same 10-minute freshness window the singleton
484
485
  * fallback applies, so a stale heartbeat from a prior session of the same id
485
- * doesn't resolve.
486
+ * doesn't resolve. When legacy Cursor Glass state contains both the canonical
487
+ * bare id and its raw `bc-` alias, candidate order wins first; ties prefer a
488
+ * named heartbeat, then the newest heartbeat, with instance id as a stable
489
+ * final tie-breaker. Filesystem enumeration order never decides identity.
486
490
  *
487
491
  * Exported for unit testing with an injectable root.
488
492
  */
@@ -502,24 +506,38 @@ export function resolveOwnerBySessionEnv(root) {
502
506
  catch {
503
507
  return null;
504
508
  }
509
+ const matches = [];
505
510
  for (const file of files) {
506
511
  if (!file.endsWith(".json"))
507
512
  continue;
508
513
  try {
509
514
  const parsed = JSON.parse(readFileSync(resolve(activeDir, file), "utf8"));
510
- if (!parsed || !sessionIds.includes(parsed.session_id))
515
+ if (!parsed)
516
+ continue;
517
+ const sessionPreference = sessionIds.indexOf(parsed.session_id);
518
+ if (sessionPreference === -1)
511
519
  continue;
512
520
  if (typeof parsed.instance_id !== "string")
513
521
  continue;
514
522
  const ts = Date.parse(parsed.last_heartbeat);
515
- if (Number.isFinite(ts) && ts >= cutoffMs)
516
- return parsed.instance_id;
523
+ if (!Number.isFinite(ts) || ts < cutoffMs)
524
+ continue;
525
+ matches.push({
526
+ instanceId: parsed.instance_id,
527
+ sessionPreference,
528
+ hasName: typeof parsed.name === "string" && parsed.name.trim().length > 0,
529
+ lastHeartbeatMs: ts,
530
+ });
517
531
  }
518
532
  catch {
519
533
  // skip malformed
520
534
  }
521
535
  }
522
- return null;
536
+ matches.sort((a, b) => a.sessionPreference - b.sessionPreference ||
537
+ Number(b.hasName) - Number(a.hasName) ||
538
+ b.lastHeartbeatMs - a.lastHeartbeatMs ||
539
+ a.instanceId.localeCompare(b.instanceId));
540
+ return matches[0]?.instanceId ?? null;
523
541
  }
524
542
  /**
525
543
  * Return the instance_id of the sole live agent in this coord root, or null
@@ -0,0 +1,68 @@
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 { type AgentFinalizationDisposition } from "../config.js";
9
+ export interface GitFinalizationResult {
10
+ ok: boolean;
11
+ claim_history_complete: boolean;
12
+ dirty_paths: string[];
13
+ unpushed_repos: string[];
14
+ host_output_paths: string[];
15
+ unsupported_paths: UnsupportedFinalizationPath[];
16
+ unverifiable_paths: string[];
17
+ unverifiable_repos: string[];
18
+ repos_checked: string[];
19
+ }
20
+ export type UnsupportedFinalizationReason = "outside_finalization_roots" | "invalid_finalization_root" | "ambiguous_finalization_roots" | "path_escapes_finalization_root" | "git_repository_unavailable" | "output_path_is_git";
21
+ export interface UnsupportedFinalizationPath {
22
+ path: string;
23
+ reason: UnsupportedFinalizationReason;
24
+ }
25
+ export interface ClaimFinalizationDescriptor {
26
+ disposition: AgentFinalizationDisposition;
27
+ root: string;
28
+ }
29
+ export type ClaimFinalizationDecision = {
30
+ allow: true;
31
+ path: string;
32
+ descriptor: ClaimFinalizationDescriptor;
33
+ authorityRoot: string;
34
+ repo?: {
35
+ root: string;
36
+ path: string;
37
+ };
38
+ } | {
39
+ allow: false;
40
+ path: string;
41
+ reason: UnsupportedFinalizationReason;
42
+ };
43
+ export interface SessionWriteClaims {
44
+ paths: string[];
45
+ complete: boolean;
46
+ }
47
+ /**
48
+ * Read every write claim made by this session, including claims released after
49
+ * a commit. Current heartbeat claims describe dirty ownership; this history is
50
+ * what keeps the touched repository in scope until its commits are pushed.
51
+ */
52
+ export declare function readSessionWriteClaims(coordRoot: string, instanceId: string, sessionId: string): SessionWriteClaims;
53
+ /**
54
+ * Resolve one guarded write to the host's single supported end-turn
55
+ * disposition. Claim event data never grants authority: every call starts from
56
+ * the project-owned finalization roots and only then inspects the selected
57
+ * repository or output tree.
58
+ */
59
+ export declare function classifyWriteClaimFinalization(coordRoot: string, heldPath: string): ClaimFinalizationDecision;
60
+ /** Check that this session's held paths are committed and their repositories are pushed. */
61
+ export declare function checkGitFinalization(coordRoot: string, heldPaths: readonly string[], options?: {
62
+ claimHistoryComplete?: boolean;
63
+ }): GitFinalizationResult;
64
+ export declare function formatGitFinalizationFailure(result: GitFinalizationResult, bin: string): string;
65
+ export declare function formatWriteClaimFinalizationDenial(decision: Extract<ClaimFinalizationDecision, {
66
+ allow: false;
67
+ }>, bin: string): string;
68
+ //# sourceMappingURL=finalization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finalization.d.ts","sourceRoot":"","sources":["../../../src/core/agents/finalization.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,KAAK,4BAA4B,EAA2B,MAAM,cAAc,CAAC;AA4B1F,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,sBAAsB,EAAE,OAAO,CAAC;IAChC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iBAAiB,EAAE,2BAA2B,EAAE,CAAC;IACjD,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,6BAA6B,GACrC,4BAA4B,GAC5B,2BAA2B,GAC3B,8BAA8B,GAC9B,gCAAgC,GAChC,4BAA4B,GAC5B,oBAAoB,CAAC;AAEzB,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,6BAA6B,CAAC;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,4BAA4B,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,yBAAyB,GACjC;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,2BAA2B,CAAC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACvC,GACD;IACE,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,6BAA6B,CAAC;CACvC,CAAC;AAEN,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,kBAAkB,CA+BpB;AA+JD;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,yBAAyB,CA2D3B;AA2ED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,OAAO,GAAE;IAAE,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/C,qBAAqB,CA6EvB;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAkD/F;AAmBD,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,OAAO,CAAC,yBAAyB,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC,EAC9D,GAAG,EAAE,MAAM,GACV,MAAM,CAQR"}
@@ -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"}