@qodeca/xezar 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +72 -39
  2. package/dist/agent-config/account-identity.d.ts +2 -8
  3. package/dist/agent-config/account-identity.js +54 -10
  4. package/dist/agent-config/account-identity.js.map +1 -1
  5. package/dist/agent-config/catalog.d.ts +49 -7
  6. package/dist/agent-config/catalog.js +107 -0
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/model-settings/pi.d.ts +12 -6
  9. package/dist/agent-config/model-settings/pi.js +12 -6
  10. package/dist/agent-config/model-settings/pi.js.map +1 -1
  11. package/dist/agent-config/seed.js +10 -0
  12. package/dist/agent-config/seed.js.map +1 -1
  13. package/dist/contract/github.d.ts +15 -0
  14. package/dist/contract/index.d.ts +9 -0
  15. package/dist/contract/index.js +1527 -895
  16. package/dist/contract/mcp-api-reference.d.ts +174 -0
  17. package/dist/contract/mcp-audit.d.ts +103 -0
  18. package/dist/contract/mcp-discovery.d.ts +257 -0
  19. package/dist/contract/mcp-event-catalog.d.ts +121 -0
  20. package/dist/contract/mcp-event-catalog.test.d.ts +1 -0
  21. package/dist/contract/mcp-idempotency.d.ts +200 -0
  22. package/dist/contract/mcp-journal.d.ts +268 -0
  23. package/dist/contract/mcp-leader.d.ts +88 -0
  24. package/dist/contract/mcp-ownership.d.ts +78 -0
  25. package/dist/contract/mcp-versioning.d.ts +139 -0
  26. package/dist/contract/runs.d.ts +81 -0
  27. package/dist/core/agent-profiles.d.ts +15 -4
  28. package/dist/core/agent-profiles.js +22 -8
  29. package/dist/core/agent-profiles.js.map +1 -1
  30. package/dist/core/opencode-server-runner.js +71 -11
  31. package/dist/core/opencode-server-runner.js.map +1 -1
  32. package/dist/core/secret-redaction.d.ts +7 -1
  33. package/dist/core/secret-redaction.js +44 -10
  34. package/dist/core/secret-redaction.js.map +1 -1
  35. package/dist/index.js +138 -27
  36. package/dist/index.js.map +1 -1
  37. package/dist/machine-identity.d.ts +66 -0
  38. package/dist/machine-identity.js +96 -0
  39. package/dist/machine-identity.js.map +1 -0
  40. package/dist/mcp/adapters/claude-code.d.ts +41 -0
  41. package/dist/mcp/adapters/claude-code.js +40 -0
  42. package/dist/mcp/adapters/claude-code.js.map +1 -0
  43. package/dist/mcp/adapters/codex.d.ts +140 -0
  44. package/dist/mcp/adapters/codex.js +323 -0
  45. package/dist/mcp/adapters/codex.js.map +1 -0
  46. package/dist/mcp/adapters/opencode.d.ts +148 -0
  47. package/dist/mcp/adapters/opencode.js +528 -0
  48. package/dist/mcp/adapters/opencode.js.map +1 -0
  49. package/dist/mcp/adapters/pi-link.d.ts +102 -0
  50. package/dist/mcp/adapters/pi-link.js +235 -0
  51. package/dist/mcp/adapters/pi-link.js.map +1 -0
  52. package/dist/mcp/adapters/pi.d.ts +188 -0
  53. package/dist/mcp/adapters/pi.js +418 -0
  54. package/dist/mcp/adapters/pi.js.map +1 -0
  55. package/dist/mcp/api-reference.d.ts +26 -0
  56. package/dist/mcp/api-reference.js +133 -0
  57. package/dist/mcp/api-reference.js.map +1 -0
  58. package/dist/mcp/audit-trail.d.ts +162 -0
  59. package/dist/mcp/audit-trail.js +306 -0
  60. package/dist/mcp/audit-trail.js.map +1 -0
  61. package/dist/mcp/bridge.d.ts +81 -0
  62. package/dist/mcp/bridge.js +407 -0
  63. package/dist/mcp/bridge.js.map +1 -0
  64. package/dist/mcp/connection-file.d.ts +49 -0
  65. package/dist/mcp/connection-file.js +63 -0
  66. package/dist/mcp/connection-file.js.map +1 -0
  67. package/dist/mcp/echo-guard.d.ts +94 -0
  68. package/dist/mcp/echo-guard.js +160 -0
  69. package/dist/mcp/echo-guard.js.map +1 -0
  70. package/dist/mcp/event-catalog.d.ts +120 -0
  71. package/dist/mcp/event-catalog.js +306 -0
  72. package/dist/mcp/event-catalog.js.map +1 -0
  73. package/dist/mcp/event-controller.d.ts +230 -0
  74. package/dist/mcp/event-controller.js +596 -0
  75. package/dist/mcp/event-controller.js.map +1 -0
  76. package/dist/mcp/event-journal.d.ts +57 -0
  77. package/dist/mcp/event-journal.js +423 -0
  78. package/dist/mcp/event-journal.js.map +1 -0
  79. package/dist/mcp/index.d.ts +63 -0
  80. package/dist/mcp/index.js +386 -0
  81. package/dist/mcp/index.js.map +1 -0
  82. package/dist/mcp/ipc.d.ts +202 -0
  83. package/dist/mcp/ipc.js +206 -0
  84. package/dist/mcp/ipc.js.map +1 -0
  85. package/dist/mcp/leader-delivery.d.ts +119 -0
  86. package/dist/mcp/leader-delivery.js +450 -0
  87. package/dist/mcp/leader-delivery.js.map +1 -0
  88. package/dist/mcp/operation-receipts.d.ts +210 -0
  89. package/dist/mcp/operation-receipts.js +723 -0
  90. package/dist/mcp/operation-receipts.js.map +1 -0
  91. package/dist/mcp/project-catalogs.d.ts +23 -0
  92. package/dist/mcp/project-catalogs.js +25 -0
  93. package/dist/mcp/project-catalogs.js.map +1 -0
  94. package/dist/mcp/project-leaders.d.ts +26 -0
  95. package/dist/mcp/project-leaders.js +17 -0
  96. package/dist/mcp/project-leaders.js.map +1 -0
  97. package/dist/mcp/protocol.d.ts +50 -0
  98. package/dist/mcp/protocol.js +47 -0
  99. package/dist/mcp/protocol.js.map +1 -0
  100. package/dist/mcp/reconnect.d.ts +297 -0
  101. package/dist/mcp/reconnect.js +465 -0
  102. package/dist/mcp/reconnect.js.map +1 -0
  103. package/dist/mcp/resource-ownership.d.ts +228 -0
  104. package/dist/mcp/resource-ownership.js +390 -0
  105. package/dist/mcp/resource-ownership.js.map +1 -0
  106. package/dist/mcp/service-adapter.d.ts +16488 -0
  107. package/dist/mcp/service-adapter.js +148 -0
  108. package/dist/mcp/service-adapter.js.map +1 -0
  109. package/dist/mcp/service.d.ts +83 -0
  110. package/dist/mcp/service.js +233 -0
  111. package/dist/mcp/service.js.map +1 -0
  112. package/dist/mcp/session-binding.d.ts +105 -0
  113. package/dist/mcp/session-binding.js +163 -0
  114. package/dist/mcp/session-binding.js.map +1 -0
  115. package/dist/mcp/stale-write.d.ts +145 -0
  116. package/dist/mcp/stale-write.js +219 -0
  117. package/dist/mcp/stale-write.js.map +1 -0
  118. package/dist/mcp/tool.d.ts +48 -0
  119. package/dist/mcp/tool.js +24 -0
  120. package/dist/mcp/tool.js.map +1 -0
  121. package/dist/mcp/tools/discovery.d.ts +40 -0
  122. package/dist/mcp/tools/discovery.js +232 -0
  123. package/dist/mcp/tools/discovery.js.map +1 -0
  124. package/dist/mcp/tools/execution-control.d.ts +171 -0
  125. package/dist/mcp/tools/execution-control.js +513 -0
  126. package/dist/mcp/tools/execution-control.js.map +1 -0
  127. package/dist/mcp/tools/handoff-git.d.ts +26 -0
  128. package/dist/mcp/tools/handoff-git.js +544 -0
  129. package/dist/mcp/tools/handoff-git.js.map +1 -0
  130. package/dist/mcp/tools/index.d.ts +2 -0
  131. package/dist/mcp/tools/index.js +23 -0
  132. package/dist/mcp/tools/index.js.map +1 -0
  133. package/dist/mcp/tools/leader-events.d.ts +46 -0
  134. package/dist/mcp/tools/leader-events.js +126 -0
  135. package/dist/mcp/tools/leader-events.js.map +1 -0
  136. package/dist/mcp/tools/local-handoff.d.ts +102 -0
  137. package/dist/mcp/tools/local-handoff.js +306 -0
  138. package/dist/mcp/tools/local-handoff.js.map +1 -0
  139. package/dist/mcp/tools/project-config.d.ts +340 -0
  140. package/dist/mcp/tools/project-config.js +1075 -0
  141. package/dist/mcp/tools/project-config.js.map +1 -0
  142. package/dist/mcp/tools/results-evidence.d.ts +112 -0
  143. package/dist/mcp/tools/results-evidence.js +1045 -0
  144. package/dist/mcp/tools/results-evidence.js.map +1 -0
  145. package/dist/mcp/tools/task-create.d.ts +70 -0
  146. package/dist/mcp/tools/task-create.js +473 -0
  147. package/dist/mcp/tools/task-create.js.map +1 -0
  148. package/dist/mcp/tools/task-reads.d.ts +102 -0
  149. package/dist/mcp/tools/task-reads.js +689 -0
  150. package/dist/mcp/tools/task-reads.js.map +1 -0
  151. package/dist/mcp/tools/work-organisation.d.ts +2 -0
  152. package/dist/mcp/tools/work-organisation.js +502 -0
  153. package/dist/mcp/tools/work-organisation.js.map +1 -0
  154. package/dist/paths.d.ts +17 -7
  155. package/dist/paths.js +18 -8
  156. package/dist/paths.js.map +1 -1
  157. package/dist/runs/project-writer.js +76 -11
  158. package/dist/runs/project-writer.js.map +1 -1
  159. package/dist/runs/retention.d.ts +25 -2
  160. package/dist/runs/retention.js +63 -2
  161. package/dist/runs/retention.js.map +1 -1
  162. package/dist/server/forge/github.js +44 -0
  163. package/dist/server/forge/github.js.map +1 -1
  164. package/dist/server/forge/types.d.ts +16 -0
  165. package/dist/server/project-context.d.ts +81 -8
  166. package/dist/server/project-context.js +143 -35
  167. package/dist/server/project-context.js.map +1 -1
  168. package/dist/server/server.d.ts +1050 -53
  169. package/dist/server/server.js +326 -120
  170. package/dist/server/server.js.map +1 -1
  171. package/dist/server/validators.d.ts +26 -6
  172. package/dist/server/validators.js +17 -0
  173. package/dist/server/validators.js.map +1 -1
  174. package/dist/skills-remote.js +64 -6
  175. package/dist/skills-remote.js.map +1 -1
  176. package/dist/workflows/run.d.ts +277 -12
  177. package/dist/workflows/run.js +599 -79
  178. package/dist/workflows/run.js.map +1 -1
  179. package/dist/workspace/agent-profiles.d.ts +30 -3
  180. package/dist/workspace/agent-profiles.js +39 -4
  181. package/dist/workspace/agent-profiles.js.map +1 -1
  182. package/dist/workspace/config.js +25 -4
  183. package/dist/workspace/config.js.map +1 -1
  184. package/dist/workspace/project-owner.d.ts +210 -0
  185. package/dist/workspace/project-owner.js +518 -0
  186. package/dist/workspace/project-owner.js.map +1 -0
  187. package/package.json +5 -3
  188. package/scripts/pi-leader-extension.ts +420 -0
  189. package/scripts/sync-readme.mjs +83 -2
  190. package/web/dist/assets/{alert-dialog-BVsVcbFy.js → alert-dialog-D6rP6d39.js} +1 -1
  191. package/web/dist/assets/arrow-down-DMOfFIzW.js +1 -0
  192. package/web/dist/assets/arrow-left-C4Ec2BUg.js +1 -0
  193. package/web/dist/assets/{centered-state-Djgly6Ni.js → centered-state-B0ZqrPaa.js} +12 -12
  194. package/web/dist/assets/chevron-right-mHJ-crvn.js +1 -0
  195. package/web/dist/assets/{collapsible-DZ4D-DHG.js → collapsible-DchEHgUM.js} +1 -1
  196. package/web/dist/assets/{commit-list-B-z1JL9G.js → commit-list-BpMim1Rv.js} +1 -1
  197. package/web/dist/assets/{compare-variants-CdS8r8E2.js → compare-variants-D5ItQbAq.js} +1 -1
  198. package/web/dist/assets/{diff-BKew9rQX.js → diff-HqqZ_WDR.js} +2 -2
  199. package/web/dist/assets/{diff-stat-D0mc5V6Z.js → diff-stat-NsU-3ecr.js} +1 -1
  200. package/web/dist/assets/{diff-view-xpIcuLIC.js → diff-view-B7c11Xux.js} +1 -1
  201. package/web/dist/assets/{dropdown-menu-CEQziH7X.js → dropdown-menu-CtTpnyod.js} +1 -1
  202. package/web/dist/assets/{editable-title-BCX3T2BY.js → editable-title-BXtuGQry.js} +1 -1
  203. package/web/dist/assets/{ellipsis-vertical-ByVdDc4z.js → ellipsis-vertical-MHtGZSH6.js} +1 -1
  204. package/web/dist/assets/{file-xBSB5hXO.js → file-DKLw5SOb.js} +1 -1
  205. package/web/dist/assets/{folder-CldgbZ5A.js → folder-Drw6-ufH.js} +1 -1
  206. package/web/dist/assets/{git-toolbar-BYlnf4qf.js → git-toolbar-BQH8bORV.js} +1 -1
  207. package/web/dist/assets/github-ynnpKFCJ.js +1 -0
  208. package/web/dist/assets/{image-preview-Cv5mZr0n.js → image-preview-DLhMN_R9.js} +1 -1
  209. package/web/dist/assets/index-BEsrNdXt.js +29 -0
  210. package/web/dist/assets/index-Da8DOBau.css +2 -0
  211. package/web/dist/assets/{markdown-CgEEE2ND.js → markdown-q365eN6Y.js} +1 -1
  212. package/web/dist/assets/{new-task-form-1r2bgsdD.js → new-task-form-CW-hNIWQ.js} +1 -1
  213. package/web/dist/assets/{pill-q-x22_ej.js → pill-B_TFulXa.js} +1 -1
  214. package/web/dist/assets/{project-router-VDN4jQM8.js → project-router-CLPc-ImZ.js} +1 -1
  215. package/web/dist/assets/{prompt-templates-BmZtCe-L.js → prompt-templates-CLc4RxyO.js} +1 -1
  216. package/web/dist/assets/{repo-git-C2On8SlI.js → repo-git-Is7R6-Tj.js} +1 -1
  217. package/web/dist/assets/{run-diff-vguEMJzh.js → run-diff-DHMvDlVo.js} +2 -2
  218. package/web/dist/assets/run-header-DvZJbQ8I.js +1 -0
  219. package/web/dist/assets/{search-x-QmhaAbg5.js → search-x-Ce2MMetu.js} +1 -1
  220. package/web/dist/assets/{skill-empty-hint-BrzUwByZ.js → skill-empty-hint-C_xCSvzB.js} +1 -1
  221. package/web/dist/assets/skills-CivqD4si.js +1 -0
  222. package/web/dist/assets/{sparkles-CP8QDOgX.js → sparkles-BCqPoj5g.js} +1 -1
  223. package/web/dist/assets/{square-terminal-CGypshSD.js → square-terminal-ikj5g-oK.js} +1 -1
  224. package/web/dist/assets/{tab-link-D4lFzzI1.js → tab-link-VsEjFV1Y.js} +1 -1
  225. package/web/dist/assets/{task-changes-6aopqAmV.js → task-changes-CH-E81-_.js} +1 -1
  226. package/web/dist/assets/{task-commits-u-dwaCyw.js → task-commits-DqfkMFxp.js} +1 -1
  227. package/web/dist/assets/{task-files-IMOZnIUK.js → task-files-D_nkHZ_a.js} +2 -2
  228. package/web/dist/assets/task-thread-bxQompxJ.js +9 -0
  229. package/web/dist/assets/{textarea-CyBCyKIk.js → textarea-xajuW1L_.js} +1 -1
  230. package/web/dist/assets/{thread-loading-Cuq2jchC.js → thread-loading-C3JQZbIE.js} +1 -1
  231. package/web/dist/assets/{trash-2-B6Dp3ZNs.js → trash-2-hJrBhpUD.js} +1 -1
  232. package/web/dist/assets/{triangle-alert-xBya8FUp.js → triangle-alert-D1u82y1a.js} +1 -1
  233. package/web/dist/assets/{upload-D6MXEB9p.js → upload-Bn6FBZ59.js} +1 -1
  234. package/web/dist/assets/{use-desktop-fLDKEJoB.js → use-desktop-DOIMfHfY.js} +1 -1
  235. package/web/dist/assets/{use-submit-shortcut-JTpNzU-u.js → use-submit-shortcut-Dj4DHTdE.js} +1 -1
  236. package/web/dist/assets/utils-YwBpOoRN.js +64 -0
  237. package/web/dist/assets/{workflows-BY3KTkFw.js → workflows-CTaAKz2m.js} +3 -3
  238. package/web/dist/assets/{zoomable-image-GTYwLtkS.js → zoomable-image-C1-6P3tg.js} +1 -1
  239. package/web/dist/index.html +23 -23
  240. package/web/dist/assets/arrow-down-CYgAlZtW.js +0 -1
  241. package/web/dist/assets/arrow-left-BOqE0a3k.js +0 -1
  242. package/web/dist/assets/chevron-right-Cc9BWnbN.js +0 -1
  243. package/web/dist/assets/github-CqGK62Lw.js +0 -1
  244. package/web/dist/assets/index-3nxVmdn5.css +0 -2
  245. package/web/dist/assets/index-qX7n11FX.js +0 -9
  246. package/web/dist/assets/refresh-cw-DcNCj2nu.js +0 -1
  247. package/web/dist/assets/run-header-DM7MiKj9.js +0 -1
  248. package/web/dist/assets/skills-Bj-uG_Hz.js +0 -1
  249. package/web/dist/assets/task-thread-CbMPmrWq.js +0 -9
  250. package/web/dist/assets/utils--z9hRtl3.js +0 -64
package/dist/paths.d.ts CHANGED
@@ -102,8 +102,8 @@ export declare function serverLockPath(instance?: string): string;
102
102
  * Where each coding agent keeps its per-user config, honouring the env vars the
103
103
  * vendors document: `$CLAUDE_CONFIG_DIR` relocates Claude Code's home;
104
104
  * `$CODEX_HOME` relocates Codex's; `$OPENCODE_CONFIG_DIR`, then `$XDG_CONFIG_HOME`,
105
- * relocate OpenCode's config dir (falling back to `~/.config`). Read per call so
106
- * tests and ops can set env live.
105
+ * relocate OpenCode's config dir (falling back to `~/.config`); `$PI_CODING_AGENT_DIR`
106
+ * relocates pi's. Read per call so tests and ops can set env live.
107
107
  *
108
108
  * OpenCode's own variable is checked FIRST because it is the narrow one. Reaching
109
109
  * for `XDG_CONFIG_HOME` to move one agent's config relocates every XDG-aware tool
@@ -115,11 +115,21 @@ export declare function serverLockPath(instance?: string): string;
115
115
  * and does not reach here — this slot IS the config dir, nothing else, so the
116
116
  * config-only variable is exactly the right precision for it.
117
117
  *
118
- * pi is the one slot with NO vendor variable: it documents none, and its binary
119
- * reads none (checked against pi 0.85.1, 2026-09-10) `PI_PACKAGE_DIR` names the
120
- * npm package, not the agent home. `src/core/agent-profiles.ts` records the same
121
- * fact for profiles. So `$HOME`/`$USERPROFILE` is the whole of pi's relocation,
122
- * and deriving it here rather than from `homedir()` at the call site is what keeps
118
+ * pi relocates through `$PI_CODING_AGENT_DIR` (#329, re-checked against pi 0.85.1
119
+ * on 2026-09-12). This repo said the opposite in five places on the strength of one
120
+ * check on 2026-09-10, so the correction is spelled out: `pi --help` lists
121
+ * `PI_CODING_AGENT_DIR - Config directory (default: ~/.pi/agent)` under "Environment
122
+ * Variables", the shipped package documents it in `docs/environment-variables.md`,
123
+ * and the binary was observed reading AND writing there — a corrupt `settings.json`
124
+ * inside the pinned dir produced `Warning: Invalid settings file <pinned>/settings.json`,
125
+ * and the run wrote `auth.json`, `models-store.json` and `sessions/` into it while
126
+ * reporting "No API key found" with a populated `~/.pi/agent/auth.json` on disk. So
127
+ * identity moves with the dir, which is what `src/core/agent-profiles.ts` needs.
128
+ * `PI_PACKAGE_DIR` remains unrelated — it names the npm package, not the agent home.
129
+ * `PI_CODING_AGENT_SESSION_DIR` is deliberately NOT read here: it moves session
130
+ * storage only, so honouring it would answer a dir that holds no credentials.
131
+ *
132
+ * Deriving pi's home here rather than from `homedir()` at the call site is what keeps
123
133
  * a test or a container off a real home — the reason this helper exists at all.
124
134
  *
125
135
  * These are the DEFAULT profile's dirs. A second login of the same CLI is an
package/dist/paths.js CHANGED
@@ -141,8 +141,8 @@ export function serverLockPath(instance = DEFAULT_SERVER_INSTANCE) {
141
141
  * Where each coding agent keeps its per-user config, honouring the env vars the
142
142
  * vendors document: `$CLAUDE_CONFIG_DIR` relocates Claude Code's home;
143
143
  * `$CODEX_HOME` relocates Codex's; `$OPENCODE_CONFIG_DIR`, then `$XDG_CONFIG_HOME`,
144
- * relocate OpenCode's config dir (falling back to `~/.config`). Read per call so
145
- * tests and ops can set env live.
144
+ * relocate OpenCode's config dir (falling back to `~/.config`); `$PI_CODING_AGENT_DIR`
145
+ * relocates pi's. Read per call so tests and ops can set env live.
146
146
  *
147
147
  * OpenCode's own variable is checked FIRST because it is the narrow one. Reaching
148
148
  * for `XDG_CONFIG_HOME` to move one agent's config relocates every XDG-aware tool
@@ -154,11 +154,21 @@ export function serverLockPath(instance = DEFAULT_SERVER_INSTANCE) {
154
154
  * and does not reach here — this slot IS the config dir, nothing else, so the
155
155
  * config-only variable is exactly the right precision for it.
156
156
  *
157
- * pi is the one slot with NO vendor variable: it documents none, and its binary
158
- * reads none (checked against pi 0.85.1, 2026-09-10) `PI_PACKAGE_DIR` names the
159
- * npm package, not the agent home. `src/core/agent-profiles.ts` records the same
160
- * fact for profiles. So `$HOME`/`$USERPROFILE` is the whole of pi's relocation,
161
- * and deriving it here rather than from `homedir()` at the call site is what keeps
157
+ * pi relocates through `$PI_CODING_AGENT_DIR` (#329, re-checked against pi 0.85.1
158
+ * on 2026-09-12). This repo said the opposite in five places on the strength of one
159
+ * check on 2026-09-10, so the correction is spelled out: `pi --help` lists
160
+ * `PI_CODING_AGENT_DIR - Config directory (default: ~/.pi/agent)` under "Environment
161
+ * Variables", the shipped package documents it in `docs/environment-variables.md`,
162
+ * and the binary was observed reading AND writing there — a corrupt `settings.json`
163
+ * inside the pinned dir produced `Warning: Invalid settings file <pinned>/settings.json`,
164
+ * and the run wrote `auth.json`, `models-store.json` and `sessions/` into it while
165
+ * reporting "No API key found" with a populated `~/.pi/agent/auth.json` on disk. So
166
+ * identity moves with the dir, which is what `src/core/agent-profiles.ts` needs.
167
+ * `PI_PACKAGE_DIR` remains unrelated — it names the npm package, not the agent home.
168
+ * `PI_CODING_AGENT_SESSION_DIR` is deliberately NOT read here: it moves session
169
+ * storage only, so honouring it would answer a dir that holds no credentials.
170
+ *
171
+ * Deriving pi's home here rather than from `homedir()` at the call site is what keeps
162
172
  * a test or a container off a real home — the reason this helper exists at all.
163
173
  *
164
174
  * These are the DEFAULT profile's dirs. A second login of the same CLI is an
@@ -173,7 +183,7 @@ export function agentHomePaths(env = process.env) {
173
183
  claude: env.CLAUDE_CONFIG_DIR?.trim() || join(home, '.claude'),
174
184
  codex: env.CODEX_HOME?.trim() || join(home, '.codex'),
175
185
  opencodeConfig: env.OPENCODE_CONFIG_DIR?.trim() || join(xdgConfig, 'opencode'),
176
- pi: join(home, '.pi', 'agent'),
186
+ pi: env.PI_CODING_AGENT_DIR?.trim() || join(home, '.pi', 'agent'),
177
187
  };
178
188
  }
179
189
  /**
package/dist/paths.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { closeSync, openSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { hostname } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { localMachineId, machineRelation } from '../machine-identity.js';
5
6
  const owned = new Map();
6
7
  export class ProjectWriterError extends Error {
7
8
  dataDir;
@@ -11,6 +12,56 @@ export class ProjectWriterError extends Error {
11
12
  this.name = 'ProjectWriterError';
12
13
  }
13
14
  }
15
+ /** The claim's exact bytes, for PUBLISHING only — the self-check (`claimIsOurs`) compares
16
+ * fields, not bytes, because the `host` recorded here is display-only and this helper reads
17
+ * `hostname()` live. `machine` is omitted (not written as null) when this host cannot identify
18
+ * itself, so an unidentifiable host keeps writing the pre-#199 shape byte for byte. */
19
+ function claimBody(machine) {
20
+ return JSON.stringify({ pid: process.pid, host: hostname(), ...(machine === null ? {} : { machine }) });
21
+ }
22
+ /**
23
+ * Is the claim this process published still the one on disk?
24
+ *
25
+ * It compares the two facts that identify a writer — the PID and the MACHINE — and never the
26
+ * hostname, for the same reason the peer scan stopped trusting it: `claimBody` reads
27
+ * `hostname()` live, so a byte-for-byte comparison against a freshly built body turns a machine
28
+ * renaming itself into "the active claim changed". That is #199's lockout reproduced INSIDE one
29
+ * process, and it is reachable long after boot — `automations/coordinator.ts` calls
30
+ * `ownProjectData` lazily, so a laptop that changes network while `xez serve` is up hits it.
31
+ * `localMachineId()` is memoized, so the hostname is the only field that could ever have moved.
32
+ *
33
+ * Fails closed on anything it cannot read or parse: an unreadable, truncated or rewritten claim
34
+ * is not this process's claim. `unknown` (this host will not name itself, or the claim predates
35
+ * the field) passes, exactly as the peer scan treats it — there is no evidence of a different
36
+ * machine, and the file's path already carries this process's PID and a UUID only it generated.
37
+ */
38
+ function claimIsOurs(path, machine) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ if (!parsed || typeof parsed !== 'object')
47
+ return false;
48
+ const claim = parsed;
49
+ return claim.pid === process.pid && machineRelation(claim.machine, machine) !== 'different';
50
+ }
51
+ /** Long hostnames and corrupt claims must not turn an error message into a wall of text. */
52
+ function short(value) {
53
+ return JSON.stringify(String(value).slice(0, 120));
54
+ }
55
+ /** Name the file, both hostnames and the PID, because the only repair is deleting one file and
56
+ * "foreign-host writer claim" pointed at nothing (#199). Says WHY the machines were judged
57
+ * different, so "a different machine" and "no identity to compare" never read the same. */
58
+ function foreignClaimDetail(path, recordedHost, pid, relation) {
59
+ const evidence = relation === 'different'
60
+ ? 'It also records a different machine identity.'
61
+ : 'It records no machine identity to compare, so the hostname was the only evidence.';
62
+ return `foreign-host writer claim: ${path} names host ${short(recordedHost)} (PID ${pid}); this host is ${short(hostname())}.`
63
+ + ` ${evidence} If that machine is no longer using this directory, delete that claim file.`;
64
+ }
14
65
  /** One process owns a project's mutable state, independent of port and XEZ_HOME.
15
66
  * Publish a unique claim BEFORE scanning. Two simultaneous contenders may both refuse,
16
67
  * but cannot both see themselves alone. Dead claims have unique paths, so reclaiming one
@@ -20,19 +71,16 @@ export class ProjectWriterError extends Error {
20
71
  export function ownProjectData(dataDir) {
21
72
  mkdirSync(dataDir, { recursive: true });
22
73
  const canonical = realpathSync(dataDir);
74
+ const machine = localMachineId();
23
75
  const prior = owned.get(canonical);
24
76
  if (prior) {
25
77
  // Deleting active runtime state is not a supported way to release ownership.
26
- let intact = false;
27
- try {
28
- intact = readFileSync(prior, 'utf8') === JSON.stringify({ pid: process.pid, host: hostname() });
29
- }
30
- catch { /* fail closed */ }
31
- if (!intact) {
78
+ if (!claimIsOurs(prior, machine)) {
32
79
  throw new ProjectWriterError(canonical, 'the active claim changed');
33
80
  }
34
81
  return;
35
82
  }
83
+ const body = claimBody(machine);
36
84
  const directory = join(canonical, 'writer-claims');
37
85
  mkdirSync(directory, { recursive: true });
38
86
  const claim = join(directory, `${process.pid}-${randomUUID()}.json`);
@@ -41,7 +89,7 @@ export function ownProjectData(dataDir) {
41
89
  const descriptor = openSync(claim, 'wx', 0o600);
42
90
  published = true; // Exclusive open established ownership even if the following write fails.
43
91
  try {
44
- writeFileSync(descriptor, JSON.stringify({ pid: process.pid, host: hostname() }));
92
+ writeFileSync(descriptor, body);
45
93
  }
46
94
  finally {
47
95
  closeSync(descriptor);
@@ -71,8 +119,22 @@ export function ownProjectData(dataDir) {
71
119
  peer = JSON.parse(raw);
72
120
  }
73
121
  catch { /* A live incomplete claim still refuses below. */ }
74
- if (peer && typeof peer === 'object' && 'host' in peer && peer.host !== hostname()) {
75
- throw new Error('foreign-host writer claim');
122
+ // What the host check was load-bearing FOR: a PID number is only evidence on the machine
123
+ // that wrote it, so a foreign claim must never be reaped by probing that PID here. A
124
+ // hostname was standing in for the machine, and a laptop that changes networks changes its
125
+ // name (#199), so a recorded identity that MATCHES this machine settles the question and
126
+ // lets the hostname become display-only.
127
+ //
128
+ // Deliberately one-way. A mismatched or absent identity falls back to the hostname exactly
129
+ // as before, so no claim this guard used to refuse becomes reapable: a restarted container
130
+ // with a pinned hostname still recovers its own dead claim, and two hosts sharing storage
131
+ // are no more reapable by PID than they were. Only "provably the same machine" is new.
132
+ const relation = peer && typeof peer === 'object'
133
+ ? machineRelation(peer.machine, machine)
134
+ : 'unknown';
135
+ const foreign = (recordedHost) => relation !== 'same' && recordedHost !== hostname();
136
+ if (peer && typeof peer === 'object' && 'host' in peer && foreign(peer.host)) {
137
+ throw new Error(foreignClaimDetail(path, peer.host, pid, relation));
76
138
  }
77
139
  let dead = false;
78
140
  try {
@@ -93,10 +155,13 @@ export function ownProjectData(dataDir) {
93
155
  }
94
156
  continue;
95
157
  }
158
+ // Same host rule as above, through the same helper: half the sites is half a fix, and a
159
+ // live writer on this machine under its earlier name must report itself as the live writer
160
+ // it is rather than as an invalid claim.
96
161
  if (!peer || typeof peer !== 'object' || !('pid' in peer) || !('host' in peer)
97
162
  || !Number.isSafeInteger(peer.pid) || Number(peer.pid) <= 0
98
- || peer.host !== hostname() || peer.pid !== pid) {
99
- throw new Error('invalid or foreign-host writer claim');
163
+ || foreign(peer.host) || peer.pid !== pid) {
164
+ throw new Error(`invalid or foreign-host writer claim: ${path}`);
100
165
  }
101
166
  throw new Error(`live writer PID ${peer.pid}`);
102
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"project-writer.js","sourceRoot":"","sources":["../../src/runs/project-writer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project-writer.js","sourceRoot":"","sources":["../../src/runs/project-writer.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type OwnershipScope } from '../mcp/resource-ownership.ts';
1
2
  import type { RunRecord } from './store.ts';
2
3
  /** A run is reclaimable when it is finished, still has a materialized worktree
3
4
  * directory, and has not already been reclaimed. */
@@ -34,7 +35,7 @@ export interface RematerializeStore {
34
35
  * run regains its isolated tree and becomes eligible for retention again;
35
36
  * without it the run would keep a directory on disk while staying invisible to
36
37
  * the enforcer forever (a leak). Returns true when it re-materialized.
37
- * Best-effort: never throws (the caller falls back to the repo root).
38
+ * Best-effort: never throws; the continuation caller refuses lost isolation before spawning.
38
39
  */
39
40
  export declare function rematerializeReclaimedWorktree(repoRoot: string, store: RematerializeStore, runId: string): Promise<boolean>;
40
41
  /**
@@ -47,7 +48,9 @@ export declare function rematerializeReclaimedWorktree(repoRoot: string, store:
47
48
  * report failure, so a run is stamped only once its directory is confirmed gone
48
49
  * — a locked/permission failure leaves the stamp unset so the next pass retries.
49
50
  * Idempotent under races: `removeWorktree` is `--force` + `prune` and a repeated
50
- * stamp is harmless.
51
+ * stamp is harmless. A caller with a lifecycle passes `shouldStop` and the sweep abandons the
52
+ * rest of the pass the moment that caller is done with the repository, returning the ids it had
53
+ * already reclaimed.
51
54
  */
52
55
  export interface ReclaimOptions {
53
56
  /** Timestamp source for the stamp — injectable for deterministic tests. */
@@ -56,5 +59,25 @@ export interface ReclaimOptions {
56
59
  * Injectable so tests can exercise the "removal failed" branch without brittle
57
60
  * filesystem-permission tricks. */
58
61
  remove?: (repoRoot: string, worktreePath: string) => Promise<void>;
62
+ /**
63
+ * Asked before EVERY iteration: has the caller stopped owning this repository?
64
+ *
65
+ * A sweep is a loop of `git worktree remove` spawns with a `store.updateRun` stamp after each
66
+ * one, so a caller that checks "am I still alive" once before calling this stops the sweep
67
+ * before its FIRST removal and never again — and a five-worktree pass that straddles a
68
+ * `RunManager.dispose()` goes on writing into a data root its owner has finished with (#200).
69
+ *
70
+ * Absent means "never stop", which is what every one-shot caller (boot, the reclaim route)
71
+ * wants: they have no lifecycle to lose mid-sweep.
72
+ */
73
+ shouldStop?: () => boolean;
59
74
  }
75
+ /**
76
+ * A run of this project whose recorded worktree, if it names one, is provably this project's own
77
+ * (`ownRun` + `ownWorktree`) or already gone — the rule above, per run. The cockpit's run routes
78
+ * that delete, commit, push or run git in that worktree ask the same question through this one
79
+ * function (#316), so reclaim and those routes cannot come to disagree about what is ours.
80
+ * `null` when the run is unknown here or reaches anywhere else.
81
+ */
82
+ export declare function ownRunAndWorktree(scope: OwnershipScope, runId: string): Promise<RunRecord | null>;
60
83
  export declare function reclaimWorktrees(repoRoot: string, store: RetentionStore, keep: number, opts?: ReclaimOptions): Promise<string[]>;
@@ -5,7 +5,9 @@
5
5
  // recoverable) and the thin I/O enforcer that performs the reclaim. The selector
6
6
  // is pure and unit-testable; the enforcer never throws (helper discipline).
7
7
  import { existsSync } from 'node:fs';
8
+ import { lstat } from 'node:fs/promises';
8
9
  import { createWorktree, removeWorktree } from '../git-worktree.js';
10
+ import { ownRun, ownWorktree, ownershipScope } from '../mcp/resource-ownership.js';
9
11
  /** The "finished" status set — mirrors `RunStore.archiveFinished`. A run at the
10
12
  * `review` gate is deliberately excluded: it still needs its worktree to render
11
13
  * the diff and open a draft PR, so reclaiming it would break the gate. */
@@ -45,7 +47,7 @@ export function selectReclaimableWorktrees(runs, keep) {
45
47
  * run regains its isolated tree and becomes eligible for retention again;
46
48
  * without it the run would keep a directory on disk while staying invisible to
47
49
  * the enforcer forever (a leak). Returns true when it re-materialized.
48
- * Best-effort: never throws (the caller falls back to the repo root).
50
+ * Best-effort: never throws; the continuation caller refuses lost isolation before spawning.
49
51
  */
50
52
  export async function rematerializeReclaimedWorktree(repoRoot, store, runId) {
51
53
  const run = store.getRun(runId);
@@ -60,13 +62,72 @@ export async function rematerializeReclaimedWorktree(repoRoot, store, runId) {
60
62
  return false;
61
63
  }
62
64
  }
65
+ /** Reclaim looks up no automation, so that half of the ownership scope refuses everything. */
66
+ const NO_AUTOMATIONS = { get: () => undefined, latestReceipts: () => new Map() };
67
+ /**
68
+ * The reclaimable runs whose worktree is provably this project's own (#288). A record's
69
+ * `worktreePath` is data — a copied or hand-edited `.local/xezar` can name another project's
70
+ * worktree — and reclaim deletes that directory. So a candidate must name this project's
71
+ * `<root>/.local/xezar/worktrees/<id>` (`ownRun`), and a directory that exists there must be a
72
+ * real one reached through no symlink (`ownWorktree`). A directory that is already gone reaches
73
+ * nothing and stays a candidate, so its stamp is still written. Anything else is left out BEFORE
74
+ * selection: it neither counts against `keep` nor is touched. Per record rather than `ownSweep`'s
75
+ * whole-sweep refusal, because this also runs unattended at boot and at every task's end, where
76
+ * refusing the whole sweep would silently switch retention (#483) off for the project.
77
+ */
78
+ async function ownedReclaimable(repoRoot, runs) {
79
+ const byId = new Map(runs.map((r) => [r.id, r]));
80
+ const scope = ownershipScope({
81
+ root: repoRoot,
82
+ store: { getRun: (id) => byId.get(id), listRuns: () => [...runs] },
83
+ automationStore: NO_AUTOMATIONS,
84
+ });
85
+ const owned = [];
86
+ for (const run of runs) {
87
+ if (isReclaimable(run) && (await ownRunAndWorktree(scope, run.id)))
88
+ owned.push(run);
89
+ }
90
+ return owned;
91
+ }
92
+ /**
93
+ * A run of this project whose recorded worktree, if it names one, is provably this project's own
94
+ * (`ownRun` + `ownWorktree`) or already gone — the rule above, per run. The cockpit's run routes
95
+ * that delete, commit, push or run git in that worktree ask the same question through this one
96
+ * function (#316), so reclaim and those routes cannot come to disagree about what is ours.
97
+ * `null` when the run is unknown here or reaches anywhere else.
98
+ */
99
+ export async function ownRunAndWorktree(scope, runId) {
100
+ const run = ownRun(scope, runId);
101
+ if (!run.ok)
102
+ return null;
103
+ const path = run.value.worktreePath;
104
+ if (path === undefined || (await ownWorktree(scope, runId)).ok || (await isAbsent(path)))
105
+ return run.value;
106
+ return null;
107
+ }
108
+ /** Nothing at `path`, not even a dangling link. Any other answer is not "absent" (fail closed). */
109
+ async function isAbsent(path) {
110
+ try {
111
+ await lstat(path);
112
+ return false;
113
+ }
114
+ catch (err) {
115
+ return err.code === 'ENOENT';
116
+ }
117
+ }
63
118
  export async function reclaimWorktrees(repoRoot, store, keep, opts = {}) {
64
119
  const now = opts.now ?? (() => new Date().toISOString());
65
120
  const remove = opts.remove ?? ((root, path) => removeWorktree(root, path)); // branch kept
121
+ const shouldStop = opts.shouldStop ?? (() => false);
66
122
  const runs = store.listRuns();
67
123
  const byId = new Map(runs.map((r) => [r.id, r]));
68
124
  const reclaimed = [];
69
- for (const id of selectReclaimableWorktrees(runs, keep)) {
125
+ for (const id of selectReclaimableWorktrees(await ownedReclaimable(repoRoot, runs), keep)) {
126
+ // Per iteration, not once before the loop: each pass costs a git spawn and a record write,
127
+ // and the caller's ownership can end between any two of them. Returning what was already
128
+ // reclaimed is honest — those directories really are gone.
129
+ if (shouldStop())
130
+ break;
70
131
  const run = byId.get(id);
71
132
  if (!run?.worktreePath)
72
133
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/runs/retention.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/runs/retention.ts"],"names":[],"mappings":"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"}
@@ -2482,6 +2482,49 @@ async function mergePullRequest(repoRoot, repoRef, number, input) {
2482
2482
  export function mergePreflightAllowed(current, overrideRules = false) {
2483
2483
  return current.canMerge || (overrideRules && current.canOverride);
2484
2484
  }
2485
+ const readyInflight = new Set();
2486
+ /**
2487
+ * Mark a draft pull request ready for review (`gh pr ready`). The same preflight shape as the
2488
+ * merge: a FRESH read of the forge, the reviewed head as a compare-and-swap, and a refusal in
2489
+ * plain words for a pull request that is not an open draft. What counts as a quality reason not
2490
+ * to ready it is the caller's policy; this only does what the forge would do. Never throws.
2491
+ */
2492
+ async function markPullRequestReady(repoRoot, repoRef, number, input) {
2493
+ const key = `${repoRoot}:${number}`;
2494
+ if (readyInflight.has(key))
2495
+ return { ready: false, status: 409, error: 'This pull request is already being marked ready.', code: 'concurrent' };
2496
+ readyInflight.add(key);
2497
+ try {
2498
+ const fresh = await fetchPrMergeState(repoRoot, repoRef, number, true);
2499
+ if (!fresh.available)
2500
+ return { ready: false, status: 502, error: fresh.reason };
2501
+ const current = fresh.mergeState;
2502
+ if (current.state !== 'open') {
2503
+ return { ready: false, status: 409, error: current.state === 'merged' ? 'This pull request is merged.' : 'This pull request is closed.', code: 'terminal', current };
2504
+ }
2505
+ if (!current.isDraft) {
2506
+ return { ready: false, status: 409, error: 'This pull request is already ready for review.', code: 'already-ready', current };
2507
+ }
2508
+ if (current.headSha !== input.expectedHeadSha) {
2509
+ return { ready: false, status: 409, error: 'The pull request head changed. Review the new commits before marking it ready.', code: 'stale-head', current };
2510
+ }
2511
+ if (process.env.XEZ_DRY_RUN === '1') {
2512
+ evictGithubProjectCaches(repoRoot);
2513
+ return { ready: true, number, url: current.url };
2514
+ }
2515
+ await gh(repoRoot, ['pr', 'ready', String(number)]);
2516
+ evictGithubProjectCaches(repoRoot);
2517
+ return { ready: true, number, url: current.url };
2518
+ }
2519
+ catch (error) {
2520
+ const message = firstLine(error instanceof Error ? error.message : String(error));
2521
+ const status = /403|permission|forbidden/i.test(message) ? 403 : /404|not found/i.test(message) ? 404 : 502;
2522
+ return { ready: false, status, error: status === 403 ? 'GitHub permission denied.' : status === 404 ? 'Pull request or repository not found.' : 'GitHub could not mark the pull request ready.' };
2523
+ }
2524
+ finally {
2525
+ readyInflight.delete(key);
2526
+ }
2527
+ }
2485
2528
  const GH_PR_STATES = {
2486
2529
  MERGED: 'merged',
2487
2530
  CLOSED: 'closed',
@@ -2520,6 +2563,7 @@ export function createGithubDriver(repoRoot, repoRef) {
2520
2563
  },
2521
2564
  prMergeState: (number, opts) => fetchPrMergeState(repoRoot, repoRef, number, opts?.refresh),
2522
2565
  mergePR: (number, input) => mergePullRequest(repoRoot, repoRef, number, input),
2566
+ markReady: (number, input) => markPullRequestReady(repoRoot, repoRef, number, input),
2523
2567
  viewUrl: (kind, ref) => {
2524
2568
  if (!repoRef)
2525
2569
  return null;