@tacuchi/agent-workflow-cli 25.8.0 → 26.0.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 (558) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +6 -3
  3. package/dist/adapters/git-cli.js +225 -17
  4. package/dist/adapters/git-cli.js.map +1 -1
  5. package/dist/adapters/node-file-system.js +48 -0
  6. package/dist/adapters/node-file-system.js.map +1 -1
  7. package/dist/application/amend-service.js +51 -14
  8. package/dist/application/amend-service.js.map +1 -1
  9. package/dist/application/branch-resolver.js +6 -3
  10. package/dist/application/branch-resolver.js.map +1 -1
  11. package/dist/application/capability/design-handler.js +218 -43
  12. package/dist/application/capability/design-handler.js.map +1 -1
  13. package/dist/application/capability/dispatcher.js +182 -19
  14. package/dist/application/capability/dispatcher.js.map +1 -1
  15. package/dist/application/capability/durable-effect.js +3 -0
  16. package/dist/application/capability/durable-effect.js.map +1 -1
  17. package/dist/application/capability/readiness.js +8 -54
  18. package/dist/application/capability/readiness.js.map +1 -1
  19. package/dist/application/capability/resolution.js +124 -192
  20. package/dist/application/capability/resolution.js.map +1 -1
  21. package/dist/application/check-branch-service.js +158 -19
  22. package/dist/application/check-branch-service.js.map +1 -1
  23. package/dist/application/checkpoint/markdown.js +2 -2
  24. package/dist/application/checkpoint/markdown.js.map +1 -1
  25. package/dist/application/checkpoint/state-reader.js +12 -0
  26. package/dist/application/checkpoint/state-reader.js.map +1 -1
  27. package/dist/application/checkpoint-service.js +12 -4
  28. package/dist/application/checkpoint-service.js.map +1 -1
  29. package/dist/application/checkpoint-write-service.js +91 -17
  30. package/dist/application/checkpoint-write-service.js.map +1 -1
  31. package/dist/application/claims-ledger.js +137 -11
  32. package/dist/application/claims-ledger.js.map +1 -1
  33. package/dist/application/claims-recovery.js +85 -44
  34. package/dist/application/claims-recovery.js.map +1 -1
  35. package/dist/application/database-tool-catalog.js +33 -0
  36. package/dist/application/database-tool-catalog.js.map +1 -1
  37. package/dist/application/design/consumer-document.js +2 -2
  38. package/dist/application/design/consumer-document.js.map +1 -1
  39. package/dist/application/design/design-content-gate-service.js +20 -2
  40. package/dist/application/design/design-content-gate-service.js.map +1 -1
  41. package/dist/application/design/design-gate-service.js +2 -0
  42. package/dist/application/design/design-gate-service.js.map +1 -1
  43. package/dist/application/design/design-index-service.js +4 -0
  44. package/dist/application/design/design-index-service.js.map +1 -1
  45. package/dist/application/design/design-publish-service.js +1 -4
  46. package/dist/application/design/design-publish-service.js.map +1 -1
  47. package/dist/application/design/design-record-service.js +13 -7
  48. package/dist/application/design/design-record-service.js.map +1 -1
  49. package/dist/application/design/design-resolver-service.js +22 -3
  50. package/dist/application/design/design-resolver-service.js.map +1 -1
  51. package/dist/application/design/design-simple-service.js +24 -5
  52. package/dist/application/design/design-simple-service.js.map +1 -1
  53. package/dist/application/design/digest.js +41 -0
  54. package/dist/application/design/digest.js.map +1 -0
  55. package/dist/application/dev-only-services.js +166 -20
  56. package/dist/application/dev-only-services.js.map +1 -1
  57. package/dist/application/doc-branch-ledger.js +145 -0
  58. package/dist/application/doc-branch-ledger.js.map +1 -0
  59. package/dist/application/docs-canon-service.js +1 -1
  60. package/dist/application/docs-canon-service.js.map +1 -1
  61. package/dist/application/doctor/dist-integrity.js +113 -0
  62. package/dist/application/doctor/dist-integrity.js.map +1 -0
  63. package/dist/application/doctor/hosts.js +5 -2
  64. package/dist/application/doctor/hosts.js.map +1 -1
  65. package/dist/application/doctor/prepare.js +3 -3
  66. package/dist/application/doctor/prepare.js.map +1 -1
  67. package/dist/application/doctor/provider-installation.js +39 -19
  68. package/dist/application/doctor/provider-installation.js.map +1 -1
  69. package/dist/application/doctor/provider-mcps.js +9 -1
  70. package/dist/application/doctor/provider-mcps.js.map +1 -1
  71. package/dist/application/doctor/provider-skills.js +29 -115
  72. package/dist/application/doctor/provider-skills.js.map +1 -1
  73. package/dist/application/doctor/provider-visibility.js +76 -3
  74. package/dist/application/doctor/provider-visibility.js.map +1 -1
  75. package/dist/application/doctor/provider-workspace-block.js +156 -0
  76. package/dist/application/doctor/provider-workspace-block.js.map +1 -0
  77. package/dist/application/doctor/repair-runner.js +36 -39
  78. package/dist/application/doctor/repair-runner.js.map +1 -1
  79. package/dist/application/doctor/report.js +24 -4
  80. package/dist/application/doctor/report.js.map +1 -1
  81. package/dist/application/export-catalog-check.js +200 -0
  82. package/dist/application/export-catalog-check.js.map +1 -0
  83. package/dist/application/export-service.js +716 -79
  84. package/dist/application/export-service.js.map +1 -1
  85. package/dist/application/fix-git-service.js +285 -64
  86. package/dist/application/fix-git-service.js.map +1 -1
  87. package/dist/application/flow/advance.js +202 -25
  88. package/dist/application/flow/advance.js.map +1 -1
  89. package/dist/application/flow/annul-service.js +3 -1
  90. package/dist/application/flow/annul-service.js.map +1 -1
  91. package/dist/application/flow/batch-commit.js +189 -0
  92. package/dist/application/flow/batch-commit.js.map +1 -0
  93. package/dist/application/flow/checkout-observation.js +63 -10
  94. package/dist/application/flow/checkout-observation.js.map +1 -1
  95. package/dist/application/flow/close-artifacts.js +105 -0
  96. package/dist/application/flow/close-artifacts.js.map +1 -0
  97. package/dist/application/flow/close-at-boundary.js +25 -14
  98. package/dist/application/flow/close-at-boundary.js.map +1 -1
  99. package/dist/application/flow/close-document-guidance.js +70 -0
  100. package/dist/application/flow/close-document-guidance.js.map +1 -0
  101. package/dist/application/flow/flow-service.js +46 -3
  102. package/dist/application/flow/flow-service.js.map +1 -1
  103. package/dist/application/flow/internal-actions.js +300 -82
  104. package/dist/application/flow/internal-actions.js.map +1 -1
  105. package/dist/application/flow/internal-drive.js +24 -1
  106. package/dist/application/flow/internal-drive.js.map +1 -1
  107. package/dist/application/flow/plan-entry.js +82 -0
  108. package/dist/application/flow/plan-entry.js.map +1 -0
  109. package/dist/application/flow/prove.js +32 -1
  110. package/dist/application/flow/prove.js.map +1 -1
  111. package/dist/application/flow/quick-checkouts.js +48 -0
  112. package/dist/application/flow/quick-checkouts.js.map +1 -0
  113. package/dist/application/flow/reinfer-batch.js +177 -0
  114. package/dist/application/flow/reinfer-batch.js.map +1 -0
  115. package/dist/application/flow/reopen-run.js +12 -5
  116. package/dist/application/flow/reopen-run.js.map +1 -1
  117. package/dist/application/flow/run-state-service.js +2 -0
  118. package/dist/application/flow/run-state-service.js.map +1 -1
  119. package/dist/application/flow/submit.js +584 -117
  120. package/dist/application/flow/submit.js.map +1 -1
  121. package/dist/application/generate-launch-service.js +16 -6
  122. package/dist/application/generate-launch-service.js.map +1 -1
  123. package/dist/application/git-flow-service.js +34 -3
  124. package/dist/application/git-flow-service.js.map +1 -1
  125. package/dist/application/history-publications.js +24 -0
  126. package/dist/application/history-publications.js.map +1 -1
  127. package/dist/application/history-reconcile-service.js +45 -0
  128. package/dist/application/history-reconcile-service.js.map +1 -0
  129. package/dist/application/history-table.js +61 -12
  130. package/dist/application/history-table.js.map +1 -1
  131. package/dist/application/history-update-service.js +25 -4
  132. package/dist/application/history-update-service.js.map +1 -1
  133. package/dist/application/hook-branch-check.js +25 -3
  134. package/dist/application/hook-branch-check.js.map +1 -1
  135. package/dist/application/hook-git-commit-advisor.js +3 -4
  136. package/dist/application/hook-git-commit-advisor.js.map +1 -1
  137. package/dist/application/hook-turn-start.js +51 -0
  138. package/dist/application/hook-turn-start.js.map +1 -0
  139. package/dist/application/host-memory/codex.js +1 -1
  140. package/dist/application/host-memory/codex.js.map +1 -1
  141. package/dist/application/hub-registry.js +135 -0
  142. package/dist/application/hub-registry.js.map +1 -0
  143. package/dist/application/local-proposal.js +72 -19
  144. package/dist/application/local-proposal.js.map +1 -1
  145. package/dist/application/mcp-host-reader.js +1 -1
  146. package/dist/application/mcp-host-reader.js.map +1 -1
  147. package/dist/application/mcp-host-writer.js +16 -10
  148. package/dist/application/mcp-host-writer.js.map +1 -1
  149. package/dist/application/merge-state-service.js +150 -34
  150. package/dist/application/merge-state-service.js.map +1 -1
  151. package/dist/application/multiroot/claude.js +17 -10
  152. package/dist/application/multiroot/claude.js.map +1 -1
  153. package/dist/application/multiroot/codex.js +17 -11
  154. package/dist/application/multiroot/codex.js.map +1 -1
  155. package/dist/application/multiroot-service.js +52 -28
  156. package/dist/application/multiroot-service.js.map +1 -1
  157. package/dist/application/parsers/execution-batches.js +64 -0
  158. package/dist/application/parsers/execution-batches.js.map +1 -0
  159. package/dist/application/parsers/phases.js +23 -2
  160. package/dist/application/parsers/phases.js.map +1 -1
  161. package/dist/application/parsers/project-block.js +146 -17
  162. package/dist/application/parsers/project-block.js.map +1 -1
  163. package/dist/application/parsers/tasks.js +16 -2
  164. package/dist/application/parsers/tasks.js.map +1 -1
  165. package/dist/application/parsers/toml.js +19 -0
  166. package/dist/application/parsers/toml.js.map +1 -0
  167. package/dist/application/paths-service.js +13 -16
  168. package/dist/application/paths-service.js.map +1 -1
  169. package/dist/application/persist-service.js +118 -5
  170. package/dist/application/persist-service.js.map +1 -1
  171. package/dist/application/plan-exec-batch-service.js +162 -22
  172. package/dist/application/plan-exec-batch-service.js.map +1 -1
  173. package/dist/application/plan-exec-decision-service.js +61 -12
  174. package/dist/application/plan-exec-decision-service.js.map +1 -1
  175. package/dist/application/plan-exec-plan-diff.js +105 -0
  176. package/dist/application/plan-exec-plan-diff.js.map +1 -0
  177. package/dist/application/plan-lint-service.js +33 -5
  178. package/dist/application/plan-lint-service.js.map +1 -1
  179. package/dist/application/plan-passes.js +41 -0
  180. package/dist/application/plan-passes.js.map +1 -0
  181. package/dist/application/preparation-receipts.js +58 -0
  182. package/dist/application/preparation-receipts.js.map +1 -0
  183. package/dist/application/project-md-upsert-service.js +191 -52
  184. package/dist/application/project-md-upsert-service.js.map +1 -1
  185. package/dist/application/project-tab-data.js +40 -10
  186. package/dist/application/project-tab-data.js.map +1 -1
  187. package/dist/application/release-data/artifacts.js +15 -7
  188. package/dist/application/release-data/artifacts.js.map +1 -1
  189. package/dist/application/release-data/bundles.js +52 -6
  190. package/dist/application/release-data/bundles.js.map +1 -1
  191. package/dist/application/release-data/common.js +2 -2
  192. package/dist/application/release-data/common.js.map +1 -1
  193. package/dist/application/release-data/rollback.js +7 -0
  194. package/dist/application/release-data/rollback.js.map +1 -0
  195. package/dist/application/release-data/sessions.js +2 -2
  196. package/dist/application/release-data/sessions.js.map +1 -1
  197. package/dist/application/release-pass-ledger.js +1 -1
  198. package/dist/application/render/history-row.js +2 -0
  199. package/dist/application/render/history-row.js.map +1 -1
  200. package/dist/application/render/project-block.js +28 -11
  201. package/dist/application/render/project-block.js.map +1 -1
  202. package/dist/application/reseal-service.js +1 -0
  203. package/dist/application/reseal-service.js.map +1 -1
  204. package/dist/application/resume-service.js +55 -8
  205. package/dist/application/resume-service.js.map +1 -1
  206. package/dist/application/retirement/apply.js +25 -7
  207. package/dist/application/retirement/apply.js.map +1 -1
  208. package/dist/application/retirement/attribution.js +122 -50
  209. package/dist/application/retirement/attribution.js.map +1 -1
  210. package/dist/application/retirement/prepare.js +17 -11
  211. package/dist/application/retirement/prepare.js.map +1 -1
  212. package/dist/application/retirement/preview.js +2 -1
  213. package/dist/application/retirement/preview.js.map +1 -1
  214. package/dist/application/self/clean-legacy.js +1 -2
  215. package/dist/application/self/clean-legacy.js.map +1 -1
  216. package/dist/application/self/codex-plugin.js +1 -1
  217. package/dist/application/self/codex-plugin.js.map +1 -1
  218. package/dist/application/self/host-states.js +3 -3
  219. package/dist/application/self/host-states.js.map +1 -1
  220. package/dist/application/self/install-hooks.js +1 -1
  221. package/dist/application/self/install-hooks.js.map +1 -1
  222. package/dist/application/self/install-skill.js +5 -49
  223. package/dist/application/self/install-skill.js.map +1 -1
  224. package/dist/application/self/mcp-config.js +19 -11
  225. package/dist/application/self/mcp-config.js.map +1 -1
  226. package/dist/application/self/opencode-plugin.js +1 -0
  227. package/dist/application/self/opencode-plugin.js.map +1 -1
  228. package/dist/application/self/skill-files.js +29 -0
  229. package/dist/application/self/skill-files.js.map +1 -0
  230. package/dist/application/self/uninstall.js +1 -1
  231. package/dist/application/self/uninstall.js.map +1 -1
  232. package/dist/application/semantic-operation/protocol.js +66 -5
  233. package/dist/application/semantic-operation/protocol.js.map +1 -1
  234. package/dist/application/session-archive-service.js +113 -0
  235. package/dist/application/session-archive-service.js.map +1 -0
  236. package/dist/application/session-binding-service.js +35 -0
  237. package/dist/application/session-binding-service.js.map +1 -1
  238. package/dist/application/session-close-service.js +390 -54
  239. package/dist/application/session-close-service.js.map +1 -1
  240. package/dist/application/session-create-service.js +97 -5
  241. package/dist/application/session-create-service.js.map +1 -1
  242. package/dist/application/session-custody-recorder.js +15 -12
  243. package/dist/application/session-custody-recorder.js.map +1 -1
  244. package/dist/application/session-custody-service.js +1 -0
  245. package/dist/application/session-custody-service.js.map +1 -1
  246. package/dist/application/session-narrative.js +42 -12
  247. package/dist/application/session-narrative.js.map +1 -1
  248. package/dist/application/session-pause-service.js +46 -0
  249. package/dist/application/session-pause-service.js.map +1 -0
  250. package/dist/application/session-resolver.js +36 -11
  251. package/dist/application/session-resolver.js.map +1 -1
  252. package/dist/application/session-resume-service.js +134 -13
  253. package/dist/application/session-resume-service.js.map +1 -1
  254. package/dist/application/sessions-service.js +4 -0
  255. package/dist/application/sessions-service.js.map +1 -1
  256. package/dist/application/sessions-sweep-service.js +77 -0
  257. package/dist/application/sessions-sweep-service.js.map +1 -0
  258. package/dist/application/settle-service.js +3 -1
  259. package/dist/application/settle-service.js.map +1 -1
  260. package/dist/application/skills-resolver-service.js +6 -92
  261. package/dist/application/skills-resolver-service.js.map +1 -1
  262. package/dist/application/source-add-service.js +37 -0
  263. package/dist/application/source-add-service.js.map +1 -0
  264. package/dist/application/source-boundary-policy.js +105 -8
  265. package/dist/application/source-boundary-policy.js.map +1 -1
  266. package/dist/application/source-pipeline.js +68 -0
  267. package/dist/application/source-pipeline.js.map +1 -0
  268. package/dist/application/source-remove-service.js +15 -3
  269. package/dist/application/source-remove-service.js.map +1 -1
  270. package/dist/application/sources-service.js +71 -16
  271. package/dist/application/sources-service.js.map +1 -1
  272. package/dist/application/status-service.js +55 -5
  273. package/dist/application/status-service.js.map +1 -1
  274. package/dist/application/unit-dependencies.js +100 -0
  275. package/dist/application/unit-dependencies.js.map +1 -0
  276. package/dist/application/unit-membership.js +37 -0
  277. package/dist/application/unit-membership.js.map +1 -0
  278. package/dist/application/unit-residue.js +72 -0
  279. package/dist/application/unit-residue.js.map +1 -0
  280. package/dist/application/visibility-doctor-service.js +66 -4
  281. package/dist/application/visibility-doctor-service.js.map +1 -1
  282. package/dist/application/working-branch-service.js +6 -0
  283. package/dist/application/working-branch-service.js.map +1 -1
  284. package/dist/application/workline-graph.js +1 -1
  285. package/dist/application/workline-graph.js.map +1 -1
  286. package/dist/application/workline-index-service.js +118 -17
  287. package/dist/application/workline-index-service.js.map +1 -1
  288. package/dist/application/workspace-commit-service.js +218 -0
  289. package/dist/application/workspace-commit-service.js.map +1 -0
  290. package/dist/application/workspace-init-service.js +64 -34
  291. package/dist/application/workspace-init-service.js.map +1 -1
  292. package/dist/application/workspace-local-config.js +90 -0
  293. package/dist/application/workspace-local-config.js.map +1 -0
  294. package/dist/application/workspace-materialization-service.js +52 -3
  295. package/dist/application/workspace-materialization-service.js.map +1 -1
  296. package/dist/application/workspace-migrate/apply.js +525 -4
  297. package/dist/application/workspace-migrate/apply.js.map +1 -1
  298. package/dist/application/workspace-migrate/markers.js +9 -6
  299. package/dist/application/workspace-migrate/markers.js.map +1 -1
  300. package/dist/application/workspace-migrate/plan.js +97 -5
  301. package/dist/application/workspace-migrate/plan.js.map +1 -1
  302. package/dist/application/workspace-move-service.js +270 -0
  303. package/dist/application/workspace-move-service.js.map +1 -0
  304. package/dist/application/workspace-untrack-service.js +28 -0
  305. package/dist/application/workspace-untrack-service.js.map +1 -0
  306. package/dist/application/worktree-service.js +654 -105
  307. package/dist/application/worktree-service.js.map +1 -1
  308. package/dist/cli/ascii.js +2 -0
  309. package/dist/cli/ascii.js.map +1 -1
  310. package/dist/cli/commands/add-source.js +38 -0
  311. package/dist/cli/commands/add-source.js.map +1 -0
  312. package/dist/cli/commands/amend.js +14 -3
  313. package/dist/cli/commands/amend.js.map +1 -1
  314. package/dist/cli/commands/capability.js +23 -4
  315. package/dist/cli/commands/capability.js.map +1 -1
  316. package/dist/cli/commands/designs.js +14 -5
  317. package/dist/cli/commands/designs.js.map +1 -1
  318. package/dist/cli/commands/dev-only.js +19 -6
  319. package/dist/cli/commands/dev-only.js.map +1 -1
  320. package/dist/cli/commands/doc-branch.js +184 -0
  321. package/dist/cli/commands/doc-branch.js.map +1 -0
  322. package/dist/cli/commands/doctor.js +5 -1
  323. package/dist/cli/commands/doctor.js.map +1 -1
  324. package/dist/cli/commands/export.js +78 -18
  325. package/dist/cli/commands/export.js.map +1 -1
  326. package/dist/cli/commands/fix-git.js +89 -14
  327. package/dist/cli/commands/fix-git.js.map +1 -1
  328. package/dist/cli/commands/flow.js +120 -15
  329. package/dist/cli/commands/flow.js.map +1 -1
  330. package/dist/cli/commands/generate-launch.js +14 -2
  331. package/dist/cli/commands/generate-launch.js.map +1 -1
  332. package/dist/cli/commands/history-update.js +1 -1
  333. package/dist/cli/commands/history-update.js.map +1 -1
  334. package/dist/cli/commands/history.js +14 -0
  335. package/dist/cli/commands/history.js.map +1 -0
  336. package/dist/cli/commands/hook.js +15 -2
  337. package/dist/cli/commands/hook.js.map +1 -1
  338. package/dist/cli/commands/index.js +18 -5
  339. package/dist/cli/commands/index.js.map +1 -1
  340. package/dist/cli/commands/merge-state.js +2 -2
  341. package/dist/cli/commands/merge-state.js.map +1 -1
  342. package/dist/cli/commands/persist.js +6 -5
  343. package/dist/cli/commands/persist.js.map +1 -1
  344. package/dist/cli/commands/project-md-upsert.js +5 -3
  345. package/dist/cli/commands/project-md-upsert.js.map +1 -1
  346. package/dist/cli/commands/release-pass.js +3 -3
  347. package/dist/cli/commands/release-pass.js.map +1 -1
  348. package/dist/cli/commands/resume.js +12 -3
  349. package/dist/cli/commands/resume.js.map +1 -1
  350. package/dist/cli/commands/self.js +8 -24
  351. package/dist/cli/commands/self.js.map +1 -1
  352. package/dist/cli/commands/session-close.js +64 -9
  353. package/dist/cli/commands/session-close.js.map +1 -1
  354. package/dist/cli/commands/session-create.js +4 -2
  355. package/dist/cli/commands/session-create.js.map +1 -1
  356. package/dist/cli/commands/session-pause.js +14 -0
  357. package/dist/cli/commands/session-pause.js.map +1 -0
  358. package/dist/cli/commands/sessions.js +16 -3
  359. package/dist/cli/commands/sessions.js.map +1 -1
  360. package/dist/cli/commands/set-branch.js +28 -4
  361. package/dist/cli/commands/set-branch.js.map +1 -1
  362. package/dist/cli/commands/set-edit-mode.js +20 -0
  363. package/dist/cli/commands/set-edit-mode.js.map +1 -0
  364. package/dist/cli/commands/set-pipeline.js +35 -0
  365. package/dist/cli/commands/set-pipeline.js.map +1 -0
  366. package/dist/cli/commands/skills.js +4 -11
  367. package/dist/cli/commands/skills.js.map +1 -1
  368. package/dist/cli/commands/sources.js +4 -0
  369. package/dist/cli/commands/sources.js.map +1 -1
  370. package/dist/cli/commands/status.js +35 -2
  371. package/dist/cli/commands/status.js.map +1 -1
  372. package/dist/cli/commands/unknown-flags.js +1 -0
  373. package/dist/cli/commands/unknown-flags.js.map +1 -1
  374. package/dist/cli/commands/visibility.js +1 -0
  375. package/dist/cli/commands/visibility.js.map +1 -1
  376. package/dist/cli/commands/workspace-commit.js +62 -0
  377. package/dist/cli/commands/workspace-commit.js.map +1 -0
  378. package/dist/cli/commands/workspace-init.js +39 -2
  379. package/dist/cli/commands/workspace-init.js.map +1 -1
  380. package/dist/cli/commands/workspace-migrate.js +31 -3
  381. package/dist/cli/commands/workspace-migrate.js.map +1 -1
  382. package/dist/cli/commands/workspace-move.js +42 -0
  383. package/dist/cli/commands/workspace-move.js.map +1 -0
  384. package/dist/cli/commands/worktree.js +4 -4
  385. package/dist/cli/commands/worktree.js.map +1 -1
  386. package/dist/cli/context-id.js +6 -0
  387. package/dist/cli/context-id.js.map +1 -1
  388. package/dist/cli/full-cli.js +555 -0
  389. package/dist/cli/full-cli.js.map +1 -0
  390. package/dist/cli/help-groups.js +76 -19
  391. package/dist/cli/help-groups.js.map +1 -1
  392. package/dist/cli/hook-stdin-cache.js +12 -0
  393. package/dist/cli/hook-stdin-cache.js.map +1 -0
  394. package/dist/cli/main.js +47 -453
  395. package/dist/cli/main.js.map +1 -1
  396. package/dist/cli/parser.js +4 -0
  397. package/dist/cli/parser.js.map +1 -1
  398. package/dist/cli/tui/app.js +3 -4
  399. package/dist/cli/tui/app.js.map +1 -1
  400. package/dist/cli/tui/components/host-admin-section.js +2 -13
  401. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  402. package/dist/cli/tui/components/tabs-config.js +1 -2
  403. package/dist/cli/tui/components/tabs-config.js.map +1 -1
  404. package/dist/cli/tui/components/workspace-init-form.js +4 -1
  405. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  406. package/dist/cli/tui/data/workflow-content.js +6 -1
  407. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  408. package/dist/cli/tui/tabs/mcp-tab.js +4 -3
  409. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  410. package/dist/cli/tui/tabs/project-tab.js +7 -2
  411. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  412. package/dist/cli/tui/tui-prefs.js +2 -0
  413. package/dist/cli/tui/tui-prefs.js.map +1 -1
  414. package/dist/dist-manifest.json +3973 -0
  415. package/dist/domain/capability/protocol.js +3 -1
  416. package/dist/domain/capability/protocol.js.map +1 -1
  417. package/dist/domain/decision-note.js +13 -0
  418. package/dist/domain/decision-note.js.map +1 -1
  419. package/dist/domain/design/artifact.js +22 -3
  420. package/dist/domain/design/artifact.js.map +1 -1
  421. package/dist/domain/design/capability.js +1 -0
  422. package/dist/domain/design/capability.js.map +1 -1
  423. package/dist/domain/design/expansion.js +1 -1
  424. package/dist/domain/design/expansion.js.map +1 -1
  425. package/dist/domain/design/maturity.js +16 -0
  426. package/dist/domain/design/maturity.js.map +1 -1
  427. package/dist/domain/design/reference.js +5 -0
  428. package/dist/domain/design/reference.js.map +1 -1
  429. package/dist/domain/doctor/operations.js +20 -12
  430. package/dist/domain/doctor/operations.js.map +1 -1
  431. package/dist/domain/effective-contract.js +3 -1
  432. package/dist/domain/effective-contract.js.map +1 -1
  433. package/dist/domain/elicitation.js +1 -1
  434. package/dist/domain/flow/answer.js +265 -130
  435. package/dist/domain/flow/answer.js.map +1 -1
  436. package/dist/domain/flow/authority.js +194 -37
  437. package/dist/domain/flow/authority.js.map +1 -1
  438. package/dist/domain/flow/batch-review.js +65 -0
  439. package/dist/domain/flow/batch-review.js.map +1 -0
  440. package/dist/domain/flow/decision-schema.js +47 -0
  441. package/dist/domain/flow/decision-schema.js.map +1 -0
  442. package/dist/domain/flow/directive.js +16 -0
  443. package/dist/domain/flow/directive.js.map +1 -1
  444. package/dist/domain/flow/execution-result.js +91 -2
  445. package/dist/domain/flow/execution-result.js.map +1 -1
  446. package/dist/domain/flow/run-state.js +352 -24
  447. package/dist/domain/flow/run-state.js.map +1 -1
  448. package/dist/domain/flow/test-run-evidence.js +285 -0
  449. package/dist/domain/flow/test-run-evidence.js.map +1 -0
  450. package/dist/domain/flow/unchanged-phase.js +15 -0
  451. package/dist/domain/flow/unchanged-phase.js.map +1 -0
  452. package/dist/domain/harnesses.js +14 -5
  453. package/dist/domain/harnesses.js.map +1 -1
  454. package/dist/domain/isolation-unit.js +2 -2
  455. package/dist/domain/isolation-unit.js.map +1 -1
  456. package/dist/domain/lineage.js +7 -2
  457. package/dist/domain/lineage.js.map +1 -1
  458. package/dist/domain/obligation-correspondence.js +4 -2
  459. package/dist/domain/obligation-correspondence.js.map +1 -1
  460. package/dist/domain/plan-handoff.js +5 -0
  461. package/dist/domain/plan-handoff.js.map +1 -0
  462. package/dist/domain/proposal.js +18 -7
  463. package/dist/domain/proposal.js.map +1 -1
  464. package/dist/domain/reservation.js +5 -3
  465. package/dist/domain/reservation.js.map +1 -1
  466. package/dist/domain/retirement/proposal.js +1 -0
  467. package/dist/domain/retirement/proposal.js.map +1 -1
  468. package/dist/domain/session/custody.js +10 -6
  469. package/dist/domain/session/custody.js.map +1 -1
  470. package/dist/domain/session/narrative.js +4 -1
  471. package/dist/domain/session/narrative.js.map +1 -1
  472. package/dist/domain/skills.js +4 -6
  473. package/dist/domain/skills.js.map +1 -1
  474. package/dist/domain/sql-lexer.js +1 -1
  475. package/dist/domain/sql-lexer.js.map +1 -1
  476. package/dist/runtime/namespace-resolver.js +4 -0
  477. package/dist/runtime/namespace-resolver.js.map +1 -1
  478. package/dist/runtime/workline-marker.js +35 -8
  479. package/dist/runtime/workline-marker.js.map +1 -1
  480. package/dist/runtime/workspace-resolution.js +123 -0
  481. package/dist/runtime/workspace-resolution.js.map +1 -0
  482. package/package.json +6 -5
  483. package/skills/w/SKILL.md +12 -17
  484. package/skills/w/artifacts/artifacts-core/CHECKPOINT.md +1 -1
  485. package/skills/w/artifacts/artifacts-core/SCRIPTS.rollback.sql +8 -0
  486. package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +4 -8
  487. package/skills/w/commands/export-diagrams.md +3 -3
  488. package/skills/w/commands/export-manuals.md +6 -6
  489. package/skills/w/commands/export-reports.md +3 -3
  490. package/skills/w/commands/export-scripts.md +5 -5
  491. package/skills/w/commands/fix-git.md +12 -13
  492. package/skills/w/commands/persist.md +2 -2
  493. package/skills/w/commands/plan-exec.md +4 -2
  494. package/skills/w/commands/spec-new.md +1 -1
  495. package/skills/w/commands/workspace-init.md +2 -0
  496. package/skills/w/context/MANIFEST.json +1 -1
  497. package/skills/w/exports/README.md +4 -4
  498. package/skills/w/exports/export-diagrams/EXPORT.md +4 -4
  499. package/skills/w/exports/export-manuals/EXPORT.md +14 -16
  500. package/skills/w/exports/export-reports/EXPORT.md +3 -3
  501. package/skills/w/exports/export-scripts/EXPORT.md +46 -22
  502. package/skills/w/harness/HARNESS.md +4 -7
  503. package/skills/w/hooks/hooks.template.json +6 -4
  504. package/skills/w/loops/CHASSIS.md +12 -6
  505. package/skills/w/loops/CODE-POLICIES.md +8 -5
  506. package/skills/w/loops/README.md +2 -2
  507. package/skills/w/loops/plan-exec-loop/LOOP.md +23 -22
  508. package/skills/w/loops/plan-new-loop/LOOP.md +23 -31
  509. package/skills/w/loops/plan-refine-loop/LOOP.md +8 -2
  510. package/skills/w/loops/quick-loop/LOOP.md +7 -6
  511. package/skills/w/loops/spec-refine-loop/LOOP.md +1 -1
  512. package/skills/w/modules/DESIGN-REFERENCES.md +9 -11
  513. package/skills/w/modules/PLAN-EXECUTION-BATCHES.md +4 -4
  514. package/skills/w/modules/SESSION-NUMBERING.md +2 -0
  515. package/skills/w/roles/README.md +11 -47
  516. package/skills/w/roles/design/CONTRACT.md +6 -4
  517. package/skills/w/roles/design/ROLE.md +7 -3
  518. package/skills/w/roles/diagrams/ROLE.md +1 -1
  519. package/skills/w/roles/git/ROLE.md +17 -10
  520. package/skills/w/roles/research/ROLE.md +2 -2
  521. package/skills/w/roles/sql/ROLE.md +17 -15
  522. package/skills/w/schemas/design/ui-screen.v1.schema.json +9 -0
  523. package/dist/application/capability/descriptor-loader.js +0 -109
  524. package/dist/application/capability/descriptor-loader.js.map +0 -1
  525. package/dist/application/capability/installed-inventory.js +0 -168
  526. package/dist/application/capability/installed-inventory.js.map +0 -1
  527. package/dist/application/self/install-plugin-skills-git.js +0 -226
  528. package/dist/application/self/install-plugin-skills-git.js.map +0 -1
  529. package/dist/application/self/install-plugin-skills.js +0 -203
  530. package/dist/application/self/install-plugin-skills.js.map +0 -1
  531. package/dist/application/self/plugin-cache-clear.js +0 -133
  532. package/dist/application/self/plugin-cache-clear.js.map +0 -1
  533. package/dist/application/self/plugin-cache-reload.js +0 -141
  534. package/dist/application/self/plugin-cache-reload.js.map +0 -1
  535. package/dist/application/self/skills-apply.js +0 -623
  536. package/dist/application/self/skills-apply.js.map +0 -1
  537. package/dist/application/self/skills-catalog.js +0 -35
  538. package/dist/application/self/skills-catalog.js.map +0 -1
  539. package/dist/application/self/skills-change.js +0 -479
  540. package/dist/application/self/skills-change.js.map +0 -1
  541. package/dist/application/self/skills-discovery.js +0 -275
  542. package/dist/application/self/skills-discovery.js.map +0 -1
  543. package/dist/application/self/skills-manager.js +0 -253
  544. package/dist/application/self/skills-manager.js.map +0 -1
  545. package/dist/application/self/skills-payload.js +0 -566
  546. package/dist/application/self/skills-payload.js.map +0 -1
  547. package/dist/application/self/skills-registry.js +0 -107
  548. package/dist/application/self/skills-registry.js.map +0 -1
  549. package/dist/application/skill-index-service.js +0 -78
  550. package/dist/application/skill-index-service.js.map +0 -1
  551. package/dist/cli/commands/plugin-cache.js +0 -25
  552. package/dist/cli/commands/plugin-cache.js.map +0 -1
  553. package/dist/cli/commands/skill-index.js +0 -19
  554. package/dist/cli/commands/skill-index.js.map +0 -1
  555. package/dist/cli/tui/data/recommended-skills.js +0 -482
  556. package/dist/cli/tui/data/recommended-skills.js.map +0 -1
  557. package/dist/cli/tui/tabs/skills-tab.js +0 -664
  558. package/dist/cli/tui/tabs/skills-tab.js.map +0 -1
@@ -1,13 +1,18 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { parseUnitPath, unitBranch, unitPath, workspaceKey, } from "../domain/isolation-unit.js";
3
- import { resolveSourceBranches } from "./branch-resolver.js";
3
+ import { isWorkingBranch, resolveSourceBranches } from "./branch-resolver.js";
4
+ import { documentOfSession, resolveDocBranch } from "./doc-branch-ledger.js";
4
5
  import { locateRun, readRun } from "./flow/run-state-service.js";
5
6
  import { withCwdLock } from "./lock-service.js";
6
7
  import { runMultiroot } from "./multiroot-service.js";
7
8
  import { normalizePath } from "./multiroot/paths.js";
8
- import { readWorkspaceBlock } from "./parsers/project-block.js";
9
+ import { readWorkspaceBlock, requireSourcePath, } from "./parsers/project-block.js";
9
10
  import { recordIntegration, recordUnitTaken } from "./session-custody-recorder.js";
10
- import { listSessionFolders, resolveSessionTarget, sessionFolderMatches, } from "./session-resolver.js";
11
+ import { readCustody } from "./session-custody-service.js";
12
+ import { listSessionFolders, readSessionState, resolveSessionTarget, sessionFolderMatches, } from "./session-resolver.js";
13
+ import { linkUnitDependencies, removeUnitSafely, } from "./unit-dependencies.js";
14
+ import { hubUnitPaths } from "./unit-membership.js";
15
+ import { finishResidue } from "./unit-residue.js";
11
16
  import { ensureWorklineMaterialized } from "./workspace-materialization-service.js";
12
17
  /**
13
18
  * How long an integration waits for the workspace lock before giving up.
@@ -16,6 +21,67 @@ import { ensureWorklineMaterialized } from "./workspace-materialization-service.
16
21
  * a merge: the wait is bounded by another merge finishing, not by a file write.
17
22
  */
18
23
  const INTEGRATE_LOCK_WAIT_MS = 10_000;
24
+ /** Read-only close classification: never treats an unreadable tree as empty. */
25
+ export async function classifyListedUnits(deps, units) {
26
+ const block = await readWorkspaceBlock(deps.fs, deps.paths.workspaceDir(), deps.paths.blockMarkers());
27
+ const declared = new Set();
28
+ for (const source of block?.fuentes ?? []) {
29
+ const roles = resolveSourceBranches(source, block);
30
+ for (const name of [
31
+ roles.prod,
32
+ roles.work,
33
+ block?.qa_branches[source.alias] ?? block?.default_branches.qa,
34
+ ])
35
+ if (name)
36
+ declared.add(`refs/heads/${name}`);
37
+ }
38
+ return Promise.all(units.map(async (unit) => {
39
+ const classify = (classification, reason, preservedIn) => ({
40
+ ...unit,
41
+ classification,
42
+ classification_reason: reason,
43
+ ...(preservedIn ? { preserved_in: preservedIn } : {}),
44
+ });
45
+ try {
46
+ if (unit.dirty === null || unit.head === null)
47
+ return classify("retained", "no se pudo leer HEAD o limpieza de la unidad");
48
+ if ((await deps.git.operationState(unit.path)) !== "clean")
49
+ return classify("retained", "operación git a medio resolver");
50
+ if (unit.dirty)
51
+ return classify("retained", "cambios sin commitear");
52
+ const custody = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), unit.session));
53
+ if (custody.status === "unreadable")
54
+ return classify("retained", `custodia ilegible: ${custody.reason}`);
55
+ const sealed = custody.status === "present"
56
+ ? custody.custody.sources.find((entry) => entry.alias === unit.alias)?.base_branch
57
+ : undefined;
58
+ const source = block?.fuentes.find((entry) => entry.alias === unit.alias);
59
+ const work = source ? resolveSourceBranches(source, block).work : null;
60
+ const base = sealed ?? work;
61
+ if (base === null || base === undefined)
62
+ return classify("pending", "sin base sellada ni rama de trabajo declarada");
63
+ const baseline = sealed !== undefined && custody.status === "present"
64
+ ? custody.custody.sources.find((entry) => entry.alias === unit.alias)?.baseline_head
65
+ : null;
66
+ if (baseline !== null && baseline !== undefined && unit.head === baseline)
67
+ return classify("empty", `sin commits sobre ${base}`);
68
+ const refs = await deps.git.refsContaining(unit.source_path, unit.head);
69
+ const baseRef = `refs/heads/${base}`;
70
+ const preserved = refs.includes(baseRef) && declared.has(baseRef)
71
+ ? baseRef
72
+ : refs.find((ref) => declared.has(ref) || ref.startsWith("refs/remotes/"));
73
+ if (preserved)
74
+ return {
75
+ ...classify("preserved", `commits contenidos en ${preserved}`, preserved),
76
+ integrated: preserved === baseRef,
77
+ };
78
+ return classify("pending", "commits no contenidos en una rama declarada ni remota");
79
+ }
80
+ catch (err) {
81
+ return classify("retained", `no se pudo verificar la unidad: ${err.message}`);
82
+ }
83
+ }));
84
+ }
19
85
  export async function runWorktree(deps, input) {
20
86
  if (input.action === "list")
21
87
  return listUnits(deps, input);
@@ -49,6 +115,7 @@ export async function runWorktree(deps, input) {
49
115
  async function integrateSession(deps, input) {
50
116
  const resolution = await resolveSessionTarget(deps.fs, deps.paths, {
51
117
  intent: "write",
118
+ allowClosed: input.sessionCode !== undefined,
52
119
  ...(input.sessionCode !== undefined ? { code: input.sessionCode } : {}),
53
120
  ...(input.contextId !== undefined ? { contextId: input.contextId } : {}),
54
121
  });
@@ -66,8 +133,18 @@ async function integrateSession(deps, input) {
66
133
  // branch — so this cannot take the side of a merge nobody resolved.
67
134
  const swept = await reclaimUnits(deps, { action: "reclaim", sessionCode: session });
68
135
  const residue = "error" in swept
69
- ? { reclaimed: [], retained: [], next: null }
136
+ ? {
137
+ reclaimed: [],
138
+ retained: [],
139
+ unreadable: [
140
+ { alias: "workspace", error: swept.message },
141
+ ],
142
+ next: null,
143
+ }
70
144
  : swept;
145
+ const unreadable = [
146
+ ...new Map([...listed.unreadable, ...residue.unreadable].map((item) => [item.alias, item])).values(),
147
+ ];
71
148
  // An alias whose unit was collected is no longer holding one, so it leaves
72
149
  // `pending`. That is not hiding its refusal — the refusal stays in `results` —
73
150
  // it is that a unit with nothing the working branch lacks had no merge pending
@@ -84,7 +161,12 @@ async function integrateSession(deps, input) {
84
161
  pending: holding,
85
162
  reclaimed: residue.reclaimed,
86
163
  retained: residue.retained,
87
- next: (first !== undefined ? (nextOf.get(first) ?? null) : null) ?? residue.next,
164
+ ...(unreadable.length > 0 ? { unreadable } : {}),
165
+ next: (first !== undefined ? (nextOf.get(first) ?? null) : null) ??
166
+ residue.next ??
167
+ (unreadable[0]?.code === "SOURCE_PATH_MISSING"
168
+ ? `aw add-source ${unreadable[0].alias}:<ruta>`
169
+ : null),
88
170
  };
89
171
  }
90
172
  /**
@@ -137,6 +219,8 @@ async function planOf(deps, session) {
137
219
  */
138
220
  async function integrateUnit(deps, target) {
139
221
  const { source, path, branch, base, identity } = target;
222
+ if (base === null)
223
+ return undeclaredWorkBranch(source.alias);
140
224
  if (!(await deps.git.isGitRepo(source.path))) {
141
225
  return {
142
226
  error: "not_a_repo",
@@ -151,9 +235,13 @@ async function integrateUnit(deps, target) {
151
235
  hint: `creála con 'aw worktree ensure --source ${source.alias} --code ${identity.session}'`,
152
236
  };
153
237
  }
154
- // Both preconditions are refusals BEFORE anything moves: a merge started over
155
- // uncommitted work is the one state where "report the conflict" is no longer
156
- // enough, because the losing side was never recorded anywhere.
238
+ if ((await deps.git.operationState(path)) !== "clean") {
239
+ return {
240
+ error: "unit_operation_in_progress",
241
+ message: `la unidad ${path} tiene una operación git pendiente`,
242
+ hint: `resolvé el merge con aw fix-git --path ${path} antes de integrar otra vez`,
243
+ };
244
+ }
157
245
  if (await deps.git.isDirty(path)) {
158
246
  return {
159
247
  error: "unit_not_committed",
@@ -161,47 +249,18 @@ async function integrateUnit(deps, target) {
161
249
  hint: "commiteá el trabajo del flujo en su unidad antes de integrarlo",
162
250
  };
163
251
  }
164
- if (await deps.git.isDirty(source.path)) {
252
+ const current = await deps.git.currentBranch(source.path);
253
+ if (current === base && (await deps.git.isDirty(source.path))) {
165
254
  return {
166
255
  error: "checkout_dirty",
167
256
  message: `el checkout principal de ${source.alias} tiene cambios sin commitear`,
168
257
  hint: "commiteá o guardá esos cambios: la integración no los va a mezclar con los del flujo",
169
258
  };
170
259
  }
171
- // Never switch the user's branch to make the command succeed. The declared
172
- // working branch is where the checkout is SUPPOSED to be — the git-safe
173
- // invariant says so — and a checkout that is somewhere else is a state the
174
- // person has to see, not one to be quietly corrected under a merge.
175
- const current = await deps.git.currentBranch(source.path);
176
- if (current !== base) {
177
- return {
178
- error: "checkout_off_branch",
179
- message: `el checkout principal de ${source.alias} está en '${current}' y la integración va a '${base}'`,
180
- hint: `posicioná el checkout en '${base}' y volvé a integrar; la integración nunca cambia de rama por su cuenta`,
181
- };
182
- }
183
- // The merge goes through GitPort, so it would otherwise bypass the command
184
- // filesystem guard. All refusal/precondition reads are complete above; from
185
- // here on this operation may change a source checkout.
186
260
  await ensureWorklineMaterialized(deps.fs, deps.paths);
187
- // The merge itself is the serialized part, and only it. Two runs integrating
188
- // into the same checkout would fight over one index and one MERGE_HEAD, and the
189
- // loser would find a repository mid-merge it never started. Waiting rather than
190
- // failing fast for the same reason a correlative claim waits: by the time this
191
- // lock is taken there is committed work with nowhere else to go, so losing the
192
- // race would lose real work instead of a retry.
193
- //
194
- // The pre-merge HEAD is read INSIDE the lock, next to the merge it describes.
195
- // Reading it outside would leave a window in which another integration lands
196
- // between the read and the merge, and the receipt would then attribute this
197
- // integration over a value the branch no longer had.
198
- const merged = await withCwdLock(deps.fs, deps.paths, async () => {
199
- const before = await deps.git.head(source.path).catch(() => null);
200
- const merge = await deps.git.merge(source.path, branch);
201
- if (merge.ok)
202
- return { ok: true, before };
203
- return { ok: false, conflicted: merge.conflicted };
204
- }, { waitMs: INTEGRATE_LOCK_WAIT_MS });
261
+ const merged = await withCwdLock(deps.fs, deps.paths, () => integrateLocked(deps, { ...target, base }), {
262
+ waitMs: INTEGRATE_LOCK_WAIT_MS,
263
+ });
205
264
  if ("error" in merged) {
206
265
  return {
207
266
  error: "integration_locked",
@@ -209,7 +268,9 @@ async function integrateUnit(deps, target) {
209
268
  hint: "esperá a que la otra integración termine y volvé a integrar; el merge no se empieza a medias",
210
269
  };
211
270
  }
212
- if (!merged.ok) {
271
+ if (merged.state === "refused")
272
+ return merged.refusal;
273
+ if (merged.state === "conflict") {
213
274
  return {
214
275
  alias: source.alias,
215
276
  source_path: source.path,
@@ -221,15 +282,29 @@ async function integrateUnit(deps, target) {
221
282
  // The unit SURVIVES a conflict: its commits are the only copy of one side
222
283
  // of the merge, and releasing it here would delete them to tidy up.
223
284
  released: false,
224
- next: `aw fix-git --path ${source.path}`,
285
+ next: `aw fix-git --path ${merged.path}`,
225
286
  };
226
287
  }
227
- await recordIntegration(deps, identity.session, {
228
- alias: source.alias,
229
- sourcePath: source.path,
230
- into: base,
231
- before: merged.before,
232
- });
288
+ if (merged.before !== null && merged.after !== null && merged.before !== merged.after) {
289
+ const tip = await deps.git.head(path);
290
+ if (tip === null)
291
+ return {
292
+ error: "custody_unreadable",
293
+ message: `no se pudo leer la punta de la unidad ${path} antes de liberarla`,
294
+ };
295
+ const recorded = await recordIntegration(deps, identity.session, {
296
+ alias: source.alias,
297
+ into: base,
298
+ before: merged.before,
299
+ after: merged.after,
300
+ unitTip: tip,
301
+ });
302
+ if (recorded.status === "unreadable")
303
+ return {
304
+ error: "custody_unreadable",
305
+ message: `la integración de ${source.alias} llegó a ${base}, pero no se pudo registrar la punta ${tip} en la custodia: ${recorded.reason}; la unidad conserva su rama`,
306
+ };
307
+ }
233
308
  const release = await releaseUnit(deps, target);
234
309
  return {
235
310
  alias: source.alias,
@@ -240,9 +315,100 @@ async function integrateUnit(deps, target) {
240
315
  integrated: true,
241
316
  conflicted: [],
242
317
  released: "released" in release ? release.released : false,
243
- next: null,
318
+ ...("branch_kept" in release && release.branch_kept
319
+ ? { branch_kept: release.branch_kept }
320
+ : {}),
321
+ ...("visibility_error" in release && release.visibility_error
322
+ ? { visibility_error: release.visibility_error }
323
+ : {}),
324
+ ...("visibility_error" in release && release.visibility_error
325
+ ? { visibility_error: release.visibility_error }
326
+ : {}),
327
+ next: "released" in release && release.released
328
+ ? null
329
+ : `aw worktree release --source ${source.alias} --code ${identity.session}`,
244
330
  };
245
331
  }
332
+ /** All ref reads, occupancy checks and the move happen under the integration lock. */
333
+ async function integrateLocked(deps, target) {
334
+ const { source, path, branch, base, roles } = target;
335
+ const current = await deps.git.currentBranch(source.path);
336
+ if (current === base) {
337
+ const before = await deps.git.head(source.path);
338
+ const merge = await deps.git.merge(source.path, branch);
339
+ return merge.ok
340
+ ? { state: "done", before, after: await deps.git.head(source.path) }
341
+ : { state: "conflict", conflicted: merge.conflicted, path: source.path };
342
+ }
343
+ const ref = `refs/heads/${base}`;
344
+ const before = await deps.git.refValue(source.path, ref);
345
+ if (before === null)
346
+ return {
347
+ state: "refused",
348
+ refusal: {
349
+ error: "target_missing",
350
+ message: `la rama destino '${base}' no existe en local`,
351
+ hint: `restaurá ${base} antes de integrar; la unidad queda intacta`,
352
+ },
353
+ };
354
+ const trees = await deps.git.worktreeList(source.path);
355
+ const occupied = trees.find((tree) => tree.branch === base && !samePath(tree.path, source.path));
356
+ if (occupied)
357
+ return {
358
+ state: "refused",
359
+ refusal: {
360
+ error: "target_occupied",
361
+ message: `${base} está puesta en otro árbol: ${occupied.path}`,
362
+ hint: `liberá ese árbol antes de mover la referencia ${ref}`,
363
+ },
364
+ };
365
+ let head = await deps.git.head(path);
366
+ if (head === null)
367
+ return {
368
+ state: "refused",
369
+ refusal: {
370
+ error: "unit_not_committed",
371
+ message: `la unidad ${path} no tiene HEAD`,
372
+ },
373
+ };
374
+ if (await deps.git.isAncestor(source.path, head, before)) {
375
+ return { state: "done", before, after: before };
376
+ }
377
+ if (!(await deps.git.isAncestor(source.path, before, head))) {
378
+ if (!isWorkingBranch(base, roles))
379
+ return {
380
+ state: "refused",
381
+ refusal: {
382
+ error: "checkout_off_branch",
383
+ message: `la rama de rol ${base} no admite integración divergente fuera de su checkout`,
384
+ hint: `posicioná el checkout en '${base}' y volvé a integrar`,
385
+ },
386
+ };
387
+ const merge = await deps.git.merge(path, base);
388
+ if (!merge.ok)
389
+ return { state: "conflict", conflicted: merge.conflicted, path };
390
+ head = await deps.git.head(path);
391
+ if (head === null)
392
+ return {
393
+ state: "refused",
394
+ refusal: {
395
+ error: "unit_not_committed",
396
+ message: `no se pudo leer HEAD de la unidad ${path} después del merge`,
397
+ },
398
+ };
399
+ }
400
+ const moved = await deps.git.updateRefCas(source.path, ref, head, before);
401
+ if (!moved.ok)
402
+ return {
403
+ state: "refused",
404
+ refusal: {
405
+ error: "integration_raced",
406
+ message: `la referencia ${ref} avanzó durante la integración: ${moved.why}`,
407
+ hint: `la unidad queda intacta: volvé a correr aw worktree integrate --source ${source.alias} --code ${target.identity.session}`,
408
+ },
409
+ };
410
+ return { state: "done", before, after: head };
411
+ }
246
412
  async function resolveTarget(deps, input) {
247
413
  const block = await readWorkspaceBlock(deps.fs, deps.paths.workspaceDir(), deps.paths.blockMarkers());
248
414
  const sources = block?.fuentes ?? [];
@@ -250,7 +416,7 @@ async function resolveTarget(deps, input) {
250
416
  return {
251
417
  error: "no_sources_declared",
252
418
  message: "el bloque WORKSPACE no declara ninguna fuente",
253
- hint: "declará la fuente en la tabla Fuentes antes de pedir una unidad",
419
+ hint: "declará la fuente con aw add-source <alias>:<ruta>:<rama> antes de pedir una unidad",
254
420
  };
255
421
  }
256
422
  if (input.alias === undefined) {
@@ -268,8 +434,16 @@ async function resolveTarget(deps, input) {
268
434
  hint: `fuentes declaradas: ${sources.map((s) => s.alias).join(", ")}`,
269
435
  };
270
436
  }
437
+ let sourcePath;
438
+ try {
439
+ sourcePath = await requireSourcePath(deps.fs, source);
440
+ }
441
+ catch (err) {
442
+ return { error: "SOURCE_PATH_MISSING", message: err.message };
443
+ }
271
444
  const resolution = await resolveSessionTarget(deps.fs, deps.paths, {
272
445
  intent: "write",
446
+ allowClosed: input.action !== "ensure" && input.sessionCode !== undefined,
273
447
  ...(input.sessionCode !== undefined ? { code: input.sessionCode } : {}),
274
448
  ...(input.contextId !== undefined ? { contextId: input.contextId } : {}),
275
449
  });
@@ -281,12 +455,58 @@ async function resolveTarget(deps, input) {
281
455
  alias: source.alias,
282
456
  session,
283
457
  };
458
+ const roles = resolveSourceBranches(source, block);
459
+ const custody = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), session));
460
+ if (custody.status === "unreadable")
461
+ return {
462
+ error: "custody_unreadable",
463
+ message: `la custodia de ${session} no se puede leer: ${custody.reason}`,
464
+ };
465
+ const sealed = custody.status === "present"
466
+ ? custody.custody.sources.find((s) => s.alias === source.alias)?.base_branch
467
+ : undefined;
468
+ const recorded = custody.status === "present"
469
+ ? custody.custody.sources.find((s) => s.alias === source.alias)
470
+ : undefined;
471
+ if (recorded?.unit_path) {
472
+ const oldIdentity = parseUnitPath(await canonicalUnitsRootForRead(deps), recorded.unit_path);
473
+ if (oldIdentity?.alias === source.alias && oldIdentity.session === session)
474
+ identity.workspaceKey = oldIdentity.workspaceKey;
475
+ }
476
+ let previous;
477
+ if (custody.status === "absent") {
478
+ const root = await canonicalUnitsRootForRead(deps);
479
+ const owns = await hubUnitPaths(deps.fs, deps.paths, root);
480
+ try {
481
+ previous = (await deps.git.worktreeList(sourcePath)).find((tree) => {
482
+ const found = parseUnitPath(root, tree.path);
483
+ return found?.session === session && found.alias === source.alias && owns(tree.path);
484
+ });
485
+ }
486
+ catch {
487
+ /* a broken git inventory is reported by ensure/list itself */
488
+ }
489
+ if (previous) {
490
+ const found = parseUnitPath(root, previous.path);
491
+ if (found)
492
+ identity.workspaceKey = found.workspaceKey;
493
+ }
494
+ }
495
+ const effective = sealed === undefined
496
+ ? await resolveDocBranch(deps.fs, deps.paths, source, block, await documentOfSession(deps.fs, deps.paths, session))
497
+ : null;
498
+ if (effective?.origin === "unreadable")
499
+ return {
500
+ error: "custody_unreadable",
501
+ message: effective.reason ?? "custodia ilegible",
502
+ };
284
503
  return {
285
- source,
504
+ source: { ...source, path: sourcePath },
286
505
  identity,
287
- path: unitPath(await canonicalUnitsRoot(deps), identity),
288
- branch: unitBranch(session),
289
- base: resolveSourceBranches(source, block).work,
506
+ path: recorded?.unit_path ?? previous?.path ?? unitPath(await canonicalUnitsRoot(deps), identity),
507
+ branch: recorded?.unit_branch ?? previous?.branch ?? unitBranch(session, deps.paths.workspaceDir()),
508
+ base: sealed ?? effective?.branch ?? null,
509
+ roles,
290
510
  };
291
511
  }
292
512
  /**
@@ -306,6 +526,13 @@ function sessionRefusal(resolution) {
306
526
  hint: resolution.action,
307
527
  };
308
528
  }
529
+ function undeclaredWorkBranch(alias) {
530
+ return {
531
+ error: "working_branch_undeclared",
532
+ message: `rama de trabajo no declarada para ${alias}`,
533
+ hint: `usá 'aw set-working-branch ${alias} <rama>'`,
534
+ };
535
+ }
309
536
  /**
310
537
  * The units root in the OS's own spelling.
311
538
  *
@@ -346,6 +573,7 @@ async function ensureUnit(deps, target) {
346
573
  message: `${source.alias} (${source.path}) no es un repositorio git`,
347
574
  };
348
575
  }
576
+ const longpathsEnabled = await enableLongPaths(deps, source.path);
349
577
  // Vanished directories keep holding their branch until git is told, so the
350
578
  // prune runs BEFORE the occupancy read — otherwise a unit whose folder the
351
579
  // user deleted by hand would look occupied forever.
@@ -353,17 +581,47 @@ async function ensureUnit(deps, target) {
353
581
  // root, so materialize explicitly before this first Git mutation rather than
354
582
  // relying on a workspace-path filesystem write to notice it.
355
583
  await ensureWorklineMaterialized(deps.fs, deps.paths);
584
+ const unitsRoot = await canonicalUnitsRootForRead(deps);
585
+ const owns = await hubUnitPaths(deps.fs, deps.paths, unitsRoot);
586
+ const staleErrors = [];
587
+ for (const tree of await deps.git.worktreeList(source.path)) {
588
+ if (!tree.prunable || !owns(tree.path) || (await deps.fs.lstat(tree.path)) !== null)
589
+ continue;
590
+ const result = await detach(deps, tree.path);
591
+ if (result.error)
592
+ staleErrors.push(`${tree.path}: ${result.error}`);
593
+ }
356
594
  await deps.git.worktreePrune(source.path);
357
595
  const existing = await deps.git.worktreeList(source.path);
358
596
  const mine = existing.find((w) => samePath(w.path, path));
359
597
  if (mine !== undefined) {
598
+ const custody = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), target.identity.session));
599
+ const recorded = custody.status === "present"
600
+ ? custody.custody.sources.find((s) => s.alias === source.alias)
601
+ : undefined;
602
+ if (custody.status === "present" &&
603
+ (recorded?.unit_path !== path || recorded.unit_branch !== branch || mine.branch !== branch)) {
604
+ return {
605
+ error: "unit_foreign",
606
+ message: `La unidad ${path} no está registrada en la custodia de esta sesión.`,
607
+ };
608
+ }
360
609
  // Idempotent: the unit is already there, on its own branch. The baseline is
361
610
  // sealed here too — it is idempotent by alias, so the FIRST reading wins and a
362
611
  // second `ensure` can never overwrite it with a state the session produced.
363
- const sealed = await sealBaseline(deps, target);
612
+ const sealed = base === null ? null : await sealBaseline(deps, { ...target, base });
364
613
  if (sealed !== null)
365
614
  return sealed;
366
- return { ...unitOf(target, false), visibility: await attach(deps, path) };
615
+ const visible = await attach(deps, path);
616
+ const visibilityError = [...staleErrors, visible.error].filter(Boolean).join("; ");
617
+ return {
618
+ ...unitOf(target, false),
619
+ base,
620
+ visibility: visible.state,
621
+ ...(visibilityError ? { visibility_error: visibilityError } : {}),
622
+ longpaths_enabled: longpathsEnabled,
623
+ dependencies: await linkUnitDependencies(deps.fs, deps.git, source.path, path),
624
+ };
367
625
  }
368
626
  const occupant = existing.find((w) => w.branch === branch);
369
627
  if (occupant !== undefined) {
@@ -378,7 +636,29 @@ async function ensureUnit(deps, target) {
378
636
  // An existing branch is checked out, not recreated: a flow that released its
379
637
  // unit and asks again must land back on its own commits, not on a fresh branch
380
638
  // that silently drops them.
381
- const from = (await deps.git.branchExists(source.path, branch)) ? null : base;
639
+ const exists = await deps.git.branchExists(source.path, branch);
640
+ const custody = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), target.identity.session));
641
+ const recorded = custody.status === "present"
642
+ ? custody.custody.sources.find((s) => s.alias === source.alias)
643
+ : undefined;
644
+ if (exists &&
645
+ custody.status === "present" &&
646
+ (recorded?.unit_branch !== branch || recorded.unit_path !== path)) {
647
+ return {
648
+ error: "unit_foreign",
649
+ message: `La rama ${branch} no está registrada en la custodia de esta sesión.`,
650
+ };
651
+ }
652
+ const from = exists ? null : base;
653
+ if (from === null && !exists)
654
+ return undeclaredWorkBranch(source.alias);
655
+ if (from !== null && !(await deps.git.branchExists(source.path, from))) {
656
+ return {
657
+ error: "base_missing",
658
+ message: `la rama base '${from}' no existe en ${source.alias}`,
659
+ hint: `restaurá '${from}' antes de abrir la unidad`,
660
+ };
661
+ }
382
662
  try {
383
663
  await deps.git.worktreeAdd(source.path, path, branch, from);
384
664
  }
@@ -386,13 +666,31 @@ async function ensureUnit(deps, target) {
386
666
  return {
387
667
  error: "worktree_add_failed",
388
668
  message: err.message,
389
- hint: `verificá que la rama base '${base}' exista en ${source.alias}`,
390
669
  };
391
670
  }
392
- const sealed = await sealBaseline(deps, target);
671
+ const sealed = base === null ? null : await sealBaseline(deps, { ...target, base });
393
672
  if (sealed !== null)
394
673
  return sealed;
395
- return { ...unitOf(target, true), visibility: await attach(deps, path) };
674
+ const visible = await attach(deps, path);
675
+ const visibilityError = [...staleErrors, visible.error].filter(Boolean).join("; ");
676
+ return {
677
+ ...unitOf(target, true),
678
+ base,
679
+ visibility: visible.state,
680
+ ...(visibilityError ? { visibility_error: visibilityError } : {}),
681
+ longpaths_enabled: longpathsEnabled,
682
+ dependencies: await linkUnitDependencies(deps.fs, deps.git, source.path, path),
683
+ };
684
+ }
685
+ async function enableLongPaths(deps, repo) {
686
+ if ((deps.platform ?? process.platform) !== "win32")
687
+ return false;
688
+ if (!deps.git.readConfig || !deps.git.writeLocalConfig)
689
+ throw new Error("git no permite configurar core.longpaths localmente");
690
+ if ((await deps.git.readConfig(repo, "core.longpaths"))?.toLowerCase() === "true")
691
+ return false;
692
+ await deps.git.writeLocalConfig(repo, "core.longpaths", "true");
693
+ return true;
396
694
  }
397
695
  /**
398
696
  * Seal how the source stood before this unit gets edited, or refuse the unit.
@@ -430,38 +728,105 @@ async function releaseUnit(deps, target) {
430
728
  };
431
729
  }
432
730
  const existing = await deps.git.worktreeList(source.path);
433
- if (!existing.some((w) => samePath(w.path, path))) {
731
+ const registered = existing.find((w) => samePath(w.path, path));
732
+ if (registered && (await deps.fs.lstat(path)) === null) {
733
+ await deps.git.worktreePrune(source.path);
734
+ if ((await deps.git.worktreeList(source.path)).some((w) => samePath(w.path, path)))
735
+ return { error: "remove_blocked", message: `git aún registra la unidad sin carpeta ${path}` };
736
+ return releaseUnit(deps, target);
737
+ }
738
+ if (registered === undefined) {
434
739
  await ensureWorklineMaterialized(deps.fs, deps.paths);
435
740
  await deps.git.worktreePrune(source.path);
741
+ let completed = false;
742
+ if ((await deps.fs.lstat(path)) !== null) {
743
+ try {
744
+ const blockers = await finishResidue(deps.fs, deps.git, source.path, path, branch);
745
+ if (blockers.length > 0)
746
+ return {
747
+ error: "remove_blocked",
748
+ message: `quedó residuo en ${path}: ${blockers.join(", ")}`,
749
+ hint: `revisá los archivos nombrados y volvé a correr 'aw worktree release --source ${source.alias} --code ${identity.session}'`,
750
+ };
751
+ completed = true;
752
+ }
753
+ catch (err) {
754
+ return {
755
+ error: "remove_blocked",
756
+ message: `quedó residuo en ${path}: ${err.message}`,
757
+ };
758
+ }
759
+ }
760
+ const branchKept = await deleteContainedUnitBranch(deps, target);
761
+ const visible = await detach(deps, path);
436
762
  return {
437
763
  alias: source.alias,
438
764
  session: identity.session,
439
765
  path,
440
766
  branch,
441
- released: false,
442
- visibility: await detach(deps, path),
767
+ released: completed,
768
+ residue_completed: completed,
769
+ ...(branchKept ? { branch_kept: branchKept } : {}),
770
+ visibility: visible.state,
771
+ ...(visible.error ? { visibility_error: visible.error } : {}),
443
772
  };
444
773
  }
774
+ if (registered.branch !== branch)
775
+ return {
776
+ error: "unit_foreign",
777
+ message: `la unidad ${path} usa ${registered.branch}, no ${branch}`,
778
+ };
445
779
  await ensureWorklineMaterialized(deps.fs, deps.paths);
446
780
  try {
447
- await deps.git.worktreeRemove(source.path, path);
781
+ await removeUnitSafely(deps.fs, deps.git, source.path, path);
448
782
  }
449
783
  catch (err) {
784
+ const now = await deps.git.worktreeList(source.path);
785
+ if (!now.some((w) => samePath(w.path, path)))
786
+ return releaseUnit(deps, target);
450
787
  return {
451
- error: "unit_not_clean",
452
- message: err.message,
453
- hint: "commiteá o descartá los cambios de la unidad y volvé a liberarla; nada se borra por la fuerza",
788
+ error: (await deps.git.isDirty(path)) ? "unit_not_clean" : "remove_blocked",
789
+ message: `quedó ${path}: ${err.message}`,
790
+ hint: `revisá ${path} y volvé a correr 'aw worktree release --source ${source.alias} --code ${identity.session}'`,
454
791
  };
455
792
  }
793
+ if ((await deps.fs.lstat(path)) !== null) {
794
+ const blockers = await finishResidue(deps.fs, deps.git, source.path, path, branch);
795
+ if (blockers.length > 0)
796
+ return { error: "remove_blocked", message: `quedó ${path}: ${blockers.join(", ")}` };
797
+ }
798
+ const branchKept = await deleteContainedUnitBranch(deps, target);
799
+ const visible = await detach(deps, path);
456
800
  return {
457
801
  alias: source.alias,
458
802
  session: identity.session,
459
803
  path,
460
804
  branch,
461
805
  released: true,
462
- visibility: await detach(deps, path),
806
+ ...(branchKept ? { branch_kept: branchKept } : {}),
807
+ visibility: visible.state,
808
+ ...(visible.error ? { visibility_error: visible.error } : {}),
463
809
  };
464
810
  }
811
+ /** Only a unit branch, only with a sealed/effective base, only by the observed tip. */
812
+ async function deleteContainedUnitBranch(deps, target) {
813
+ const { source, path, branch, base } = target;
814
+ if (!branch.startsWith("aw/") || branch === base)
815
+ return "no es una rama de unidad borrable";
816
+ if ((await deps.fs.lstat(path)) !== null ||
817
+ (await deps.git.worktreeList(source.path)).some((tree) => tree.branch === branch))
818
+ return "la carpeta o la rama siguen ocupadas; no se borra";
819
+ const tip = await deps.git.refValue(source.path, `refs/heads/${branch}`);
820
+ if (tip === null)
821
+ return null;
822
+ if (base === null)
823
+ return "sin base sellada o rama de trabajo; la rama se conserva";
824
+ const baseTip = await deps.git.refValue(source.path, `refs/heads/${base}`);
825
+ if (baseTip === null || !(await deps.git.isAncestor(source.path, tip, baseTip)))
826
+ return `commits de ${branch} no contenidos en ${base}; la rama se conserva`;
827
+ const deleted = await deps.git.deleteRef(source.path, `refs/heads/${branch}`, tip);
828
+ return deleted.ok ? null : `no se pudo borrar ${branch} con punta ${tip}: ${deleted.why}`;
829
+ }
465
830
  /**
466
831
  * Collect the residue: one act over the whole workspace, or over one session.
467
832
  *
@@ -485,7 +850,7 @@ async function reclaimUnits(deps, input) {
485
850
  return {
486
851
  error: "no_sources_declared",
487
852
  message: "el bloque WORKSPACE no declara ninguna fuente",
488
- hint: "declará la fuente en la tabla Fuentes antes de pedir una recogida",
853
+ hint: "declará la fuente con aw add-source <alias>:<ruta>:<rama> antes de pedir una recogida",
489
854
  };
490
855
  }
491
856
  if (input.alias !== undefined && !sources.some((s) => s.alias === input.alias)) {
@@ -498,6 +863,7 @@ async function reclaimUnits(deps, input) {
498
863
  const key = workspaceKey(deps.paths.workspaceDir());
499
864
  const root = await canonicalUnitsRootForRead(deps);
500
865
  const sessions = await sessionStates(deps);
866
+ const owns = await hubUnitPaths(deps.fs, deps.paths, root);
501
867
  const only = input.sessionCode ?? null;
502
868
  const reclaimed = [];
503
869
  const retained = [];
@@ -505,9 +871,13 @@ async function reclaimUnits(deps, input) {
505
871
  for (const source of sources) {
506
872
  if (input.alias !== undefined && source.alias !== input.alias)
507
873
  continue;
508
- const swept = await sweepSource(deps, source, { root, key, only, sessions, block });
874
+ const swept = await sweepSource(deps, source, { root, key, only, sessions, block, owns });
509
875
  if ("error" in swept) {
510
- unreadable.push({ alias: source.alias, error: swept.error });
876
+ unreadable.push({
877
+ alias: source.alias,
878
+ error: swept.error,
879
+ ...(swept.code ? { code: swept.code } : {}),
880
+ });
511
881
  continue;
512
882
  }
513
883
  reclaimed.push(...swept.reclaimed);
@@ -519,21 +889,30 @@ async function reclaimUnits(deps, input) {
519
889
  reclaimed,
520
890
  retained,
521
891
  unreadable,
522
- next: retained[0]?.next ?? null,
892
+ next: retained[0]?.next ??
893
+ (unreadable[0]?.code === "SOURCE_PATH_MISSING"
894
+ ? `aw add-source ${unreadable[0].alias}:<ruta>`
895
+ : null),
523
896
  };
524
897
  }
525
898
  /** What one source's residue produced, or why its trees could not be read. */
526
899
  async function sweepSource(deps, source, ctx) {
527
- if (!(await deps.git.isGitRepo(source.path)))
900
+ let repo;
901
+ try {
902
+ repo = await requireSourcePath(deps.fs, source);
903
+ }
904
+ catch (err) {
905
+ return { error: err.message, code: "SOURCE_PATH_MISSING" };
906
+ }
907
+ if (!(await deps.git.isGitRepo(repo)))
528
908
  return { reclaimed: [], retained: [] };
529
909
  let trees;
530
910
  try {
531
- trees = await deps.git.worktreeList(source.path);
911
+ trees = await deps.git.worktreeList(repo);
532
912
  }
533
913
  catch (err) {
534
914
  return { error: err.message };
535
915
  }
536
- const work = resolveSourceBranches(source, ctx.block).work;
537
916
  const reclaimed = [];
538
917
  const retained = [];
539
918
  const vanished = [];
@@ -541,7 +920,8 @@ async function sweepSource(deps, source, ctx) {
541
920
  const candidate = candidateOf(tree, ctx);
542
921
  if (candidate === null)
543
922
  continue;
544
- const swept = await sweepOne(deps, source, tree, work, candidate);
923
+ const work = await baseForReclaim(deps, source.alias, candidate.session, resolveSourceBranches(source, ctx.block).work);
924
+ const swept = await sweepOne(deps, { ...source, path: repo }, tree, work, candidate);
545
925
  if ("retained" in swept) {
546
926
  retained.push(swept.retained);
547
927
  continue;
@@ -555,18 +935,124 @@ async function sweepSource(deps, source, ctx) {
555
935
  reclaimed.push(swept.reclaimed);
556
936
  }
557
937
  if (vanished.length > 0) {
558
- const settled = await prune(deps, source, vanished);
938
+ const settled = await prune(deps, { ...source, path: repo }, vanished, ctx.block);
559
939
  reclaimed.push(...settled.reclaimed);
560
940
  retained.push(...settled.retained);
561
941
  }
942
+ const registered = new Set(trees.map((tree) => normalizePath(tree.path)));
943
+ // Also visit previous workspace keys registered by 075, without claiming another hub's units.
944
+ for (const keyEntry of (await deps.fs.lstat(ctx.root)) === null
945
+ ? []
946
+ : await deps.fs.list(ctx.root)) {
947
+ if (keyEntry.type !== "dir")
948
+ continue;
949
+ const aliasDir = join(keyEntry.path, source.alias);
950
+ if ((await deps.fs.lstat(aliasDir))?.type !== "dir")
951
+ continue;
952
+ for (const sessionEntry of await deps.fs.list(aliasDir)) {
953
+ if (sessionEntry.type !== "dir" ||
954
+ registered.has(normalizePath(sessionEntry.path)) ||
955
+ !ctx.owns(sessionEntry.path))
956
+ continue;
957
+ const identity = parseUnitPath(ctx.root, sessionEntry.path);
958
+ if (identity?.alias !== source.alias ||
959
+ (ctx.only !== null && !sessionFolderMatches(identity.session, ctx.only)))
960
+ continue;
961
+ const orphan = orphanReason(identity.session, ctx.sessions, false);
962
+ if (orphan === null && ctx.only === null)
963
+ continue;
964
+ const branch = await branchForResidue(deps, repo, identity, sessionEntry.path);
965
+ const where = {
966
+ alias: source.alias,
967
+ session: identity.session,
968
+ path: sessionEntry.path,
969
+ branch,
970
+ };
971
+ try {
972
+ const blockers = await finishResidue(deps.fs, deps.git, repo, sessionEntry.path, branch);
973
+ if (blockers.length > 0) {
974
+ retained.push({
975
+ ...where,
976
+ reason: "archivos_no_recuperables",
977
+ detail: blockers.join(", "),
978
+ next: `revisá ${sessionEntry.path} y volvé a correr 'aw worktree reclaim --code ${identity.session}'`,
979
+ });
980
+ continue;
981
+ }
982
+ await deps.git.worktreePrune(repo);
983
+ const base = await baseForReclaim(deps, source.alias, identity.session, resolveSourceBranches(source, ctx.block).work);
984
+ const kept = await deleteContainedUnitBranch(deps, {
985
+ source: { ...source, path: repo },
986
+ path: sessionEntry.path,
987
+ branch: branch ?? "",
988
+ base,
989
+ });
990
+ const visible = await detach(deps, sessionEntry.path);
991
+ reclaimed.push({
992
+ ...where,
993
+ reason: orphan ?? "already_on_work_branch",
994
+ ...(kept ? { branch_kept: kept } : {}),
995
+ ...(visible.error ? { visibility_error: visible.error } : {}),
996
+ });
997
+ }
998
+ catch (err) {
999
+ retained.push({
1000
+ ...where,
1001
+ reason: "unreadable",
1002
+ detail: err.message,
1003
+ next: `revisá ${sessionEntry.path} antes de recoger`,
1004
+ });
1005
+ }
1006
+ }
1007
+ }
562
1008
  return { reclaimed, retained };
563
1009
  }
1010
+ async function branchForResidue(deps, repo, identity, path) {
1011
+ const custody = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), identity.session));
1012
+ const recorded = custody.status === "present"
1013
+ ? custody.custody.sources.find((s) => s.alias === identity.alias && s.unit_path === path)
1014
+ : undefined;
1015
+ if (recorded?.unit_branch)
1016
+ return recorded.unit_branch;
1017
+ const names = [
1018
+ `aw/${identity.workspaceKey.slice(-8)}/${identity.session}`,
1019
+ unitBranch(identity.session),
1020
+ ];
1021
+ for (const branch of names)
1022
+ if (await deps.git.branchExists(repo, branch))
1023
+ return branch;
1024
+ return null;
1025
+ }
1026
+ async function baseForReclaim(deps, alias, session, fallback) {
1027
+ const read = await readCustody(deps.fs, join(deps.paths.cwdSessionsDir(), session));
1028
+ if (read.status === "unreadable")
1029
+ return null;
1030
+ return read.status === "present"
1031
+ ? (read.custody.sources.find((source) => source.alias === alias)?.base_branch ?? fallback)
1032
+ : fallback;
1033
+ }
564
1034
  /** The vanished trees git dropped, or the same ones still listed and why. */
565
- async function prune(deps, source, vanished) {
1035
+ async function prune(deps, source, vanished, block) {
566
1036
  await ensureWorklineMaterialized(deps.fs, deps.paths);
567
1037
  try {
568
1038
  await deps.git.worktreePrune(source.path);
569
- return { reclaimed: vanished, retained: [] };
1039
+ const reclaimed = [];
1040
+ for (const unit of vanished) {
1041
+ const base = await baseForReclaim(deps, unit.alias, unit.session, resolveSourceBranches(source, block).work);
1042
+ const kept = await deleteContainedUnitBranch(deps, {
1043
+ source,
1044
+ path: unit.path,
1045
+ branch: unit.branch ?? "",
1046
+ base,
1047
+ });
1048
+ const visible = await detach(deps, unit.path);
1049
+ reclaimed.push({
1050
+ ...unit,
1051
+ ...(kept ? { branch_kept: kept } : {}),
1052
+ ...(visible.error ? { visibility_error: visible.error } : {}),
1053
+ });
1054
+ }
1055
+ return { reclaimed, retained: [] };
570
1056
  }
571
1057
  catch (err) {
572
1058
  return {
@@ -586,7 +1072,7 @@ async function prune(deps, source, vanished) {
586
1072
  /** The identity and residue state of a tree this sweep may act on; `null` otherwise. */
587
1073
  function candidateOf(tree, ctx) {
588
1074
  const identity = tree.main ? null : parseUnitPath(ctx.root, tree.path);
589
- if (identity === null || identity.workspaceKey !== ctx.key)
1075
+ if (identity === null || !ctx.owns(tree.path))
590
1076
  return null;
591
1077
  if (ctx.only !== null && !sessionFolderMatches(identity.session, ctx.only))
592
1078
  return null;
@@ -612,28 +1098,72 @@ async function sweepOne(deps, source, tree, work, candidate) {
612
1098
  // was, so there is no work this can lose.
613
1099
  return { reclaimed: { ...where, reason: candidate.orphan }, prune: true };
614
1100
  }
1101
+ if (work === null)
1102
+ return {
1103
+ retained: {
1104
+ ...where,
1105
+ reason: "unreadable",
1106
+ detail: `no se pudo determinar la base sellada ni una rama de trabajo para ${source.alias}`,
1107
+ next: `revisá la custodia de ${candidate.session} o usá 'aw set-working-branch ${source.alias} <rama>' antes de recoger`,
1108
+ },
1109
+ };
615
1110
  const verdict = await reclaimability(deps, source, tree, work, candidate.orphan, candidate.session);
616
1111
  if (!verdict.free)
617
1112
  return { retained: { ...where, ...verdict.retention } };
618
1113
  await ensureWorklineMaterialized(deps.fs, deps.paths);
619
1114
  try {
620
- await deps.git.worktreeRemove(source.path, tree.path);
1115
+ await removeUnitSafely(deps.fs, deps.git, source.path, tree.path);
621
1116
  }
622
1117
  catch (err) {
623
- // git refused, and git refusing is the last line of the same rule: never
624
- // `--force`, so whatever it is holding stays held.
625
- return {
626
- retained: {
627
- ...where,
628
- reason: "remove_refused",
629
- detail: err.message,
630
- next: `revisá ${tree.path} y volvé a correr 'aw worktree reclaim'; nada se borra por la fuerza`,
631
- },
632
- };
1118
+ if ((await deps.git.worktreeList(source.path)).some((w) => samePath(w.path, tree.path)))
1119
+ return {
1120
+ retained: {
1121
+ ...where,
1122
+ reason: "remove_refused",
1123
+ detail: err.message,
1124
+ next: `revisá ${tree.path} y volvé a correr 'aw worktree reclaim'; nada se borra por la fuerza`,
1125
+ },
1126
+ };
1127
+ }
1128
+ if ((await deps.fs.lstat(tree.path)) !== null) {
1129
+ try {
1130
+ const blockers = await finishResidue(deps.fs, deps.git, source.path, tree.path, tree.branch);
1131
+ if (blockers.length > 0)
1132
+ return {
1133
+ retained: {
1134
+ ...where,
1135
+ reason: "archivos_no_recuperables",
1136
+ detail: blockers.join(", "),
1137
+ next: `revisá ${tree.path} y volvé a recoger`,
1138
+ },
1139
+ };
1140
+ }
1141
+ catch (err) {
1142
+ return {
1143
+ retained: {
1144
+ ...where,
1145
+ reason: "remove_refused",
1146
+ detail: err.message,
1147
+ next: `revisá ${tree.path} y volvé a recoger`,
1148
+ },
1149
+ };
1150
+ }
633
1151
  }
634
- await detach(deps, tree.path);
1152
+ await deps.git.worktreePrune(source.path);
1153
+ const branchKept = await deleteContainedUnitBranch(deps, {
1154
+ source,
1155
+ path: tree.path,
1156
+ branch: tree.branch ?? "",
1157
+ base: work,
1158
+ });
1159
+ const visible = await detach(deps, tree.path);
635
1160
  return {
636
- reclaimed: { ...where, reason: candidate.orphan ?? "already_on_work_branch" },
1161
+ reclaimed: {
1162
+ ...where,
1163
+ reason: candidate.orphan ?? "already_on_work_branch",
1164
+ ...(branchKept ? { branch_kept: branchKept } : {}),
1165
+ ...(visible.error ? { visibility_error: visible.error } : {}),
1166
+ },
637
1167
  prune: false,
638
1168
  };
639
1169
  }
@@ -675,6 +1205,9 @@ async function reclaimability(deps, source, tree, work, orphan, session) {
675
1205
  if (tree.head === null) {
676
1206
  return held("unreadable", "git no reportó el HEAD de la unidad", `revisá ${tree.path} a mano: sin HEAD no se puede decidir si sus commits ya están en '${work}'`);
677
1207
  }
1208
+ if ((await deps.git.refValue(source.path, `refs/heads/${work}`)) === null) {
1209
+ return held("unreadable", `la base sellada '${work}' ya no existe en local`, `restaurá '${work}' antes de recoger la unidad ${tree.path}`);
1210
+ }
678
1211
  if (!(await deps.git.isAncestor(source.path, tree.head, work))) {
679
1212
  return held("commits_outside_work_branch", `la unidad tiene commits que no están en '${work}'`, recoveryFor(source, tree, orphan, work, session));
680
1213
  }
@@ -683,7 +1216,7 @@ async function reclaimability(deps, source, tree, work, orphan, session) {
683
1216
  /** How the work of a unit that is not on the working branch gets back onto it. */
684
1217
  function recoveryFor(source, tree, orphan, work, session) {
685
1218
  if (orphan === "session_closed") {
686
- return `aw session-resume --code ${session} --reopen y después 'aw worktree integrate --source ${source.alias} --code ${session}'`;
1219
+ return `aw worktree integrate --source ${source.alias} --code ${session}`;
687
1220
  }
688
1221
  if (orphan === "session_absent") {
689
1222
  return `la sesión ${session} ya no existe: revisá la rama ${tree.branch ?? unitBranch(session)} en ${source.path} y llevá sus commits a '${work}' antes de volver a recoger`;
@@ -713,16 +1246,21 @@ async function listUnits(deps, input) {
713
1246
  const key = workspaceKey(deps.paths.workspaceDir());
714
1247
  const root = await canonicalUnitsRootForRead(deps);
715
1248
  const sessions = await sessionStates(deps);
1249
+ const owns = await hubUnitPaths(deps.fs, deps.paths, root);
716
1250
  const units = [];
717
1251
  const orphans = [];
718
1252
  const unreadable = [];
719
1253
  for (const source of block?.fuentes ?? []) {
720
- const scanned = await scanSource(deps, source, { root, key, only, sessions });
1254
+ const scanned = await scanSource(deps, source, { root, key, only, sessions, owns });
721
1255
  if ("error" in scanned) {
722
1256
  // Reported, never skipped in silence: a source whose trees cannot be read
723
1257
  // would otherwise show up as "no units", which is the one answer that is
724
1258
  // certainly wrong — its flows are exactly the ones nobody would clean up.
725
- unreadable.push({ alias: source.alias, error: scanned.error });
1259
+ unreadable.push({
1260
+ alias: source.alias,
1261
+ error: scanned.error,
1262
+ ...(scanned.code ? { code: scanned.code } : {}),
1263
+ });
726
1264
  continue;
727
1265
  }
728
1266
  units.push(...scanned.units);
@@ -741,11 +1279,18 @@ async function scanSource(deps, source, ctx) {
741
1279
  const empty = { units: [], orphans: [] };
742
1280
  // Not a repo is not unreadable: it has no worktrees to report, and calling it
743
1281
  // an error would put every non-git source in front of the reader forever.
744
- if (!(await deps.git.isGitRepo(source.path)))
1282
+ let repo;
1283
+ try {
1284
+ repo = await requireSourcePath(deps.fs, source);
1285
+ }
1286
+ catch (err) {
1287
+ return { error: err.message, code: "SOURCE_PATH_MISSING" };
1288
+ }
1289
+ if (!(await deps.git.isGitRepo(repo)))
745
1290
  return empty;
746
1291
  let trees;
747
1292
  try {
748
- trees = await deps.git.worktreeList(source.path);
1293
+ trees = await deps.git.worktreeList(repo);
749
1294
  }
750
1295
  catch (err) {
751
1296
  return { error: err.message };
@@ -754,13 +1299,13 @@ async function scanSource(deps, source, ctx) {
754
1299
  const orphans = [];
755
1300
  for (const tree of trees) {
756
1301
  const identity = tree.main ? null : parseUnitPath(ctx.root, tree.path);
757
- if (identity === null || identity.workspaceKey !== ctx.key)
1302
+ if (identity === null || !ctx.owns(tree.path))
758
1303
  continue;
759
1304
  if (ctx.only !== null && identity.session !== ctx.only)
760
1305
  continue;
761
1306
  const reason = orphanReason(identity.session, ctx.sessions, tree.prunable);
762
1307
  if (reason === null)
763
- units.push(await liveUnit(deps, identity, source.path, tree));
1308
+ units.push(await liveUnit(deps, identity, repo, tree));
764
1309
  else
765
1310
  orphans.push(orphanOf(identity, tree, reason));
766
1311
  }
@@ -825,7 +1370,7 @@ async function sessionStates(deps) {
825
1370
  for (const folder of folders) {
826
1371
  // `.closed` is the only source of a session's state (the session resolver's
827
1372
  // own rule); re-deriving it from anything else would let the two disagree.
828
- if (!(await deps.fs.exists(join(folder.path, ".closed"))))
1373
+ if ((await readSessionState(deps.fs, folder.path)) === "active")
829
1374
  active.add(folder.name);
830
1375
  }
831
1376
  return { known: new Set(folders.map((f) => f.name)), active };
@@ -860,11 +1405,15 @@ function unitOf(target, created) {
860
1405
  */
861
1406
  async function attach(deps, path) {
862
1407
  const result = await runMultiroot(deps.fs, deps.env, deps.paths, "attach", { paths: [path] });
863
- return "error" in result ? "unavailable" : "attached";
1408
+ return "error" in result
1409
+ ? { state: "unavailable", error: `${result.error}: ${result.hint ?? "sin detalle"}` }
1410
+ : { state: "attached" };
864
1411
  }
865
1412
  async function detach(deps, path) {
866
1413
  const result = await runMultiroot(deps.fs, deps.env, deps.paths, "detach", { paths: [path] });
867
- return "error" in result ? "unavailable" : "detached";
1414
+ return "error" in result
1415
+ ? { state: "unavailable", error: `${result.error}: ${result.hint ?? "sin detalle"}` }
1416
+ : { state: "detached" };
868
1417
  }
869
1418
  function samePath(a, b) {
870
1419
  return normalizePath(a) === normalizePath(b);