apex-code 0.0.5 → 0.0.6

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 (48) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/dist/cli/agent-lifecycle.d.ts +24 -0
  3. package/dist/cli/agent-lifecycle.d.ts.map +1 -0
  4. package/dist/cli/agent-lifecycle.js +126 -0
  5. package/dist/cli/agent-lifecycle.js.map +1 -0
  6. package/dist/cli/args.d.ts +13 -0
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +80 -0
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/core/agent-session.d.ts +160 -0
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +265 -0
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/delegation/runtime.d.ts +682 -1
  15. package/dist/core/delegation/runtime.d.ts.map +1 -1
  16. package/dist/core/delegation/runtime.js +1339 -66
  17. package/dist/core/delegation/runtime.js.map +1 -1
  18. package/dist/core/sdk.d.ts +61 -3
  19. package/dist/core/sdk.d.ts.map +1 -1
  20. package/dist/core/sdk.js +316 -22
  21. package/dist/core/sdk.js.map +1 -1
  22. package/dist/core/tools/delegate.d.ts +8 -0
  23. package/dist/core/tools/delegate.d.ts.map +1 -1
  24. package/dist/core/tools/delegate.js +33 -3
  25. package/dist/core/tools/delegate.js.map +1 -1
  26. package/dist/core/workspace/git-observer.d.ts +16 -0
  27. package/dist/core/workspace/git-observer.d.ts.map +1 -1
  28. package/dist/core/workspace/git-observer.js +8 -1
  29. package/dist/core/workspace/git-observer.js.map +1 -1
  30. package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
  31. package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
  32. package/dist/core/workspace/git-worktree-owner.js +240 -0
  33. package/dist/core/workspace/git-worktree-owner.js.map +1 -0
  34. package/dist/main.d.ts.map +1 -1
  35. package/dist/main.js +20 -0
  36. package/dist/main.js.map +1 -1
  37. package/dist/modes/acp/server.d.ts +43 -1
  38. package/dist/modes/acp/server.d.ts.map +1 -1
  39. package/dist/modes/acp/server.js +78 -0
  40. package/dist/modes/acp/server.js.map +1 -1
  41. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  42. package/dist/modes/rpc/rpc-mode.js +38 -0
  43. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  44. package/dist/modes/rpc/rpc-types.d.ts +110 -0
  45. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  46. package/dist/modes/rpc/rpc-types.js.map +1 -1
  47. package/npm-shrinkwrap.json +5 -5
  48. package/package.json +2 -2
@@ -19,7 +19,14 @@ const DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;
19
19
  const DEFAULT_TIMEOUT_MS = 10_000;
20
20
  const GIT_CONFIG = ["-c", "core.autocrlf=false", "-c", "status.renames=true"];
21
21
  const UNMERGED_XY = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
22
- function runGit(cwd, args, timeoutMs, signal) {
22
+ /**
23
+ * The workspace subsystem's one git exec seam: shell-less argv (no quoting
24
+ * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks
25
+ * disabled, per-command timeout, abort support. The observer above uses it
26
+ * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so
27
+ * the subsystem never grows a second spawn pattern.
28
+ */
29
+ export function runGit(cwd, args, timeoutMs, signal) {
23
30
  return new Promise((resolve) => {
24
31
  let stdout = Buffer.alloc(0);
25
32
  let stderr = "";
@@ -1 +1 @@
1
- {"version":3,"file":"git-observer.js","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EACN,wBAAwB,GAIxB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AAEH,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B,MAAM,sBAAsB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAC/C,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,qBAAqB,CAAC,CAAC;AAC9E,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AA8BxE,SAAS,MAAM,CAAC,GAAW,EAAE,IAAc,EAAE,SAAiB,EAAE,MAAoB,EAAyB;IAC5G,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/B,IAAI,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,KAAmB,CAAC;QACxB,MAAM,MAAM,GAAG,CAAC,IAAmB,EAAE,UAAmB,EAAE,EAAE,CAAC;YAC5D,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,CAAC;gBACP,EAAE,EAAE,CAAC,UAAU,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,CAAC;gBAC1C,IAAI;gBACJ,MAAM;gBACN,MAAM;gBACN,QAAQ;gBACR,UAAU;aACV,CAAC,CAAC;QAAA,CACH,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;YACrB,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAAA,CACvB,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAC9B,QAAQ,GAAG,IAAI,CAAC;YAChB,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAAA,CACvB,EAAE,SAAS,CAAC,CAAC;QACd,IAAI,CAAC;YACJ,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,EAAE,GAAG,IAAI,CAAC,EAAE;gBAC9C,GAAG;gBACH,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;gBACjC,sDAAsD;gBACtD,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE;aAChD,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1F,OAAO;QACR,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACZ,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAC;;gBACzB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;YAC3C,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAAA,CACxC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAAA,CAClC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC5C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IAAA,CACjD,CAAC,CAAC;AAAA,CACH;AAED,SAAS,aAAa,GAA2B;IAChD,OAAO;QACN,OAAO,EAAE,KAAK;QACd,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,KAAK;QAChB,OAAO,EAAE,KAAK;QACd,MAAM,EAAE,KAAK;QACb,KAAK,EAAE,KAAK;KACZ,CAAC;AAAA,CACF;AAED,SAAS,YAAY,GAA2B;IAC/C,OAAO;QACN,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,IAAI;QACd,SAAS,EAAE,IAAI;QACf,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,KAAK;KACZ,CAAC;AAAA,CACF;AASD,SAAS,eAAe,CAAC,MAAc,EAAsB;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAC3C,MAAM,KAAK,GAAqB,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACtF,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YAC9E,KAAK,CAAC,YAAY,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED,SAAS,QAAQ,CAAC,KAAuB,EAKvC;IACD,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC;IACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7E,CAAC;IACD,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;QACjC,OAAO;YACN,IAAI,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU;YACrD,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,IAAI;YACd,OAAO,EAAE,KAAK;SACd,CAAC;IACH,CAAC;IACD,IAAI,IAAI,GAA+B,UAAU,CAAC;IAClD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,IAAI,GAAG,OAAO,CAAC;SAChE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,IAAI,GAAG,SAAS,CAAC;SAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,IAAI,GAAG,SAAS,CAAC;IAClD,OAAO;QACN,IAAI;QACJ,MAAM,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAC3C,QAAQ,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAC7C,OAAO,EAAE,KAAK;KACd,CAAC;AAAA,CACF;AAED,8EAA8E;AAC9E,SAAS,iBAAiB,CACzB,QAAgB,EAChB,IAAY,EACZ,QAAgB,EACuC;IACvD,IAAI,CAAC;QACJ,MAAM,IAAI,GAAG,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,IAAI,GAAG,QAAQ;gBAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;YACrD,OAAO,EAAE,IAAI,EAAE,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC5G,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAC3B,OAAO;gBACN,IAAI,EAAE,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;gBAC/E,OAAO,EAAE,KAAK;aACd,CAAC;QACH,CAAC;QACD,qEAAqE;QACrE,wCAAwC;QACxC,IAAI,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAC7B,CAAC;AAAA,CACD;AAED,SAAS,cAAc,CAAC,MAAc,EAAE,IAAY,EAAU;IAC7D,IAAI,MAAM,KAAK,EAAE,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;AAAA,CAC3B;AAkBD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC9B,aAAqB,EACrB,QAAgB,EAChB,OAGC,EACoB;IACrB,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACvD,MAAM,OAAO,GAAG,QAAQ,KAAK,OAAO,CAAC;IACrC,MAAM,QAAQ,GACb,OAAO,EAAE,QAAQ;QACjB,CAAC,OAAO;YACP,CAAC,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC;gBACf,IAAI,CAAC;oBACJ,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC/B,CAAC;gBAAC,MAAM,CAAC;oBACR,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;gBACxB,CAAC;YAAA,CACD;YACF,CAAC,CAAC,YAAY,CAAC,CAAC;IAClB,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,YAAY,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IACjE,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IACjG,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QACvB,wEAAwE;QACxE,6DAA6D;QAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;AAAA,CACxC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,aAAqB,EACrB,OAA2B,EACK;IAChC,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,iBAAiB,CAAC;IACxD,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,sBAAsB,CAAC;IACrE,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,kBAAkB,CAAC;IAC3D,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,IAAI,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;IAC/B,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,EAAE,CAAC;SAChD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;SAC1E,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9B,MAAM,UAAU,GAAG,CAAC,QAAgB,EAAW,EAAE,CAChD,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,EAAE,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,CAAC,OAAe,EAAwB,EAAE,CAAC,CAAC;QAC1D,OAAO,EAAE,wBAAwB;QACjC,aAAa,EAAE,UAAU,EAAE;QAC3B,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,KAAK;QACd,aAAa;QACb,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,QAAQ,EAAE,aAAa,EAAE;QACzB,KAAK,EAAE,EAAE;QACT,QAAQ,EAAE,CAAC,OAAO,CAAC;KACnB,CAAC,CAAC;IAEH,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC,iCAAiC,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAC9F,IAAI,IAAI,CAAC,UAAU;QAAE,OAAO,MAAM,CAAC,2BAA2B,CAAC,CAAC;IAChE,IAAI,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,CAAC,iCAAiC,CAAC,CAAC;IACpE,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,MAAM,CAAC,iCAAiC,CAAC,CAAC;IACtE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACd,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACpE,OAAO;gBACN,GAAG,MAAM,CAAC,sBAAsB,CAAC;gBACjC,MAAM,EAAE,aAAa;aACrB,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC,sBAAsB,CAAC,CAAC;IACvC,CAAC;IACD,oEAAoE;IACpE,wEAAwE;IACxE,sEAAsE;IACtE,uEAAuE;IACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,MAAM,UAAU,GAAG,eAAe,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;IAC5D,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO,MAAM,CAAC,mDAAmD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC;IAEvC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACrG,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAChH,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,MAAM,CAAC,iCAAiC,CAAC,CAAC;IACtE,IAAI,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,gBAAgB,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAE5F,MAAM,eAAe,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/F,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5E,IAAI,WAAW,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAEvD,kBAAkB;IAClB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAC5F,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAExF,uDAAuD;IACvD,MAAM,MAAM,GAAG,MAAM,MAAM,CAC1B,aAAa,EACb,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,uBAAuB,EAAE,oBAAoB,CAAC,EAC9E,SAAS,EACT,MAAM,CACN,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAEnD,MAAM,KAAK,GAAyB,EAAE,CAAC;IACvC,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACpD,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,KAAK,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;YACpG,SAAS;QACV,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;YAC9B,SAAS,GAAG,IAAI,CAAC;YACjB,QAAQ,CAAC,IAAI,CAAC,0BAA0B,QAAQ,QAAQ,CAAC,CAAC;YAC1D,MAAM;QACP,CAAC;QACD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC5D,MAAM,aAAa,GAAuB;YACzC,IAAI,EAAE,cAAc,CAAC,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC;YAC9C,IAAI;YACJ,MAAM;YACN,QAAQ;YACR,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,cAAc,CAAC,YAAY,EAAE,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjG,CAAC;QACF,IAAI,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,iBAAiB,CAAC,UAAU,CAAC,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;YAC/F,IAAI,IAAI;gBAAE,aAAa,CAAC,WAAW,GAAG,IAAI,CAAC;iBACtC,IAAI,OAAO;gBAAE,aAAa,EAAE,CAAC;QACnC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,aAAa,GAAG,CAAC;QAAE,QAAQ,CAAC,IAAI,CAAC,+BAA+B,aAAa,UAAU,CAAC,CAAC;IAE7F,iEAAiE;IACjE,8DAA8D;IAC9D,MAAM,WAAW,GAAG,KAAK;SACvB,GAAG,CACH,CAAC,CAAC,EAAE,EAAE,CACL,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,WAAW,IAAI,EAAE,IAAI,CACxH;SACA,IAAI,EAAE;SACN,IAAI,CAAC,EAAE,CAAC,CAAC;IAEX,OAAO;QACN,OAAO,EAAE,wBAAwB;QACjC,aAAa,EAAE,UAAU,EAAE;QAC3B,MAAM,EAAE,SAAS,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU;QAClE,OAAO,EAAE,KAAK;QACd,aAAa;QACb,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,qEAAqE;QACrE,iDAAiD;QACjD,QAAQ,EAAE,EAAE,GAAG,YAAY,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE;QAClD,IAAI,EAAE;YACL,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,WAAW;YACX,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;SAC5G;QACD,KAAK;QACL,QAAQ;KACR,CAAC;AAAA,CACF","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\ninterface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\nfunction runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
1
+ {"version":3,"file":"git-observer.js","sourceRoot":"","sources":["../../../src/core/workspace/git-observer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstatSync, readFileSync, readlinkSync, realpathSync } from \"node:fs\";\nimport { dirname, isAbsolute, win32 as pathWin32, relative, sep } from \"node:path\";\nimport {\n\tWORKSPACE_RECORD_VERSION,\n\ttype WorkspaceStateCoverage,\n\ttype WorkspaceStatePath,\n\ttype WorkspaceStateRecord,\n} from \"./state.ts\";\n\n/**\n * Read-only Git workspace observation (spec 2026-09-01-harness-correctness-\n * and-workspace-state.md § 1).\n *\n * The adapter never mutates repository state: `HEAD`, the current branch, the\n * index, the worktree, the stash, and local config are only read. Optional\n * index locks are disabled (`GIT_OPTIONAL_LOCKS=0`) so even `git status`\n * cannot refresh the on-disk index. It never walks the workspace; all path\n * knowledge comes from git. Symlinks are hashed as link text (what git\n * stores), never followed. Submodule (gitlink) paths carry no worktree hash.\n */\n\nconst DEFAULT_MAX_PATHS = 200;\nconst DEFAULT_MAX_HASH_BYTES = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst GIT_CONFIG = [\"-c\", \"core.autocrlf=false\", \"-c\", \"status.renames=true\"];\nconst UNMERGED_XY = new Set([\"DD\", \"AU\", \"UD\", \"UA\", \"DU\", \"AA\", \"UU\"]);\n\nexport interface GitObserveOptions {\n\t/** Compute per-path content hashes of present files (default true). */\n\thashPaths?: boolean;\n\t/** Maximum number of reported paths (default 200). */\n\tmaxPaths?: number;\n\t/** Skip hashing files larger than this many bytes (default 5 MiB). */\n\tmaxHashBytes?: number;\n\t/** Per-command timeout in milliseconds (default 10 000). */\n\ttimeoutMs?: number;\n\t/** Cancels the observation; the record reports `failed`. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Repository-relative path prefixes to omit from `paths` (exact match or\n\t * directory prefix). Used for harness bookkeeping that lives inside the\n\t * workspace — e.g. the session directory — which is not workspace state.\n\t */\n\texcludePaths?: string[];\n}\n\nexport interface GitRunResult {\n\tok: boolean;\n\tcode: number | null;\n\tstdout: Buffer;\n\tstderr: string;\n\ttimedOut: boolean;\n\tspawnError: boolean;\n}\n\n/**\n * The workspace subsystem's one git exec seam: shell-less argv (no quoting\n * bugs, Windows-safe), pinned `core.autocrlf=false`, optional index locks\n * disabled, per-command timeout, abort support. The observer above uses it\n * read-only; `git-worktree-owner.ts` reuses it for the worktree mutations so\n * the subsystem never grows a second spawn pattern.\n */\nexport function runGit(cwd: string, args: string[], timeoutMs: number, signal?: AbortSignal): Promise<GitRunResult> {\n\treturn new Promise((resolve) => {\n\t\tlet stdout = Buffer.alloc(0);\n\t\tlet stderr = \"\";\n\t\tlet timedOut = false;\n\t\tlet settled = false;\n\t\tlet child: ChildProcess;\n\t\tconst finish = (code: number | null, spawnError: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve({\n\t\t\t\tok: !spawnError && !timedOut && code === 0,\n\t\t\t\tcode,\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\ttimedOut,\n\t\t\t\tspawnError,\n\t\t\t});\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tchild?.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\t\ttry {\n\t\t\tchild = spawn(\"git\", [...GIT_CONFIG, ...args], {\n\t\t\t\tcwd,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t// Observation must leave the on-disk index untouched.\n\t\t\t\tenv: { ...process.env, GIT_OPTIONAL_LOCKS: \"0\" },\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ ok: false, code: null, stdout, stderr: \"\", timedOut: false, spawnError: true });\n\t\t\treturn;\n\t\t}\n\t\tif (signal) {\n\t\t\tif (signal.aborted) onAbort();\n\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t}\n\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstdout = Buffer.concat([stdout, chunk]);\n\t\t});\n\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString(\"utf-8\");\n\t\t});\n\t\tchild.on(\"error\", () => finish(null, true));\n\t\tchild.on(\"close\", (code) => finish(code, false));\n\t});\n}\n\nfunction emptyCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: false,\n\t\tstaged: false,\n\t\tunstaged: false,\n\t\tuntracked: false,\n\t\tignored: false,\n\t\thashes: false,\n\t\tpatch: false,\n\t};\n}\n\nfunction fullCoverage(): WorkspaceStateCoverage {\n\treturn {\n\t\ttracked: true,\n\t\tstaged: true,\n\t\tunstaged: true,\n\t\tuntracked: true,\n\t\tignored: true,\n\t\thashes: true,\n\t\tpatch: false,\n\t};\n}\n\ninterface ParsedStatusPath {\n\tx: string;\n\ty: string;\n\tpath: string;\n\tpreviousPath?: string;\n}\n\nfunction parsePorcelainZ(stdout: Buffer): ParsedStatusPath[] {\n\tconst fields = stdout.toString(\"utf-8\").split(\"\\0\");\n\tconst parsed: ParsedStatusPath[] = [];\n\tlet i = 0;\n\twhile (i < fields.length) {\n\t\tconst record = fields[i++];\n\t\tif (!record || record.length < 4) continue;\n\t\tconst entry: ParsedStatusPath = { x: record[0], y: record[1], path: record.slice(3) };\n\t\tif (entry.x === \"R\" || entry.x === \"C\" || entry.y === \"R\" || entry.y === \"C\") {\n\t\t\tentry.previousPath = fields[i++];\n\t\t}\n\t\tparsed.push(entry);\n\t}\n\treturn parsed;\n}\n\nfunction classify(entry: ParsedStatusPath): {\n\tkind: WorkspaceStatePath[\"kind\"];\n\tstaged: boolean;\n\tunstaged: boolean;\n\tignored: boolean;\n} {\n\tconst { x, y } = entry;\n\tif (x === \"?\" && y === \"?\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: false };\n\t}\n\tif (x === \"!\" && y === \"!\") {\n\t\treturn { kind: \"untracked\", staged: false, unstaged: false, ignored: true };\n\t}\n\tif (UNMERGED_XY.has(`${x}${y}`)) {\n\t\treturn {\n\t\t\tkind: x === \"D\" && y === \"D\" ? \"deleted\" : \"modified\",\n\t\t\tstaged: true,\n\t\t\tunstaged: true,\n\t\t\tignored: false,\n\t\t};\n\t}\n\tlet kind: WorkspaceStatePath[\"kind\"] = \"modified\";\n\tif (x === \"A\" || y === \"A\" || x === \"C\" || y === \"C\") kind = \"added\";\n\telse if (x === \"D\" || y === \"D\") kind = \"deleted\";\n\telse if (x === \"R\" || y === \"R\") kind = \"renamed\";\n\treturn {\n\t\tkind,\n\t\tstaged: x !== \" \" && x !== \"?\" && x !== \"!\",\n\t\tunstaged: y !== \" \" && y !== \"?\" && y !== \"!\",\n\t\tignored: false,\n\t};\n}\n\n/** Hash one workspace path: regular files by bytes, symlinks by link text. */\nfunction hashWorkspacePath(\n\ttoplevel: string,\n\tpath: string,\n\tmaxBytes: number,\n): { hash?: string; skipped: \"size\" | \"error\" | false } {\n\ttry {\n\t\tconst full = `${toplevel}/${path}`;\n\t\tconst stat = lstatSync(full);\n\t\tif (stat.isFile()) {\n\t\t\tif (stat.size > maxBytes) return { skipped: \"size\" };\n\t\t\treturn { hash: `sha256:${createHash(\"sha256\").update(readFileSync(full)).digest(\"hex\")}`, skipped: false };\n\t\t}\n\t\tif (stat.isSymbolicLink()) {\n\t\t\treturn {\n\t\t\t\thash: `sha256:${createHash(\"sha256\").update(readlinkSync(full)).digest(\"hex\")}`,\n\t\t\t\tskipped: false,\n\t\t\t};\n\t\t}\n\t\t// Directories (submodule gitlinks) have no worktree content to hash;\n\t\t// that is expected, not a coverage gap.\n\t\tif (stat.isDirectory()) return { skipped: false };\n\t\treturn { skipped: \"error\" };\n\t} catch {\n\t\treturn { skipped: \"error\" };\n\t}\n}\n\nfunction prefixRelative(prefix: string, path: string): string {\n\tif (prefix === \"\" || prefix === \".\") return path;\n\treturn `${prefix}/${path}`;\n}\n\n/**\n * Observe a Git workspace without mutating it. Returns a version-1\n * `WorkspaceStateRecord`: `unsupported` for non-Git workspaces, `failed`\n * when git could not run or the observation was cancelled, and `incomplete`\n * when a limit truncated the capture. `observed` never implies more than\n * `coverage` states.\n */\nexport interface ToplevelComparison {\n\t/** Whether the physically resolved workspace root sits under the toplevel. */\n\tinside: boolean;\n\t/** Root path relative to the toplevel; strip this prefix from porcelain paths. */\n\tprefix: string;\n\t/** Physically resolved toplevel; base for absolute-path work like hashing. */\n\ttoplevelReal: string;\n}\n\n/**\n * Decide containment of the workspace root inside git's toplevel after both\n * sides are physically resolved. Windows spellings diverge from what Node's\n * realpath returns for the same directory (short 8.3 names, drive-letter\n * case), so the win32 branch resolves through the native resolver, folds\n * case for the comparison, and uses win32 path semantics. The platform and\n * resolver are injectable so tests can exercise those branches anywhere.\n */\nexport function compareToplevel(\n\tworkspaceRoot: string,\n\ttoplevel: string,\n\toptions?: {\n\t\tplatform?: NodeJS.Platform;\n\t\trealpath?: (path: string) => string;\n\t},\n): ToplevelComparison {\n\tconst platform = options?.platform ?? process.platform;\n\tconst isWin32 = platform === \"win32\";\n\tconst realpath =\n\t\toptions?.realpath ??\n\t\t(isWin32\n\t\t\t? (p: string) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn realpathSync.native(p);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn realpathSync(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: realpathSync);\n\tconst rootReal = realpath(workspaceRoot);\n\tconst toplevelReal = realpath(toplevel);\n\tconst pathMod = isWin32 ? pathWin32 : { relative, sep, dirname };\n\tconst fold = (p: string) => (isWin32 ? p.toLowerCase() : p);\n\tconst prefix = pathMod.relative(toplevelReal, rootReal);\n\tconst inside = !(prefix === \"..\" || prefix.startsWith(`..${pathMod.sep}`) || isAbsolute(prefix));\n\tif (isWin32 && inside) {\n\t\t// Recompute the strip prefix under case folding so a divergent drive or\n\t\t// directory case still yields the clean \"\" or relative form.\n\t\tconst folded = pathMod.relative(fold(toplevelReal), fold(rootReal));\n\t\treturn { inside, prefix: folded, toplevelReal };\n\t}\n\treturn { inside, prefix, toplevelReal };\n}\n\nexport async function observeWorkspaceGit(\n\tworkspaceRoot: string,\n\toptions?: GitObserveOptions,\n): Promise<WorkspaceStateRecord> {\n\tconst maxPaths = options?.maxPaths ?? DEFAULT_MAX_PATHS;\n\tconst maxHashBytes = options?.maxHashBytes ?? DEFAULT_MAX_HASH_BYTES;\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tconst hashPaths = options?.hashPaths ?? true;\n\tconst signal = options?.signal;\n\tconst excludePaths = (options?.excludePaths ?? [])\n\t\t.map((p) => p.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/\\/+$/, \"\"))\n\t\t.filter((p) => p.length > 0);\n\tconst isExcluded = (repoPath: string): boolean =>\n\t\texcludePaths.some((ex) => repoPath === ex || repoPath.startsWith(`${ex}/`));\n\n\tconst warnings: string[] = [];\n\tconst failed = (message: string): WorkspaceStateRecord => ({\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: \"failed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\tcoverage: emptyCoverage(),\n\t\tpaths: [],\n\t\twarnings: [message],\n\t});\n\n\tif (signal?.aborted) {\n\t\treturn failed(\"workspace observation cancelled\");\n\t}\n\n\tconst head = await runGit(workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], timeoutMs, signal);\n\tif (head.spawnError) return failed(\"git could not be executed\");\n\tif (head.timedOut) return failed(\"workspace observation timed out\");\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (!head.ok) {\n\t\tif (/not a git repository/i.test(head.stderr) || head.code === 128) {\n\t\t\treturn {\n\t\t\t\t...failed(\"not a git repository\"),\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t};\n\t\t}\n\t\treturn failed(\"git rev-parse failed\");\n\t}\n\t// git resolves the toplevel physically (macOS /tmp -> /private/tmp,\n\t// Windows drive-letter case and short names), so containment is decided\n\t// through compareToplevel; otherwise a divergent spelling of the same\n\t// directory would fail the observation or corrupt every reported path.\n\tconst toplevel = head.stdout.toString(\"utf-8\").trim();\n\tconst comparison = compareToplevel(workspaceRoot, toplevel);\n\tif (!comparison.inside) {\n\t\treturn failed(\"workspace root is outside the repository toplevel\");\n\t}\n\tconst insidePrefix = comparison.prefix;\n\n\tconst headCommit = await runGit(workspaceRoot, [\"rev-parse\", \"--verify\", \"HEAD\"], timeoutMs, signal);\n\tconst mergeHead = await runGit(workspaceRoot, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"], timeoutMs, signal);\n\tif (signal?.aborted) return failed(\"workspace observation cancelled\");\n\tif (mergeHead.ok) warnings.push(\"merge in progress\");\n\tconst branch = await runGit(workspaceRoot, [\"branch\", \"--show-current\"], timeoutMs, signal);\n\n\tconst headCommitValue = headCommit.ok ? headCommit.stdout.toString(\"utf-8\").trim() : undefined;\n\tif (!headCommit.ok) warnings.push(\"unborn HEAD (no commits)\");\n\tconst branchValue = branch.ok ? branch.stdout.toString(\"utf-8\").trim() : \"\";\n\tif (branchValue === \"\") warnings.push(\"detached HEAD\");\n\n\t// Index baseline.\n\tconst index = await runGit(workspaceRoot, [\"ls-files\", \"--stage\", \"-z\"], timeoutMs, signal);\n\tif (!index.ok) return failed(\"git ls-files failed\");\n\tconst indexDigest = `sha256:${createHash(\"sha256\").update(index.stdout).digest(\"hex\")}`;\n\n\t// Staged / unstaged / untracked / ignored change sets.\n\tconst status = await runGit(\n\t\tworkspaceRoot,\n\t\t[\"status\", \"--porcelain\", \"-z\", \"--untracked-files=all\", \"--ignored=matching\"],\n\t\ttimeoutMs,\n\t\tsignal,\n\t);\n\tif (!status.ok) return failed(\"git status failed\");\n\n\tconst paths: WorkspaceStatePath[] = [];\n\tlet truncated = false;\n\tlet skippedHashes = 0;\n\tfor (const entry of parsePorcelainZ(status.stdout)) {\n\t\tif (isExcluded(entry.path) || (entry.previousPath !== undefined && isExcluded(entry.previousPath))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (paths.length >= maxPaths) {\n\t\t\ttruncated = true;\n\t\t\twarnings.push(`path list truncated at ${maxPaths} paths`);\n\t\t\tbreak;\n\t\t}\n\t\tconst { kind, staged, unstaged, ignored } = classify(entry);\n\t\tconst workspacePath: WorkspaceStatePath = {\n\t\t\tpath: prefixRelative(insidePrefix, entry.path),\n\t\t\tkind,\n\t\t\tstaged,\n\t\t\tunstaged,\n\t\t\t...(ignored ? { ignored: true } : {}),\n\t\t\t...(entry.previousPath ? { previousPath: prefixRelative(insidePrefix, entry.previousPath) } : {}),\n\t\t};\n\t\tif (hashPaths && kind !== \"deleted\") {\n\t\t\tconst { hash, skipped } = hashWorkspacePath(comparison.toplevelReal, entry.path, maxHashBytes);\n\t\t\tif (hash) workspacePath.contentHash = hash;\n\t\t\telse if (skipped) skippedHashes++;\n\t\t}\n\t\tpaths.push(workspacePath);\n\t}\n\tif (skippedHashes > 0) warnings.push(`content hashing skipped for ${skippedHashes} path(s)`);\n\n\t// Digest over exactly the paths this record includes, so a later\n\t// comparison can cheaply detect that the covered state moved.\n\tconst digestInput = paths\n\t\t.map(\n\t\t\t(p) =>\n\t\t\t\t`${p.path}\\0${p.kind}\\0${p.staged ? \"S\" : \"\"}${p.unstaged ? \"U\" : \"\"}${p.ignored ? \"I\" : \"\"}\\0${p.contentHash ?? \"\"}\\n`,\n\t\t)\n\t\t.sort()\n\t\t.join(\"\");\n\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tobservationId: randomUUID(),\n\t\tstatus: truncated || skippedHashes > 0 ? \"incomplete\" : \"observed\",\n\t\tbackend: \"git\",\n\t\tworkspaceRoot,\n\t\tcapturedAt: new Date().toISOString(),\n\t\t// `hashes` reflects the configured policy: a disabled hash pass is a\n\t\t// coverage statement, not an incomplete capture.\n\t\tcoverage: { ...fullCoverage(), hashes: hashPaths },\n\t\tbase: {\n\t\t\t...(headCommitValue ? { headCommit: headCommitValue } : {}),\n\t\t\t...(branchValue ? { branch: branchValue } : {}),\n\t\t\tindexDigest,\n\t\t\t...(truncated ? {} : { worktreeDigest: `sha256:${createHash(\"sha256\").update(digestInput).digest(\"hex\")}` }),\n\t\t},\n\t\tpaths,\n\t\twarnings,\n\t};\n}\n"]}
@@ -0,0 +1,89 @@
1
+ import type { ChildWorkspaceRequest, WorkspaceReleaseOutcome } from "../delegation/runtime.ts";
2
+ export interface GitWorktreeWorkspaceOwnerOptions {
3
+ /** Per-command timeout in milliseconds (default 15 000). */
4
+ timeoutMs?: number;
5
+ }
6
+ /**
7
+ * The production workspace owner for worktree isolation (spec
8
+ * 2026-09-09-run-and-child-session-architecture.md, "Parallel work and
9
+ * ownership"). The workspace subsystem owns git; delegation stays git-free:
10
+ * `prepare()` creates one linked worktree per child at
11
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` on branch
12
+ * `apex-child-<short-sessionId>` and returns the request with `root` set, which
13
+ * is how the child session's cwd ends up inside the worktree. `release()`
14
+ * removes it again. Refusals are throws: outside a git repository (or without
15
+ * a git executable) `prepare()` throws, and the delegation runtime surfaces
16
+ * that throw as the delegation's failure.
17
+ *
18
+ * Cleanup choices, deliberately:
19
+ * - `git worktree remove` runs without `--force` and there is no automatic
20
+ * fallback. When the plain remove refuses -- typically a dirty tree, since a
21
+ * child that wrote files leaves untracked content behind -- the tree is
22
+ * inspected and *kept*, reported as a `kept: "dirty"` release outcome so
23
+ * uncommitted child work is preserved. Only an explicit
24
+ * `release(id, { force: true })` from the caller force-removes. The child's
25
+ * branch ref survives either way, so the parent can still integrate through
26
+ * an explicit git workflow, and this owner never merges, commits, or pushes
27
+ * anything.
28
+ * - A worktree that is already gone is tolerated, so releasing twice, or
29
+ * releasing a failed delegation whose `prepare()` died before creating
30
+ * anything, are both no-ops.
31
+ * - The worktrees directory is kept out of the parent checkout's `git status`
32
+ * through the repository-local `.git/info/exclude` -- the one ignore channel
33
+ * that cannot touch tracked project files such as `.gitignore` (which the
34
+ * spec forbids the coordinator from editing). Left unignored, the linked
35
+ * worktrees would report as untracked churn and spuriously retire
36
+ * verification results. Best effort only: a failure here never fails the
37
+ * worktree itself.
38
+ */
39
+ export declare class GitWorktreeWorkspaceOwner {
40
+ private readonly workspaceRoot;
41
+ private readonly timeoutMs;
42
+ constructor(workspaceRoot: string, options?: GitWorktreeWorkspaceOwnerOptions);
43
+ /** The worktree directory this owner uses for one child session. */
44
+ private worktreeDir;
45
+ /**
46
+ * Create the child's worktree and return the request with `root` set to its
47
+ * path. Throws (refusing the delegation) when the workspace is not a git
48
+ * repository, git is unavailable, or the worktree cannot be created.
49
+ */
50
+ prepare(request: ChildWorkspaceRequest & {
51
+ sessionId: string;
52
+ }): Promise<ChildWorkspaceRequest & {
53
+ root: string;
54
+ }>;
55
+ /**
56
+ * Remove the child's worktree safely. Plain `git worktree remove` first; if
57
+ * it refuses, the tree's status decides the outcome: uncommitted or
58
+ * untracked content means the tree is kept ("dirty") so child work is
59
+ * never destroyed silently; a clean tree that still cannot be removed is
60
+ * reported as "failed". `--force` runs only when the caller passes
61
+ * `options.force` -- the explicit escape hatch, never an automatic
62
+ * fallback. Already-removed worktrees are tolerated and never throw.
63
+ */
64
+ release(sessionId: string, options?: {
65
+ force?: boolean;
66
+ }): Promise<WorkspaceReleaseOutcome>;
67
+ /**
68
+ * Read-only verification for explicit workspace recovery (spec
69
+ * 2026-09-09-run-and-child-session-architecture.md, "Workspace states and
70
+ * explicit recovery"): confirm `root` is still THIS owner's worktree for
71
+ * `sessionId` in the CURRENT workspace. Three checks, each refusal naming
72
+ * the check: (layout) the recorded path is exactly the owner's
73
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` and exists;
74
+ * (admin entry) `<root>/.git` is a linked-worktree pointer file whose gitdir
75
+ * resolves into the current repository's `.git/worktrees`; (branch) the
76
+ * checked-out branch is `apex-child-<short-sessionId>`. Never creates,
77
+ * checks out, resets, or force-removes anything -- git inspection is
78
+ * read-only (`rev-parse`, `status`) plus the administrative entry read.
79
+ * Returns the worktree's dirty state so callers can report preserved
80
+ * uncommitted work without a second inspection.
81
+ */
82
+ verify(sessionId: string, root: string): Promise<{
83
+ dirty: boolean;
84
+ }>;
85
+ private currentGitDir;
86
+ private requireRepository;
87
+ private keepWorktreesUntracked;
88
+ }
89
+ //# sourceMappingURL=git-worktree-owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-worktree-owner.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/git-worktree-owner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAqC/F,MAAM,WAAW,gCAAgC;IAChD,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,YAAY,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gCAAgC,EAG5E;IAED,oEAAoE;IACpE,OAAO,CAAC,WAAW;IAOnB;;;;OAIG;IACG,OAAO,CACZ,OAAO,EAAE,qBAAqB,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC,qBAAqB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAUnD;IAED;;;;;;;;OAQG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAuBhG;IAED;;;;;;;;;;;;;;OAcG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAmDzE;YAGa,aAAa;YAWb,iBAAiB;YAgBjB,sBAAsB;CAoBpC","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport type { ChildWorkspaceRequest, WorkspaceReleaseOutcome } from \"../delegation/runtime.ts\";\nimport { type GitRunResult, runGit } from \"./git-observer.ts\";\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\n/** Fixed location beneath the workspace root, so a child's checkout is always findable and always inside the workspace. */\nconst WORKTREES_SEGMENT = join(\".apex-code\", \"worktrees\");\nconst BRANCH_PREFIX = \"apex-child-\";\n/**\n * Short branch ids keep `git branch` output readable. Eight hex characters of a\n * UUID still bound prefix collisions between concurrent children to ~2^-32 per\n * pair; the branch ref (not the checkout) is the durable artifact anyway.\n */\nconst SHORT_SESSION_ID_LENGTH = 8;\n\n/**\n * A session id names both a filesystem path and a branch here, so it must be a\n * single safe component. A caller-supplied delegation handle flows in as the\n * session id, and `../../` or a leading `-` must never become a path traversal\n * or an option injection. Same allowlist shape the checkpoint engine uses for\n * ref components.\n */\nfunction isSafeSessionId(sessionId: string): boolean {\n\treturn /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sessionId) && !sessionId.includes(\"..\") && !sessionId.endsWith(\".lock\");\n}\n\nfunction shortSessionId(sessionId: string): string {\n\treturn sessionId.slice(0, SHORT_SESSION_ID_LENGTH);\n}\n\n/** One-line human explanation of a failed git run, for refusal and failure errors. */\nfunction describeFailure(result: GitRunResult): string {\n\tif (result.spawnError) return \"git could not be executed\";\n\tif (result.timedOut) return \"git timed out\";\n\tconst detail = result.stderr.trim().split(\"\\n\")[0] ?? \"\";\n\treturn detail ? `git failed: ${detail}` : `git failed with exit code ${result.code ?? \"unknown\"}`;\n}\n\nexport interface GitWorktreeWorkspaceOwnerOptions {\n\t/** Per-command timeout in milliseconds (default 15 000). */\n\ttimeoutMs?: number;\n}\n\n/**\n * The production workspace owner for worktree isolation (spec\n * 2026-09-09-run-and-child-session-architecture.md, \"Parallel work and\n * ownership\"). The workspace subsystem owns git; delegation stays git-free:\n * `prepare()` creates one linked worktree per child at\n * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` on branch\n * `apex-child-<short-sessionId>` and returns the request with `root` set, which\n * is how the child session's cwd ends up inside the worktree. `release()`\n * removes it again. Refusals are throws: outside a git repository (or without\n * a git executable) `prepare()` throws, and the delegation runtime surfaces\n * that throw as the delegation's failure.\n *\n * Cleanup choices, deliberately:\n * - `git worktree remove` runs without `--force` and there is no automatic\n * fallback. When the plain remove refuses -- typically a dirty tree, since a\n * child that wrote files leaves untracked content behind -- the tree is\n * inspected and *kept*, reported as a `kept: \"dirty\"` release outcome so\n * uncommitted child work is preserved. Only an explicit\n * `release(id, { force: true })` from the caller force-removes. The child's\n * branch ref survives either way, so the parent can still integrate through\n * an explicit git workflow, and this owner never merges, commits, or pushes\n * anything.\n * - A worktree that is already gone is tolerated, so releasing twice, or\n * releasing a failed delegation whose `prepare()` died before creating\n * anything, are both no-ops.\n * - The worktrees directory is kept out of the parent checkout's `git status`\n * through the repository-local `.git/info/exclude` -- the one ignore channel\n * that cannot touch tracked project files such as `.gitignore` (which the\n * spec forbids the coordinator from editing). Left unignored, the linked\n * worktrees would report as untracked churn and spuriously retire\n * verification results. Best effort only: a failure here never fails the\n * worktree itself.\n */\nexport class GitWorktreeWorkspaceOwner {\n\tprivate readonly workspaceRoot: string;\n\tprivate readonly timeoutMs: number;\n\n\tconstructor(workspaceRoot: string, options?: GitWorktreeWorkspaceOwnerOptions) {\n\t\tthis.workspaceRoot = workspaceRoot;\n\t\tthis.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\t}\n\n\t/** The worktree directory this owner uses for one child session. */\n\tprivate worktreeDir(sessionId: string): string {\n\t\tif (!isSafeSessionId(sessionId)) {\n\t\t\tthrow new Error(`Worktree isolation refused: session id \"${sessionId}\" is not a safe path component.`);\n\t\t}\n\t\treturn join(this.workspaceRoot, WORKTREES_SEGMENT, sessionId);\n\t}\n\n\t/**\n\t * Create the child's worktree and return the request with `root` set to its\n\t * path. Throws (refusing the delegation) when the workspace is not a git\n\t * repository, git is unavailable, or the worktree cannot be created.\n\t */\n\tasync prepare(\n\t\trequest: ChildWorkspaceRequest & { sessionId: string },\n\t): Promise<ChildWorkspaceRequest & { root: string }> {\n\t\tconst dir = this.worktreeDir(request.sessionId);\n\t\tconst toplevel = await this.requireRepository(request.sessionId);\n\t\tawait this.keepWorktreesUntracked(toplevel);\n\t\tconst branch = `${BRANCH_PREFIX}${shortSessionId(request.sessionId)}`;\n\t\tconst added = await runGit(this.workspaceRoot, [\"worktree\", \"add\", \"-b\", branch, dir], this.timeoutMs);\n\t\tif (!added.ok) {\n\t\t\tthrow new Error(`Worktree isolation failed for session \"${request.sessionId}\": ${describeFailure(added)}.`);\n\t\t}\n\t\treturn { ...request, root: dir };\n\t}\n\n\t/**\n\t * Remove the child's worktree safely. Plain `git worktree remove` first; if\n\t * it refuses, the tree's status decides the outcome: uncommitted or\n\t * untracked content means the tree is kept (\"dirty\") so child work is\n\t * never destroyed silently; a clean tree that still cannot be removed is\n\t * reported as \"failed\". `--force` runs only when the caller passes\n\t * `options.force` -- the explicit escape hatch, never an automatic\n\t * fallback. Already-removed worktrees are tolerated and never throw.\n\t */\n\tasync release(sessionId: string, options?: { force?: boolean }): Promise<WorkspaceReleaseOutcome> {\n\t\tif (!isSafeSessionId(sessionId)) return { removed: true }; // an unsafe id can never have been prepared\n\t\tconst dir = this.worktreeDir(sessionId);\n\t\tif (!existsSync(dir)) {\n\t\t\t// Never created, already removed, or removed out of band: drop any\n\t\t\t// stale administrative entry and succeed either way.\n\t\t\tawait runGit(this.workspaceRoot, [\"worktree\", \"prune\"], this.timeoutMs);\n\t\t\treturn { removed: true };\n\t\t}\n\t\tif (options?.force) {\n\t\t\tconst forced = await runGit(this.workspaceRoot, [\"worktree\", \"remove\", \"--force\", dir], this.timeoutMs);\n\t\t\tif (forced.ok) return { removed: true };\n\t\t\treturn { removed: false, kept: \"failed\", dir, error: describeFailure(forced) };\n\t\t}\n\t\tconst removed = await runGit(this.workspaceRoot, [\"worktree\", \"remove\", dir], this.timeoutMs);\n\t\tif (removed.ok) return { removed: true };\n\t\t// The plain remove refused. Inspect the tree: uncommitted/untracked\n\t\t// content means the refusal is protective and the tree stays.\n\t\tconst status = await runGit(dir, [\"status\", \"--porcelain\"], this.timeoutMs);\n\t\tif (status.ok && status.stdout.toString(\"utf-8\").trim() !== \"\") {\n\t\t\treturn { removed: false, kept: \"dirty\", dir, error: describeFailure(removed) };\n\t\t}\n\t\treturn { removed: false, kept: \"failed\", dir, error: describeFailure(removed) };\n\t}\n\n\t/**\n\t * Read-only verification for explicit workspace recovery (spec\n\t * 2026-09-09-run-and-child-session-architecture.md, \"Workspace states and\n\t * explicit recovery\"): confirm `root` is still THIS owner's worktree for\n\t * `sessionId` in the CURRENT workspace. Three checks, each refusal naming\n\t * the check: (layout) the recorded path is exactly the owner's\n\t * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` and exists;\n\t * (admin entry) `<root>/.git` is a linked-worktree pointer file whose gitdir\n\t * resolves into the current repository's `.git/worktrees`; (branch) the\n\t * checked-out branch is `apex-child-<short-sessionId>`. Never creates,\n\t * checks out, resets, or force-removes anything -- git inspection is\n\t * read-only (`rev-parse`, `status`) plus the administrative entry read.\n\t * Returns the worktree's dirty state so callers can report preserved\n\t * uncommitted work without a second inspection.\n\t */\n\tasync verify(sessionId: string, root: string): Promise<{ dirty: boolean }> {\n\t\tif (!isSafeSessionId(sessionId)) {\n\t\t\tthrow new Error(`layout check failed: session id \"${sessionId}\" is not a safe path component.`);\n\t\t}\n\t\t// (c) The directory sits at this owner's layout for the session.\n\t\tconst expected = this.worktreeDir(sessionId);\n\t\tif (resolve(root) !== expected) {\n\t\t\tthrow new Error(\n\t\t\t\t`layout check failed: recorded root \"${root}\" is not this workspace's worktree location for session \"${sessionId}\" (expected \"${expected}\").`,\n\t\t\t);\n\t\t}\n\t\tif (!existsSync(root)) {\n\t\t\tthrow new Error(`layout check failed: the recorded worktree root \"${root}\" does not exist.`);\n\t\t}\n\t\t// (a) The directory is a linked worktree of the CURRENT parent workspace:\n\t\t// git resolves its administrative entry, and the resolved gitdir must sit\n\t\t// inside this repository's worktrees dir. Git reads the pointer itself,\n\t\t// which direct file reads cannot do portably (Windows denies the open).\n\t\tconst resolved = await runGit(root, [\"rev-parse\", \"--absolute-git-dir\"], this.timeoutMs);\n\t\tif (!resolved.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: \"${root}\" is not a linked worktree (git could not resolve its administrative entry: ${describeFailure(resolved)}).`,\n\t\t\t);\n\t\t}\n\t\tconst gitdir = resolve(resolved.stdout.toString(\"utf-8\").trim());\n\t\tconst parentGitDir = await this.currentGitDir();\n\t\tconst worktreesDir = join(parentGitDir, \"worktrees\");\n\t\tconst rel = relative(worktreesDir, gitdir);\n\t\tif (rel === \"\" || isAbsolute(rel) || rel.startsWith(\"..\") || !existsSync(gitdir)) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: \"${root}\" is not a linked worktree of the current repository (its gitdir \"${gitdir}\" does not resolve into \"${worktreesDir}\").`,\n\t\t\t);\n\t\t}\n\t\t// (b) The checked-out branch is this child's own.\n\t\tconst branch = `${BRANCH_PREFIX}${shortSessionId(sessionId)}`;\n\t\tconst head = await runGit(root, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], this.timeoutMs);\n\t\tconst checkedOut = head.ok ? head.stdout.toString(\"utf-8\").trim() : \"\";\n\t\tif (!head.ok || checkedOut !== branch) {\n\t\t\tthrow new Error(\n\t\t\t\t`branch check failed: the worktree for session \"${sessionId}\" has ${\n\t\t\t\t\thead.ok ? `\"${checkedOut}\" checked out` : describeFailure(head)\n\t\t\t\t}, expected \"${branch}\".`,\n\t\t\t);\n\t\t}\n\t\t// The dirty report is informational, not a gate: uncommitted child work is\n\t\t// exactly what recovery must preserve, never destroy.\n\t\tconst status = await runGit(root, [\"status\", \"--porcelain\"], this.timeoutMs);\n\t\tif (!status.ok) {\n\t\t\tthrow new Error(`status check failed: the worktree's status could not be read: ${describeFailure(status)}.`);\n\t\t}\n\t\treturn { dirty: status.stdout.toString(\"utf-8\").trim() !== \"\" };\n\t}\n\n\t/** The current workspace repository's absolute git dir, for admin-entry verification. */\n\tprivate async currentGitDir(): Promise<string> {\n\t\tconst probe = await runGit(this.workspaceRoot, [\"rev-parse\", \"--absolute-git-dir\"], this.timeoutMs);\n\t\tif (!probe.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: the current workspace is not a readable git repository (${describeFailure(probe)}).`,\n\t\t\t);\n\t\t}\n\t\treturn resolve(this.workspaceRoot, probe.stdout.toString(\"utf-8\").trim());\n\t}\n\n\t/** Verify git is usable and the workspace sits inside a repository; returns the repository toplevel. */\n\tprivate async requireRepository(sessionId: string): Promise<string> {\n\t\tconst probe = await runGit(this.workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], this.timeoutMs);\n\t\tif (!probe.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Worktree isolation refused for session \"${sessionId}\": ${describeFailure(probe)}; worktree isolation requires a git repository.`,\n\t\t\t);\n\t\t}\n\t\treturn probe.stdout.toString(\"utf-8\").trim();\n\t}\n\n\t/**\n\t * Keep `<...>/.apex-code/worktrees/` out of the parent checkout's status\n\t * via `.git/info/exclude` (idempotent; forward-slash pattern as git\n\t * requires, repo-relative so it also holds when the workspace root is a\n\t * subdirectory of the repository).\n\t */\n\tprivate async keepWorktreesUntracked(toplevel: string): Promise<void> {\n\t\tconst dir = join(this.workspaceRoot, WORKTREES_SEGMENT);\n\t\tconst repoRelative = relative(toplevel, dir);\n\t\tif (repoRelative === \"\" || isAbsolute(repoRelative) || repoRelative.startsWith(\"..\")) return;\n\t\tconst entry = `${repoRelative.split(sep).join(\"/\")}/`;\n\t\ttry {\n\t\t\tconst ignored = await runGit(toplevel, [\"check-ignore\", \"-q\", entry], this.timeoutMs);\n\t\t\tif (ignored.ok) return;\n\t\t\tconst excludePathResult = await runGit(toplevel, [\"rev-parse\", \"--git-path\", \"info/exclude\"], this.timeoutMs);\n\t\t\tif (!excludePathResult.ok) return;\n\t\t\tconst excludePath = resolve(toplevel, excludePathResult.stdout.toString(\"utf-8\").trim());\n\t\t\tmkdirSync(dirname(excludePath), { recursive: true });\n\t\t\tconst current = existsSync(excludePath) ? readFileSync(excludePath, \"utf-8\") : \"\";\n\t\t\tif (current.split(/\\r?\\n/).includes(entry)) return;\n\t\t\tconst needsNewLine = current.length > 0 && !current.endsWith(\"\\n\");\n\t\t\twriteFileSync(excludePath, `${needsNewLine ? \"\\n\" : \"\"}${entry}\\n`, { flag: \"a\", encoding: \"utf-8\" });\n\t\t} catch {\n\t\t\t// Hygiene only: an unwritable or unreadable exclude never fails the worktree.\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,240 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { runGit } from "./git-observer.js";
4
+ const DEFAULT_TIMEOUT_MS = 15_000;
5
+ /** Fixed location beneath the workspace root, so a child's checkout is always findable and always inside the workspace. */
6
+ const WORKTREES_SEGMENT = join(".apex-code", "worktrees");
7
+ const BRANCH_PREFIX = "apex-child-";
8
+ /**
9
+ * Short branch ids keep `git branch` output readable. Eight hex characters of a
10
+ * UUID still bound prefix collisions between concurrent children to ~2^-32 per
11
+ * pair; the branch ref (not the checkout) is the durable artifact anyway.
12
+ */
13
+ const SHORT_SESSION_ID_LENGTH = 8;
14
+ /**
15
+ * A session id names both a filesystem path and a branch here, so it must be a
16
+ * single safe component. A caller-supplied delegation handle flows in as the
17
+ * session id, and `../../` or a leading `-` must never become a path traversal
18
+ * or an option injection. Same allowlist shape the checkpoint engine uses for
19
+ * ref components.
20
+ */
21
+ function isSafeSessionId(sessionId) {
22
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sessionId) && !sessionId.includes("..") && !sessionId.endsWith(".lock");
23
+ }
24
+ function shortSessionId(sessionId) {
25
+ return sessionId.slice(0, SHORT_SESSION_ID_LENGTH);
26
+ }
27
+ /** One-line human explanation of a failed git run, for refusal and failure errors. */
28
+ function describeFailure(result) {
29
+ if (result.spawnError)
30
+ return "git could not be executed";
31
+ if (result.timedOut)
32
+ return "git timed out";
33
+ const detail = result.stderr.trim().split("\n")[0] ?? "";
34
+ return detail ? `git failed: ${detail}` : `git failed with exit code ${result.code ?? "unknown"}`;
35
+ }
36
+ /**
37
+ * The production workspace owner for worktree isolation (spec
38
+ * 2026-09-09-run-and-child-session-architecture.md, "Parallel work and
39
+ * ownership"). The workspace subsystem owns git; delegation stays git-free:
40
+ * `prepare()` creates one linked worktree per child at
41
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` on branch
42
+ * `apex-child-<short-sessionId>` and returns the request with `root` set, which
43
+ * is how the child session's cwd ends up inside the worktree. `release()`
44
+ * removes it again. Refusals are throws: outside a git repository (or without
45
+ * a git executable) `prepare()` throws, and the delegation runtime surfaces
46
+ * that throw as the delegation's failure.
47
+ *
48
+ * Cleanup choices, deliberately:
49
+ * - `git worktree remove` runs without `--force` and there is no automatic
50
+ * fallback. When the plain remove refuses -- typically a dirty tree, since a
51
+ * child that wrote files leaves untracked content behind -- the tree is
52
+ * inspected and *kept*, reported as a `kept: "dirty"` release outcome so
53
+ * uncommitted child work is preserved. Only an explicit
54
+ * `release(id, { force: true })` from the caller force-removes. The child's
55
+ * branch ref survives either way, so the parent can still integrate through
56
+ * an explicit git workflow, and this owner never merges, commits, or pushes
57
+ * anything.
58
+ * - A worktree that is already gone is tolerated, so releasing twice, or
59
+ * releasing a failed delegation whose `prepare()` died before creating
60
+ * anything, are both no-ops.
61
+ * - The worktrees directory is kept out of the parent checkout's `git status`
62
+ * through the repository-local `.git/info/exclude` -- the one ignore channel
63
+ * that cannot touch tracked project files such as `.gitignore` (which the
64
+ * spec forbids the coordinator from editing). Left unignored, the linked
65
+ * worktrees would report as untracked churn and spuriously retire
66
+ * verification results. Best effort only: a failure here never fails the
67
+ * worktree itself.
68
+ */
69
+ export class GitWorktreeWorkspaceOwner {
70
+ workspaceRoot;
71
+ timeoutMs;
72
+ constructor(workspaceRoot, options) {
73
+ this.workspaceRoot = workspaceRoot;
74
+ this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
75
+ }
76
+ /** The worktree directory this owner uses for one child session. */
77
+ worktreeDir(sessionId) {
78
+ if (!isSafeSessionId(sessionId)) {
79
+ throw new Error(`Worktree isolation refused: session id "${sessionId}" is not a safe path component.`);
80
+ }
81
+ return join(this.workspaceRoot, WORKTREES_SEGMENT, sessionId);
82
+ }
83
+ /**
84
+ * Create the child's worktree and return the request with `root` set to its
85
+ * path. Throws (refusing the delegation) when the workspace is not a git
86
+ * repository, git is unavailable, or the worktree cannot be created.
87
+ */
88
+ async prepare(request) {
89
+ const dir = this.worktreeDir(request.sessionId);
90
+ const toplevel = await this.requireRepository(request.sessionId);
91
+ await this.keepWorktreesUntracked(toplevel);
92
+ const branch = `${BRANCH_PREFIX}${shortSessionId(request.sessionId)}`;
93
+ const added = await runGit(this.workspaceRoot, ["worktree", "add", "-b", branch, dir], this.timeoutMs);
94
+ if (!added.ok) {
95
+ throw new Error(`Worktree isolation failed for session "${request.sessionId}": ${describeFailure(added)}.`);
96
+ }
97
+ return { ...request, root: dir };
98
+ }
99
+ /**
100
+ * Remove the child's worktree safely. Plain `git worktree remove` first; if
101
+ * it refuses, the tree's status decides the outcome: uncommitted or
102
+ * untracked content means the tree is kept ("dirty") so child work is
103
+ * never destroyed silently; a clean tree that still cannot be removed is
104
+ * reported as "failed". `--force` runs only when the caller passes
105
+ * `options.force` -- the explicit escape hatch, never an automatic
106
+ * fallback. Already-removed worktrees are tolerated and never throw.
107
+ */
108
+ async release(sessionId, options) {
109
+ if (!isSafeSessionId(sessionId))
110
+ return { removed: true }; // an unsafe id can never have been prepared
111
+ const dir = this.worktreeDir(sessionId);
112
+ if (!existsSync(dir)) {
113
+ // Never created, already removed, or removed out of band: drop any
114
+ // stale administrative entry and succeed either way.
115
+ await runGit(this.workspaceRoot, ["worktree", "prune"], this.timeoutMs);
116
+ return { removed: true };
117
+ }
118
+ if (options?.force) {
119
+ const forced = await runGit(this.workspaceRoot, ["worktree", "remove", "--force", dir], this.timeoutMs);
120
+ if (forced.ok)
121
+ return { removed: true };
122
+ return { removed: false, kept: "failed", dir, error: describeFailure(forced) };
123
+ }
124
+ const removed = await runGit(this.workspaceRoot, ["worktree", "remove", dir], this.timeoutMs);
125
+ if (removed.ok)
126
+ return { removed: true };
127
+ // The plain remove refused. Inspect the tree: uncommitted/untracked
128
+ // content means the refusal is protective and the tree stays.
129
+ const status = await runGit(dir, ["status", "--porcelain"], this.timeoutMs);
130
+ if (status.ok && status.stdout.toString("utf-8").trim() !== "") {
131
+ return { removed: false, kept: "dirty", dir, error: describeFailure(removed) };
132
+ }
133
+ return { removed: false, kept: "failed", dir, error: describeFailure(removed) };
134
+ }
135
+ /**
136
+ * Read-only verification for explicit workspace recovery (spec
137
+ * 2026-09-09-run-and-child-session-architecture.md, "Workspace states and
138
+ * explicit recovery"): confirm `root` is still THIS owner's worktree for
139
+ * `sessionId` in the CURRENT workspace. Three checks, each refusal naming
140
+ * the check: (layout) the recorded path is exactly the owner's
141
+ * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` and exists;
142
+ * (admin entry) `<root>/.git` is a linked-worktree pointer file whose gitdir
143
+ * resolves into the current repository's `.git/worktrees`; (branch) the
144
+ * checked-out branch is `apex-child-<short-sessionId>`. Never creates,
145
+ * checks out, resets, or force-removes anything -- git inspection is
146
+ * read-only (`rev-parse`, `status`) plus the administrative entry read.
147
+ * Returns the worktree's dirty state so callers can report preserved
148
+ * uncommitted work without a second inspection.
149
+ */
150
+ async verify(sessionId, root) {
151
+ if (!isSafeSessionId(sessionId)) {
152
+ throw new Error(`layout check failed: session id "${sessionId}" is not a safe path component.`);
153
+ }
154
+ // (c) The directory sits at this owner's layout for the session.
155
+ const expected = this.worktreeDir(sessionId);
156
+ if (resolve(root) !== expected) {
157
+ throw new Error(`layout check failed: recorded root "${root}" is not this workspace's worktree location for session "${sessionId}" (expected "${expected}").`);
158
+ }
159
+ if (!existsSync(root)) {
160
+ throw new Error(`layout check failed: the recorded worktree root "${root}" does not exist.`);
161
+ }
162
+ // (a) The directory is a linked worktree of the CURRENT parent workspace:
163
+ // git resolves its administrative entry, and the resolved gitdir must sit
164
+ // inside this repository's worktrees dir. Git reads the pointer itself,
165
+ // which direct file reads cannot do portably (Windows denies the open).
166
+ const resolved = await runGit(root, ["rev-parse", "--absolute-git-dir"], this.timeoutMs);
167
+ if (!resolved.ok) {
168
+ throw new Error(`admin entry check failed: "${root}" is not a linked worktree (git could not resolve its administrative entry: ${describeFailure(resolved)}).`);
169
+ }
170
+ const gitdir = resolve(resolved.stdout.toString("utf-8").trim());
171
+ const parentGitDir = await this.currentGitDir();
172
+ const worktreesDir = join(parentGitDir, "worktrees");
173
+ const rel = relative(worktreesDir, gitdir);
174
+ if (rel === "" || isAbsolute(rel) || rel.startsWith("..") || !existsSync(gitdir)) {
175
+ throw new Error(`admin entry check failed: "${root}" is not a linked worktree of the current repository (its gitdir "${gitdir}" does not resolve into "${worktreesDir}").`);
176
+ }
177
+ // (b) The checked-out branch is this child's own.
178
+ const branch = `${BRANCH_PREFIX}${shortSessionId(sessionId)}`;
179
+ const head = await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"], this.timeoutMs);
180
+ const checkedOut = head.ok ? head.stdout.toString("utf-8").trim() : "";
181
+ if (!head.ok || checkedOut !== branch) {
182
+ throw new Error(`branch check failed: the worktree for session "${sessionId}" has ${head.ok ? `"${checkedOut}" checked out` : describeFailure(head)}, expected "${branch}".`);
183
+ }
184
+ // The dirty report is informational, not a gate: uncommitted child work is
185
+ // exactly what recovery must preserve, never destroy.
186
+ const status = await runGit(root, ["status", "--porcelain"], this.timeoutMs);
187
+ if (!status.ok) {
188
+ throw new Error(`status check failed: the worktree's status could not be read: ${describeFailure(status)}.`);
189
+ }
190
+ return { dirty: status.stdout.toString("utf-8").trim() !== "" };
191
+ }
192
+ /** The current workspace repository's absolute git dir, for admin-entry verification. */
193
+ async currentGitDir() {
194
+ const probe = await runGit(this.workspaceRoot, ["rev-parse", "--absolute-git-dir"], this.timeoutMs);
195
+ if (!probe.ok) {
196
+ throw new Error(`admin entry check failed: the current workspace is not a readable git repository (${describeFailure(probe)}).`);
197
+ }
198
+ return resolve(this.workspaceRoot, probe.stdout.toString("utf-8").trim());
199
+ }
200
+ /** Verify git is usable and the workspace sits inside a repository; returns the repository toplevel. */
201
+ async requireRepository(sessionId) {
202
+ const probe = await runGit(this.workspaceRoot, ["rev-parse", "--show-toplevel"], this.timeoutMs);
203
+ if (!probe.ok) {
204
+ throw new Error(`Worktree isolation refused for session "${sessionId}": ${describeFailure(probe)}; worktree isolation requires a git repository.`);
205
+ }
206
+ return probe.stdout.toString("utf-8").trim();
207
+ }
208
+ /**
209
+ * Keep `<...>/.apex-code/worktrees/` out of the parent checkout's status
210
+ * via `.git/info/exclude` (idempotent; forward-slash pattern as git
211
+ * requires, repo-relative so it also holds when the workspace root is a
212
+ * subdirectory of the repository).
213
+ */
214
+ async keepWorktreesUntracked(toplevel) {
215
+ const dir = join(this.workspaceRoot, WORKTREES_SEGMENT);
216
+ const repoRelative = relative(toplevel, dir);
217
+ if (repoRelative === "" || isAbsolute(repoRelative) || repoRelative.startsWith(".."))
218
+ return;
219
+ const entry = `${repoRelative.split(sep).join("/")}/`;
220
+ try {
221
+ const ignored = await runGit(toplevel, ["check-ignore", "-q", entry], this.timeoutMs);
222
+ if (ignored.ok)
223
+ return;
224
+ const excludePathResult = await runGit(toplevel, ["rev-parse", "--git-path", "info/exclude"], this.timeoutMs);
225
+ if (!excludePathResult.ok)
226
+ return;
227
+ const excludePath = resolve(toplevel, excludePathResult.stdout.toString("utf-8").trim());
228
+ mkdirSync(dirname(excludePath), { recursive: true });
229
+ const current = existsSync(excludePath) ? readFileSync(excludePath, "utf-8") : "";
230
+ if (current.split(/\r?\n/).includes(entry))
231
+ return;
232
+ const needsNewLine = current.length > 0 && !current.endsWith("\n");
233
+ writeFileSync(excludePath, `${needsNewLine ? "\n" : ""}${entry}\n`, { flag: "a", encoding: "utf-8" });
234
+ }
235
+ catch {
236
+ // Hygiene only: an unwritable or unreadable exclude never fails the worktree.
237
+ }
238
+ }
239
+ }
240
+ //# sourceMappingURL=git-worktree-owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-worktree-owner.js","sourceRoot":"","sources":["../../../src/core/workspace/git-worktree-owner.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport type { ChildWorkspaceRequest, WorkspaceReleaseOutcome } from \"../delegation/runtime.ts\";\nimport { type GitRunResult, runGit } from \"./git-observer.ts\";\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\n/** Fixed location beneath the workspace root, so a child's checkout is always findable and always inside the workspace. */\nconst WORKTREES_SEGMENT = join(\".apex-code\", \"worktrees\");\nconst BRANCH_PREFIX = \"apex-child-\";\n/**\n * Short branch ids keep `git branch` output readable. Eight hex characters of a\n * UUID still bound prefix collisions between concurrent children to ~2^-32 per\n * pair; the branch ref (not the checkout) is the durable artifact anyway.\n */\nconst SHORT_SESSION_ID_LENGTH = 8;\n\n/**\n * A session id names both a filesystem path and a branch here, so it must be a\n * single safe component. A caller-supplied delegation handle flows in as the\n * session id, and `../../` or a leading `-` must never become a path traversal\n * or an option injection. Same allowlist shape the checkpoint engine uses for\n * ref components.\n */\nfunction isSafeSessionId(sessionId: string): boolean {\n\treturn /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sessionId) && !sessionId.includes(\"..\") && !sessionId.endsWith(\".lock\");\n}\n\nfunction shortSessionId(sessionId: string): string {\n\treturn sessionId.slice(0, SHORT_SESSION_ID_LENGTH);\n}\n\n/** One-line human explanation of a failed git run, for refusal and failure errors. */\nfunction describeFailure(result: GitRunResult): string {\n\tif (result.spawnError) return \"git could not be executed\";\n\tif (result.timedOut) return \"git timed out\";\n\tconst detail = result.stderr.trim().split(\"\\n\")[0] ?? \"\";\n\treturn detail ? `git failed: ${detail}` : `git failed with exit code ${result.code ?? \"unknown\"}`;\n}\n\nexport interface GitWorktreeWorkspaceOwnerOptions {\n\t/** Per-command timeout in milliseconds (default 15 000). */\n\ttimeoutMs?: number;\n}\n\n/**\n * The production workspace owner for worktree isolation (spec\n * 2026-09-09-run-and-child-session-architecture.md, \"Parallel work and\n * ownership\"). The workspace subsystem owns git; delegation stays git-free:\n * `prepare()` creates one linked worktree per child at\n * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` on branch\n * `apex-child-<short-sessionId>` and returns the request with `root` set, which\n * is how the child session's cwd ends up inside the worktree. `release()`\n * removes it again. Refusals are throws: outside a git repository (or without\n * a git executable) `prepare()` throws, and the delegation runtime surfaces\n * that throw as the delegation's failure.\n *\n * Cleanup choices, deliberately:\n * - `git worktree remove` runs without `--force` and there is no automatic\n * fallback. When the plain remove refuses -- typically a dirty tree, since a\n * child that wrote files leaves untracked content behind -- the tree is\n * inspected and *kept*, reported as a `kept: \"dirty\"` release outcome so\n * uncommitted child work is preserved. Only an explicit\n * `release(id, { force: true })` from the caller force-removes. The child's\n * branch ref survives either way, so the parent can still integrate through\n * an explicit git workflow, and this owner never merges, commits, or pushes\n * anything.\n * - A worktree that is already gone is tolerated, so releasing twice, or\n * releasing a failed delegation whose `prepare()` died before creating\n * anything, are both no-ops.\n * - The worktrees directory is kept out of the parent checkout's `git status`\n * through the repository-local `.git/info/exclude` -- the one ignore channel\n * that cannot touch tracked project files such as `.gitignore` (which the\n * spec forbids the coordinator from editing). Left unignored, the linked\n * worktrees would report as untracked churn and spuriously retire\n * verification results. Best effort only: a failure here never fails the\n * worktree itself.\n */\nexport class GitWorktreeWorkspaceOwner {\n\tprivate readonly workspaceRoot: string;\n\tprivate readonly timeoutMs: number;\n\n\tconstructor(workspaceRoot: string, options?: GitWorktreeWorkspaceOwnerOptions) {\n\t\tthis.workspaceRoot = workspaceRoot;\n\t\tthis.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\t}\n\n\t/** The worktree directory this owner uses for one child session. */\n\tprivate worktreeDir(sessionId: string): string {\n\t\tif (!isSafeSessionId(sessionId)) {\n\t\t\tthrow new Error(`Worktree isolation refused: session id \"${sessionId}\" is not a safe path component.`);\n\t\t}\n\t\treturn join(this.workspaceRoot, WORKTREES_SEGMENT, sessionId);\n\t}\n\n\t/**\n\t * Create the child's worktree and return the request with `root` set to its\n\t * path. Throws (refusing the delegation) when the workspace is not a git\n\t * repository, git is unavailable, or the worktree cannot be created.\n\t */\n\tasync prepare(\n\t\trequest: ChildWorkspaceRequest & { sessionId: string },\n\t): Promise<ChildWorkspaceRequest & { root: string }> {\n\t\tconst dir = this.worktreeDir(request.sessionId);\n\t\tconst toplevel = await this.requireRepository(request.sessionId);\n\t\tawait this.keepWorktreesUntracked(toplevel);\n\t\tconst branch = `${BRANCH_PREFIX}${shortSessionId(request.sessionId)}`;\n\t\tconst added = await runGit(this.workspaceRoot, [\"worktree\", \"add\", \"-b\", branch, dir], this.timeoutMs);\n\t\tif (!added.ok) {\n\t\t\tthrow new Error(`Worktree isolation failed for session \"${request.sessionId}\": ${describeFailure(added)}.`);\n\t\t}\n\t\treturn { ...request, root: dir };\n\t}\n\n\t/**\n\t * Remove the child's worktree safely. Plain `git worktree remove` first; if\n\t * it refuses, the tree's status decides the outcome: uncommitted or\n\t * untracked content means the tree is kept (\"dirty\") so child work is\n\t * never destroyed silently; a clean tree that still cannot be removed is\n\t * reported as \"failed\". `--force` runs only when the caller passes\n\t * `options.force` -- the explicit escape hatch, never an automatic\n\t * fallback. Already-removed worktrees are tolerated and never throw.\n\t */\n\tasync release(sessionId: string, options?: { force?: boolean }): Promise<WorkspaceReleaseOutcome> {\n\t\tif (!isSafeSessionId(sessionId)) return { removed: true }; // an unsafe id can never have been prepared\n\t\tconst dir = this.worktreeDir(sessionId);\n\t\tif (!existsSync(dir)) {\n\t\t\t// Never created, already removed, or removed out of band: drop any\n\t\t\t// stale administrative entry and succeed either way.\n\t\t\tawait runGit(this.workspaceRoot, [\"worktree\", \"prune\"], this.timeoutMs);\n\t\t\treturn { removed: true };\n\t\t}\n\t\tif (options?.force) {\n\t\t\tconst forced = await runGit(this.workspaceRoot, [\"worktree\", \"remove\", \"--force\", dir], this.timeoutMs);\n\t\t\tif (forced.ok) return { removed: true };\n\t\t\treturn { removed: false, kept: \"failed\", dir, error: describeFailure(forced) };\n\t\t}\n\t\tconst removed = await runGit(this.workspaceRoot, [\"worktree\", \"remove\", dir], this.timeoutMs);\n\t\tif (removed.ok) return { removed: true };\n\t\t// The plain remove refused. Inspect the tree: uncommitted/untracked\n\t\t// content means the refusal is protective and the tree stays.\n\t\tconst status = await runGit(dir, [\"status\", \"--porcelain\"], this.timeoutMs);\n\t\tif (status.ok && status.stdout.toString(\"utf-8\").trim() !== \"\") {\n\t\t\treturn { removed: false, kept: \"dirty\", dir, error: describeFailure(removed) };\n\t\t}\n\t\treturn { removed: false, kept: \"failed\", dir, error: describeFailure(removed) };\n\t}\n\n\t/**\n\t * Read-only verification for explicit workspace recovery (spec\n\t * 2026-09-09-run-and-child-session-architecture.md, \"Workspace states and\n\t * explicit recovery\"): confirm `root` is still THIS owner's worktree for\n\t * `sessionId` in the CURRENT workspace. Three checks, each refusal naming\n\t * the check: (layout) the recorded path is exactly the owner's\n\t * `<workspaceRoot>/.apex-code/worktrees/<sessionId>` and exists;\n\t * (admin entry) `<root>/.git` is a linked-worktree pointer file whose gitdir\n\t * resolves into the current repository's `.git/worktrees`; (branch) the\n\t * checked-out branch is `apex-child-<short-sessionId>`. Never creates,\n\t * checks out, resets, or force-removes anything -- git inspection is\n\t * read-only (`rev-parse`, `status`) plus the administrative entry read.\n\t * Returns the worktree's dirty state so callers can report preserved\n\t * uncommitted work without a second inspection.\n\t */\n\tasync verify(sessionId: string, root: string): Promise<{ dirty: boolean }> {\n\t\tif (!isSafeSessionId(sessionId)) {\n\t\t\tthrow new Error(`layout check failed: session id \"${sessionId}\" is not a safe path component.`);\n\t\t}\n\t\t// (c) The directory sits at this owner's layout for the session.\n\t\tconst expected = this.worktreeDir(sessionId);\n\t\tif (resolve(root) !== expected) {\n\t\t\tthrow new Error(\n\t\t\t\t`layout check failed: recorded root \"${root}\" is not this workspace's worktree location for session \"${sessionId}\" (expected \"${expected}\").`,\n\t\t\t);\n\t\t}\n\t\tif (!existsSync(root)) {\n\t\t\tthrow new Error(`layout check failed: the recorded worktree root \"${root}\" does not exist.`);\n\t\t}\n\t\t// (a) The directory is a linked worktree of the CURRENT parent workspace:\n\t\t// git resolves its administrative entry, and the resolved gitdir must sit\n\t\t// inside this repository's worktrees dir. Git reads the pointer itself,\n\t\t// which direct file reads cannot do portably (Windows denies the open).\n\t\tconst resolved = await runGit(root, [\"rev-parse\", \"--absolute-git-dir\"], this.timeoutMs);\n\t\tif (!resolved.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: \"${root}\" is not a linked worktree (git could not resolve its administrative entry: ${describeFailure(resolved)}).`,\n\t\t\t);\n\t\t}\n\t\tconst gitdir = resolve(resolved.stdout.toString(\"utf-8\").trim());\n\t\tconst parentGitDir = await this.currentGitDir();\n\t\tconst worktreesDir = join(parentGitDir, \"worktrees\");\n\t\tconst rel = relative(worktreesDir, gitdir);\n\t\tif (rel === \"\" || isAbsolute(rel) || rel.startsWith(\"..\") || !existsSync(gitdir)) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: \"${root}\" is not a linked worktree of the current repository (its gitdir \"${gitdir}\" does not resolve into \"${worktreesDir}\").`,\n\t\t\t);\n\t\t}\n\t\t// (b) The checked-out branch is this child's own.\n\t\tconst branch = `${BRANCH_PREFIX}${shortSessionId(sessionId)}`;\n\t\tconst head = await runGit(root, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], this.timeoutMs);\n\t\tconst checkedOut = head.ok ? head.stdout.toString(\"utf-8\").trim() : \"\";\n\t\tif (!head.ok || checkedOut !== branch) {\n\t\t\tthrow new Error(\n\t\t\t\t`branch check failed: the worktree for session \"${sessionId}\" has ${\n\t\t\t\t\thead.ok ? `\"${checkedOut}\" checked out` : describeFailure(head)\n\t\t\t\t}, expected \"${branch}\".`,\n\t\t\t);\n\t\t}\n\t\t// The dirty report is informational, not a gate: uncommitted child work is\n\t\t// exactly what recovery must preserve, never destroy.\n\t\tconst status = await runGit(root, [\"status\", \"--porcelain\"], this.timeoutMs);\n\t\tif (!status.ok) {\n\t\t\tthrow new Error(`status check failed: the worktree's status could not be read: ${describeFailure(status)}.`);\n\t\t}\n\t\treturn { dirty: status.stdout.toString(\"utf-8\").trim() !== \"\" };\n\t}\n\n\t/** The current workspace repository's absolute git dir, for admin-entry verification. */\n\tprivate async currentGitDir(): Promise<string> {\n\t\tconst probe = await runGit(this.workspaceRoot, [\"rev-parse\", \"--absolute-git-dir\"], this.timeoutMs);\n\t\tif (!probe.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`admin entry check failed: the current workspace is not a readable git repository (${describeFailure(probe)}).`,\n\t\t\t);\n\t\t}\n\t\treturn resolve(this.workspaceRoot, probe.stdout.toString(\"utf-8\").trim());\n\t}\n\n\t/** Verify git is usable and the workspace sits inside a repository; returns the repository toplevel. */\n\tprivate async requireRepository(sessionId: string): Promise<string> {\n\t\tconst probe = await runGit(this.workspaceRoot, [\"rev-parse\", \"--show-toplevel\"], this.timeoutMs);\n\t\tif (!probe.ok) {\n\t\t\tthrow new Error(\n\t\t\t\t`Worktree isolation refused for session \"${sessionId}\": ${describeFailure(probe)}; worktree isolation requires a git repository.`,\n\t\t\t);\n\t\t}\n\t\treturn probe.stdout.toString(\"utf-8\").trim();\n\t}\n\n\t/**\n\t * Keep `<...>/.apex-code/worktrees/` out of the parent checkout's status\n\t * via `.git/info/exclude` (idempotent; forward-slash pattern as git\n\t * requires, repo-relative so it also holds when the workspace root is a\n\t * subdirectory of the repository).\n\t */\n\tprivate async keepWorktreesUntracked(toplevel: string): Promise<void> {\n\t\tconst dir = join(this.workspaceRoot, WORKTREES_SEGMENT);\n\t\tconst repoRelative = relative(toplevel, dir);\n\t\tif (repoRelative === \"\" || isAbsolute(repoRelative) || repoRelative.startsWith(\"..\")) return;\n\t\tconst entry = `${repoRelative.split(sep).join(\"/\")}/`;\n\t\ttry {\n\t\t\tconst ignored = await runGit(toplevel, [\"check-ignore\", \"-q\", entry], this.timeoutMs);\n\t\t\tif (ignored.ok) return;\n\t\t\tconst excludePathResult = await runGit(toplevel, [\"rev-parse\", \"--git-path\", \"info/exclude\"], this.timeoutMs);\n\t\t\tif (!excludePathResult.ok) return;\n\t\t\tconst excludePath = resolve(toplevel, excludePathResult.stdout.toString(\"utf-8\").trim());\n\t\t\tmkdirSync(dirname(excludePath), { recursive: true });\n\t\t\tconst current = existsSync(excludePath) ? readFileSync(excludePath, \"utf-8\") : \"\";\n\t\t\tif (current.split(/\\r?\\n/).includes(entry)) return;\n\t\t\tconst needsNewLine = current.length > 0 && !current.endsWith(\"\\n\");\n\t\t\twriteFileSync(excludePath, `${needsNewLine ? \"\\n\" : \"\"}${entry}\\n`, { flag: \"a\", encoding: \"utf-8\" });\n\t\t} catch {\n\t\t\t// Hygiene only: an unwritable or unreadable exclude never fails the worktree.\n\t\t}\n\t}\n}\n"]}