@fitlab-ai/agent-infra 0.11.15 → 0.12.1

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 (265) hide show
  1. package/bin/cli.ts +1 -1
  2. package/dist/bin/cli.js +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +6 -6
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +8 -18
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -1
  7. package/dist/lib/decide.js +2 -7
  8. package/dist/lib/internal/cli-route-inventory.js +3 -0
  9. package/dist/lib/internal/manual-validation.js +19 -33
  10. package/dist/lib/internal/platform-comment.js +59 -9
  11. package/dist/lib/internal/task-activity.js +1 -12
  12. package/dist/lib/internal/task-event.js +1 -12
  13. package/dist/lib/internal/task-ledger.js +1 -12
  14. package/dist/lib/internal/task-operation-registry.js +2 -1
  15. package/dist/lib/internal/task-qualification.js +6 -15
  16. package/dist/lib/internal/task-review.js +3 -16
  17. package/dist/lib/internal/task-short-id.js +2 -1
  18. package/dist/lib/internal/task-verify.js +1 -11
  19. package/dist/lib/internal/task-warning.js +1 -12
  20. package/dist/lib/internal/task-workspace.js +43 -0
  21. package/dist/lib/merge.js +4 -3
  22. package/dist/lib/platform/issue-comments.js +373 -267
  23. package/dist/lib/platform/issue-summary-content.js +112 -0
  24. package/dist/lib/platform/pr-change-report.js +175 -26
  25. package/dist/lib/platform/pr-summary-content.js +54 -0
  26. package/dist/lib/platform/pr-summary.js +321 -296
  27. package/dist/lib/platform/pull-requests.js +3 -15
  28. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  29. package/dist/lib/pr-review/evidence-grading.js +3 -2
  30. package/dist/lib/process-data/index.js +5 -4
  31. package/dist/lib/process-data/sources.js +6 -0
  32. package/dist/lib/run/index.js +3 -2
  33. package/dist/lib/sandbox/commands/create.js +5 -1
  34. package/dist/lib/sandbox/commands/enter.js +1 -1
  35. package/dist/lib/sandbox/commands/list-running.js +2 -1
  36. package/dist/lib/sandbox/control/client.js +8 -13
  37. package/dist/lib/sandbox/control/completed-reentry.js +2 -1
  38. package/dist/lib/sandbox/control/task-view.js +4 -2
  39. package/dist/lib/sandbox/mounts.js +4 -4
  40. package/dist/lib/sandbox/recovery.js +26 -16
  41. package/dist/lib/sandbox/removal.js +62 -12
  42. package/dist/lib/sandbox/task-resolver.js +9 -6
  43. package/dist/lib/sandbox/workspace-identity.js +6 -5
  44. package/dist/lib/sandbox/workspace-view.js +29 -22
  45. package/dist/lib/task/activity-intent.js +2 -12
  46. package/dist/lib/task/artifact-lifecycle.js +17 -11
  47. package/dist/lib/task/artifact-operations.js +41 -49
  48. package/dist/lib/task/capabilities.js +2 -2
  49. package/dist/lib/task/commands/ls.js +4 -3
  50. package/dist/lib/task/commands/status.js +1 -1
  51. package/dist/lib/task/commit-operation.js +6 -6
  52. package/dist/lib/task/control-authority.js +32 -32
  53. package/dist/lib/task/create-service.js +2 -4
  54. package/dist/lib/task/create.js +12 -65
  55. package/dist/lib/task/delegation-receipts.js +1 -1
  56. package/dist/lib/task/delivery.js +2 -8
  57. package/dist/lib/task/events.js +5 -31
  58. package/dist/lib/task/finalization.js +33 -29
  59. package/dist/lib/task/lifecycle.js +9 -48
  60. package/dist/lib/task/local-artifact-finalization.js +1 -2
  61. package/dist/lib/task/orchestration.js +24 -57
  62. package/dist/lib/task/platform-operation-recovery.js +87 -88
  63. package/dist/lib/task/resolve-ref.js +17 -12
  64. package/dist/lib/task/review-finalization.js +2 -35
  65. package/dist/lib/task/review-identity.js +3 -1
  66. package/dist/lib/task/short-id.js +10 -8
  67. package/dist/lib/task/verification-engine.js +19 -58
  68. package/dist/lib/task/workspace-root.js +59 -0
  69. package/dist/lib/task/workspace-snapshot.js +4 -3
  70. package/dist/lifecycle-build-manifest.json +3 -3
  71. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
  72. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
  73. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
  74. package/lib/decide.ts +2 -7
  75. package/lib/internal/cli-route-inventory.ts +2 -0
  76. package/lib/internal/manual-validation.ts +20 -37
  77. package/lib/internal/platform-comment.ts +49 -6
  78. package/lib/internal/task-activity.ts +1 -10
  79. package/lib/internal/task-event.ts +1 -10
  80. package/lib/internal/task-ledger.ts +1 -10
  81. package/lib/internal/task-operation-registry.ts +2 -1
  82. package/lib/internal/task-qualification.ts +5 -11
  83. package/lib/internal/task-review.ts +3 -14
  84. package/lib/internal/task-short-id.ts +2 -1
  85. package/lib/internal/task-verify.ts +1 -9
  86. package/lib/internal/task-warning.ts +1 -10
  87. package/lib/internal/task-workspace.ts +39 -0
  88. package/lib/merge.ts +4 -3
  89. package/lib/platform/issue-comments.ts +478 -280
  90. package/lib/platform/issue-summary-content.ts +132 -0
  91. package/lib/platform/pr-change-report.ts +165 -20
  92. package/lib/platform/pr-summary-content.ts +60 -0
  93. package/lib/platform/pr-summary.ts +367 -269
  94. package/lib/platform/pull-requests.ts +3 -24
  95. package/lib/platform/summary-content-pipeline.ts +23 -0
  96. package/lib/pr-review/evidence-grading.ts +3 -2
  97. package/lib/process-data/index.ts +4 -3
  98. package/lib/process-data/sources.ts +6 -0
  99. package/lib/run/index.ts +3 -2
  100. package/lib/sandbox/commands/create.ts +5 -1
  101. package/lib/sandbox/commands/enter.ts +1 -1
  102. package/lib/sandbox/commands/list-running.ts +2 -1
  103. package/lib/sandbox/control/client.ts +7 -12
  104. package/lib/sandbox/control/completed-reentry.ts +2 -1
  105. package/lib/sandbox/control/task-view.ts +4 -2
  106. package/lib/sandbox/mounts.ts +4 -4
  107. package/lib/sandbox/recovery.ts +26 -18
  108. package/lib/sandbox/removal.ts +60 -27
  109. package/lib/sandbox/task-resolver.ts +9 -6
  110. package/lib/sandbox/workspace-identity.ts +6 -5
  111. package/lib/sandbox/workspace-view.ts +28 -20
  112. package/lib/task/activity-intent.ts +3 -17
  113. package/lib/task/artifact-lifecycle.ts +39 -11
  114. package/lib/task/artifact-operations.ts +28 -35
  115. package/lib/task/capabilities.ts +2 -2
  116. package/lib/task/commands/ls.ts +4 -3
  117. package/lib/task/commands/status.ts +1 -1
  118. package/lib/task/commit-operation.ts +5 -6
  119. package/lib/task/control-authority.ts +29 -35
  120. package/lib/task/create-service.ts +2 -4
  121. package/lib/task/create.ts +10 -51
  122. package/lib/task/delegation-receipts.ts +1 -1
  123. package/lib/task/delivery.ts +2 -10
  124. package/lib/task/events.ts +5 -32
  125. package/lib/task/finalization.ts +27 -30
  126. package/lib/task/lifecycle.ts +9 -50
  127. package/lib/task/local-artifact-finalization.ts +1 -4
  128. package/lib/task/orchestration.ts +31 -80
  129. package/lib/task/platform-operation-recovery.ts +70 -82
  130. package/lib/task/resolve-ref.ts +18 -12
  131. package/lib/task/review-finalization.ts +2 -52
  132. package/lib/task/review-identity.ts +3 -1
  133. package/lib/task/short-id.ts +10 -8
  134. package/lib/task/verification-engine.ts +20 -62
  135. package/lib/task/workspace-root.ts +78 -0
  136. package/lib/task/workspace-snapshot.ts +4 -3
  137. package/package.json +1 -1
  138. package/templates/.agents/QUICKSTART.en.md +19 -8
  139. package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
  140. package/templates/.agents/README.en.md +10 -15
  141. package/templates/.agents/README.zh-CN.md +9 -13
  142. package/templates/.agents/rules/issue-sync.en.md +1 -1
  143. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  144. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  145. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  146. package/templates/.agents/rules/next-step-output.en.md +2 -2
  147. package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
  148. package/templates/.agents/rules/pr-sync.en.md +5 -3
  149. package/templates/.agents/rules/pr-sync.zh-CN.md +7 -10
  150. package/templates/.agents/rules/release-commands.en.md +3 -0
  151. package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
  152. package/templates/.agents/rules/task-management.en.md +0 -2
  153. package/templates/.agents/rules/task-management.zh-CN.md +0 -2
  154. package/templates/.agents/rules/task-short-id.en.md +2 -2
  155. package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
  156. package/templates/.agents/scripts/task-short-id.js +2 -8
  157. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  158. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  159. package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
  160. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
  161. package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
  162. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
  163. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
  164. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
  166. package/templates/.agents/skills/block-task/config/verify.json +0 -1
  167. package/templates/.agents/skills/cancel-task/SKILL.en.md +7 -7
  168. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +7 -7
  169. package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
  170. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  171. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  172. package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
  173. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
  174. package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
  175. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  176. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  177. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  178. package/templates/.agents/skills/commit/config/verify.json +0 -1
  179. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
  180. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
  181. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +2 -2
  182. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +2 -2
  183. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  184. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  185. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
  186. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
  187. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +3 -3
  188. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +3 -3
  189. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -30
  190. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -32
  191. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  192. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  195. package/templates/.agents/skills/create-pr/config/verify.json +0 -1
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +10 -3
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +10 -3
  198. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +3 -3
  199. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +3 -3
  200. package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
  201. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
  202. package/templates/.agents/skills/create-task/SKILL.en.md +4 -22
  203. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -22
  204. package/templates/.agents/skills/create-task/config/verify.json +0 -1
  205. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  206. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  207. package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
  208. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
  209. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  210. package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
  211. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  212. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  213. package/templates/.agents/skills/import-issue/config/verify.json +0 -1
  214. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  215. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  216. package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
  217. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
  218. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  219. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
  220. package/templates/.agents/skills/restore-task/config/verify.json +0 -1
  221. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  222. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  223. package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
  224. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
  225. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  226. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  227. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
  230. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
  231. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  232. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  233. package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
  234. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
  235. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  236. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  237. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
  238. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
  239. package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
  240. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
  241. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
  242. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
  243. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
  244. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
  245. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  246. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  247. package/templates/.agents/skills/test/SKILL.en.md +0 -3
  248. package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
  249. package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
  250. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
  251. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
  252. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
  253. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
  254. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  255. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  256. package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
  257. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  258. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
  259. package/templates/.agents/templates/handoff.en.md +1 -1
  260. package/templates/.agents/templates/handoff.zh-CN.md +1 -1
  261. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  262. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  263. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  264. package/dist/lib/task/task-execution-lock.js +0 -250
  265. package/lib/task/task-execution-lock.ts +0 -320
@@ -14,7 +14,6 @@ import { planPullRequestMetadata } from './pull-request-metadata.js';
14
14
  import { planInLabelUpdate, validateInLabelMapping } from './in-label-sync.js';
15
15
  import { platformResult } from './types.js';
16
16
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
17
- import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
18
17
  import { resolveDeliveryTarget } from '../task/delivery-target.js';
19
18
  import { mergeOperationWarnings } from '../task/operation-outcome.js';
20
19
  import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
@@ -1007,16 +1006,9 @@ async function createPlatformPullRequestImpl(taskRef, options) {
1007
1006
  }), PRECONDITION_NOT_CREATED);
1008
1007
  }
1009
1008
  try {
1010
- return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createExternalPullRequest(taskRef, options, base));
1009
+ return await createExternalPullRequest(taskRef, options, base);
1011
1010
  }
1012
1011
  catch (error) {
1013
- if (error instanceof TaskExecutionLockError) {
1014
- return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
1015
- platform: base.context.platform,
1016
- capabilities: base.context.capabilities,
1017
- error: { code: error.code, message: error.message, retryable: true }
1018
- }), PRECONDITION_NOT_CREATED);
1019
- }
1020
1012
  return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
1021
1013
  platform: base.context.platform,
1022
1014
  capabilities: base.context.capabilities,
@@ -1456,7 +1448,7 @@ function readProjectPrFlow(repoRoot) {
1456
1448
  return undefined;
1457
1449
  }
1458
1450
  }
1459
- function skipPlatformPullRequestFactUnlocked(resolved, options) {
1451
+ function applySkippedPullRequestFact(resolved, options) {
1460
1452
  let content;
1461
1453
  let frontmatter;
1462
1454
  try {
@@ -1533,13 +1525,9 @@ async function skipPlatformPullRequestFact(taskRef, options) {
1533
1525
  error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
1534
1526
  });
1535
1527
  try {
1536
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'platform-pr.skip', () => skipPlatformPullRequestFactUnlocked(resolved, options));
1528
+ return await applySkippedPullRequestFact(resolved, options);
1537
1529
  }
1538
1530
  catch (error) {
1539
- if (error instanceof TaskExecutionLockError)
1540
- return result('blocked', resolved.taskId, null, null, {
1541
- error: { code: error.code, message: error.message, retryable: true }
1542
- });
1543
1531
  return result('failed', resolved.taskId, null, null, {
1544
1532
  error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
1545
1533
  });
@@ -0,0 +1,18 @@
1
+ function parse(value, parser) {
2
+ try {
3
+ return parser(value);
4
+ }
5
+ catch (error) {
6
+ throw error instanceof Error ? error : new Error(String(error));
7
+ }
8
+ }
9
+ export function runSummaryContentPipeline(value, operations) {
10
+ try {
11
+ parse(value, operations.validate);
12
+ }
13
+ catch { /* Source correction gets a chance to repair invalid structure. */ }
14
+ const corrected = operations.correct(value);
15
+ const content = parse(corrected, operations.validate);
16
+ return { content, rendered: operations.render(content) };
17
+ }
18
+ //# sourceMappingURL=summary-content-pipeline.js.map
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
4
4
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
5
5
  import { resourceIdentityEquals, resourceIdentityNumber } from '../platform/resource-identity.js';
6
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
6
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
7
8
  export class HostResolutionError extends Error {
8
9
  code;
@@ -35,12 +36,12 @@ export function extractClosingIssueNumbers(body) {
35
36
  return result;
36
37
  }
37
38
  /**
38
- * Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
39
+ * Scan active project task workspace task.md files for tasks bound to the
39
40
  * PR number or to one of the PR's closing issues. A task is emitted at most once;
40
41
  * a verified fact identity hit takes priority over the issue-identity reverse lookup.
41
42
  */
42
43
  export function collectHostCandidates(input) {
43
- const activeRoot = path.join(input.workspaceRoot, '.agents', 'workspace', 'active');
44
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(input.workspaceRoot), 'active');
44
45
  const candidates = [];
45
46
  let entries;
46
47
  try {
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { detectRepoRoot } from '../task/resolve-ref.js';
5
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
5
6
  import { acquireGitHubCheckpoint, commitGitHubCheckpoint, readGitHubCheckpoint, releaseGitHubCheckpoint } from './checkpoint.js';
6
7
  import { materializeJson, materializeSnapshot } from './materialize.js';
7
8
  import { normalizeResources } from './normalize.js';
@@ -60,14 +61,14 @@ function dataRoot(repoRoot, requested) {
60
61
  input = path.join(boundary, '.agent-infra', 'data-store', config.project);
61
62
  }
62
63
  const resolved = path.resolve(repoRoot, input);
63
- const relative = path.relative(repoRoot, resolved);
64
- if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative)))
65
- throw new Error('data root must be inside the repository');
66
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
64
+ const workspace = resolveProjectTaskWorkspaceRoot(repoRoot);
67
65
  const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
68
66
  if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
69
67
  throw new Error('data root cannot be the workspace root or a task directory');
70
68
  }
69
+ const relative = path.relative(repoRoot, resolved);
70
+ if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative)))
71
+ throw new Error('data root must be inside the repository');
71
72
  let current = resolved;
72
73
  while (current !== boundary) {
73
74
  if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createGitHubClient } from '../platform/github-client.js';
4
4
  import { enumerateAllTaskDirs } from '../task/resolve-ref.js';
5
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
5
6
  import { captureText, inspectPrivacy, redactExcerpt } from './privacy.js';
6
7
  import { canonicalJsonBytes, sha256 } from './store.js';
7
8
  function endpointForPage(endpoint, page, perPage) {
@@ -103,6 +104,11 @@ function fetchRestCollection(options) {
103
104
  };
104
105
  }
105
106
  function relativeIdentity(repoRoot, filePath) {
107
+ const workspaceRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
108
+ const taskRelative = path.relative(workspaceRoot, filePath);
109
+ if (taskRelative && !taskRelative.startsWith('..') && !path.isAbsolute(taskRelative)) {
110
+ return path.posix.join('task-workspace', taskRelative.split(path.sep).join('/'));
111
+ }
106
112
  const relative = path.relative(repoRoot, filePath);
107
113
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
108
114
  throw new Error(`path escapes repository: ${filePath}`);
@@ -4,6 +4,7 @@ import { loadServerConfig } from '../server/config.js';
4
4
  import { resolveSandboxTarget } from '../sandbox/workspace-identity.js';
5
5
  import { createRunId, runInSandbox } from '../sandbox/capture.js';
6
6
  import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.js';
7
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
7
8
  import { buildTuiCommand, renderPrompt, selectTui } from './tui.js';
8
9
  import { getSkillRunSpec } from './skills.js';
9
10
  import { runHostCommand } from './host.js';
@@ -133,7 +134,7 @@ function readShortIdLength(repoRoot) {
133
134
  return 2;
134
135
  }
135
136
  function readActiveShortIdRegistry(repoRoot) {
136
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
137
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
137
138
  try {
138
139
  const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
139
140
  return data && typeof data === 'object' && data.ids && typeof data.ids === 'object' ? data.ids : {};
@@ -157,7 +158,7 @@ function resolveActiveTaskIdentity(taskRef, repoRoot) {
157
158
  }
158
159
  if (!taskId)
159
160
  return null;
160
- const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
161
+ const taskDir = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId);
161
162
  if (!fs.existsSync(path.join(taskDir, 'task.md')))
162
163
  return null;
163
164
  return { taskId, taskDir, taskRef: resolvedTaskRef };
@@ -1040,7 +1040,7 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1040
1040
  const completedTaskSource = target.workspace.mode === 'task-bound'
1041
1041
  ? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
1042
1042
  : null;
1043
- prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
1043
+ prepareSandboxWorkspaceMountTargets(effectiveConfig.repoRoot);
1044
1044
  if (previousCutoverSnapshot) {
1045
1045
  assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1046
1046
  if (previousManifest) {
@@ -1177,6 +1177,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1177
1177
  : []),
1178
1178
  '--label',
1179
1179
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1180
+ // Every task sandbox runs without seccomp and AppArmor filtering;
1181
+ // capabilities and namespace isolation remain unchanged.
1182
+ '--security-opt', 'seccomp=unconfined',
1183
+ '--security-opt', 'apparmor=unconfined',
1180
1184
  ...coreVolumes,
1181
1185
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1182
1186
  ...dotfilesMount,
@@ -13,7 +13,7 @@ import { resolveSandboxTarget } from '../workspace-identity.js';
13
13
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
14
14
 
15
15
  N references an active task short id from
16
- .agents/workspace/active/.short-ids.json. They resolve only via that
16
+ the read-only task-workspace mount at /task-workspace/active/.short-ids.json. They resolve only via that
17
17
  registry — they do not reference a container's row position in
18
18
  'ai sandbox ls' output. --recreate rebuilds the matched container while
19
19
  preserving its task or branch identity. The same token after the target is
@@ -5,6 +5,7 @@ import { runSafeEngine, runVerboseEngine } from '../shell.js';
5
5
  import { resolveTaskBranch } from '../task-resolver.js';
6
6
  import { isRemovedHashShortIdInput } from '../../task/short-id.js';
7
7
  import { parseSandboxWorkspaceIdentity } from '../workspace-identity.js';
8
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.js';
8
9
  export function containerListFormat() {
9
10
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
10
11
  }
@@ -111,7 +112,7 @@ function tryResolveFromRegistry(arg, repoRoot) {
111
112
  throw new Error(`Registry returned malformed task id for '${arg}': ${JSON.stringify(taskId)}`);
112
113
  }
113
114
  for (const sub of ['active', 'completed', 'blocked', 'archive']) {
114
- const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
115
+ const taskMdPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), sub, taskId, 'task.md');
115
116
  if (!fs.existsSync(taskMdPath))
116
117
  continue;
117
118
  const content = fs.readFileSync(taskMdPath, 'utf8');
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.js';
5
+ import { detectRepoRoot } from '../../task/resolve-ref.js';
4
6
  import { SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
5
7
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
6
8
  import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
@@ -127,19 +129,12 @@ const CONTROL_TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
127
129
  const CONTROL_SHORT_ID_RE = /^\d+$/;
128
130
  // Use the current short-id contract for the first visible active registry.
129
131
  export function resolveVisibleActiveShortId(ref) {
130
- let dir = process.cwd();
131
- for (let depth = 0; depth < 64; depth += 1) {
132
- const registryPath = path.join(dir, '.agents', 'workspace', 'active', '.short-ids.json');
133
- if (fs.existsSync(registryPath)) {
134
- const resolved = resolveShortIdReadOnly(ref, dir, { shortIdLength: configuredShortIdLength(dir) });
135
- return resolved.ok ? resolved.taskId : null;
136
- }
137
- const parent = path.dirname(dir);
138
- if (parent === dir)
139
- break;
140
- dir = parent;
141
- }
142
- return null;
132
+ const repoRoot = detectRepoRoot();
133
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
134
+ if (!fs.existsSync(registryPath))
135
+ return null;
136
+ const resolved = resolveShortIdReadOnly(ref, repoRoot, { shortIdLength: configuredShortIdLength(repoRoot) });
137
+ return resolved.ok ? resolved.taskId : null;
143
138
  }
144
139
  // A request's leading argument is only a task reference for some families and
145
140
  // may be a documented short id. Compare canonical ids so a bound sandbox keeps
@@ -4,6 +4,7 @@ import { resolveTaskWorkspace } from '../task-resolver.js';
4
4
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
5
5
  import { parseTypedTaskFrontmatter } from '../../task/frontmatter.js';
6
6
  import { readTaskFinalizationReceipt } from '../../task/finalization.js';
7
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.js';
7
8
  import { inspectSandboxControlContainer } from './container-identity.js';
8
9
  import { atomicWriteJson, readSandboxControlStatus } from './state.js';
9
10
  import { taskViewForManifest } from './task-view.js';
@@ -40,7 +41,7 @@ export async function prepareCompletedReentry(manifest, inspect = inspectSandbox
40
41
  || previous.receipt.requestId !== view.receipt.requestId
41
42
  || previous.receipt.revision > view.receipt.revision)
42
43
  throw invalid();
43
- const completedRoot = fs.realpathSync.native(path.join(manifest.repoRoot, '.agents', 'workspace', 'completed'));
44
+ const completedRoot = fs.realpathSync.native(path.join(resolveProjectTaskWorkspaceRoot(manifest.repoRoot), 'completed'));
44
45
  const source = fs.realpathSync.native(path.join(completedRoot, manifest.taskId));
45
46
  if (source !== path.join(completedRoot, manifest.taskId))
46
47
  throw invalid();
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.js';
3
4
  export const SANDBOX_TASK_VIEW_STATES = ['not-applicable', 'current', 'finalized-stale', 'unknown'];
4
5
  export const SANDBOX_TASK_VIEW_SOURCES = ['active', 'completed', 'unknown'];
5
6
  function invalidView(message) {
@@ -71,8 +72,9 @@ export function taskViewForManifest(params) {
71
72
  if (params.mode !== 'task-bound' || !params.taskId) {
72
73
  return { state: 'not-applicable', taskId: null, observedSource: null, receipt: null, reasonCode: null };
73
74
  }
74
- const active = path.join(params.repoRoot, '.agents', 'workspace', 'active', params.taskId, 'task.md');
75
- const completed = path.join(params.repoRoot, '.agents', 'workspace', 'completed', params.taskId, 'task.md');
75
+ const taskRoot = resolveProjectTaskWorkspaceRoot(params.repoRoot);
76
+ const active = path.join(taskRoot, 'active', params.taskId, 'task.md');
77
+ const completed = path.join(taskRoot, 'completed', params.taskId, 'task.md');
76
78
  const activeExists = fs.existsSync(active);
77
79
  const completedExists = fs.existsSync(completed);
78
80
  if (activeExists === completedExists) {
@@ -11,12 +11,12 @@ function workspaceStateMounts(workspaceViewRoot, taskBound) {
11
11
  .map(({ state, hostPath }) => taskBound && state === 'active'
12
12
  ? {
13
13
  hostPaths: [path.join(hostPath, '.short-ids.json')],
14
- containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
14
+ containerPath: '/task-workspace/active/.short-ids.json',
15
15
  readOnly: true
16
16
  }
17
17
  : {
18
18
  hostPaths: [hostPath],
19
- containerPath: path.posix.join('/workspace/.agents/workspace', state),
19
+ containerPath: path.posix.join('/task-workspace', state),
20
20
  readOnly: true
21
21
  });
22
22
  }
@@ -37,13 +37,13 @@ function taskWorkspaceMounts(taskSources, taskBound, overrides) {
37
37
  const taskId = overrides.taskId;
38
38
  const mounts = [{
39
39
  hostPaths: taskSources,
40
- containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
40
+ containerPath: path.posix.join('/task-workspace/active', taskId),
41
41
  readOnly: false
42
42
  }];
43
43
  if (overrides.completedTaskSource)
44
44
  mounts.push({
45
45
  hostPaths: [overrides.completedTaskSource],
46
- containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
46
+ containerPath: path.posix.join('/task-workspace/completed', taskId),
47
47
  readOnly: false
48
48
  });
49
49
  return mounts;
@@ -11,6 +11,7 @@ import { sandboxCoreBindMounts } from './mounts.js';
11
11
  import { resolveSandboxGitMetadata } from './git-metadata.js';
12
12
  import { assertSandboxTaskSource, sandboxCompletedTaskMountSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
13
13
  import { resolveTaskWorkspace } from './task-resolver.js';
14
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
14
15
  import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
15
16
  import { inspectWorktree } from './worktree-safety.js';
16
17
  import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
@@ -277,11 +278,13 @@ export function classifySandboxRecovery(snapshot) {
277
278
  });
278
279
  }
279
280
  if (snapshot.workspaceTopology === 'legacy-parent') {
281
+ const legacyRoot = snapshot.mounts.some((mount) => mount.path === '/task-workspace')
282
+ ? '/task-workspace' : '/workspace/.agents/workspace';
280
283
  findings.push({
281
284
  repairKind: 'hard-failure',
282
285
  code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
283
286
  message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
284
- path: '/workspace/.agents/workspace'
287
+ path: legacyRoot
285
288
  });
286
289
  }
287
290
  if (snapshot.taskView && !snapshot.taskView.readable) {
@@ -302,7 +305,8 @@ export function classifySandboxRecovery(snapshot) {
302
305
  }
303
306
  for (const mount of snapshot.mounts) {
304
307
  if (snapshot.workspaceTopology === 'legacy-parent'
305
- && mount.path.startsWith('/workspace/.agents/workspace'))
308
+ && (mount.path.startsWith('/workspace/.agents/workspace')
309
+ || mount.path.startsWith('/task-workspace')))
306
310
  continue;
307
311
  if (mount.actualType !== mount.expectedType
308
312
  || !mount.sourceMatches
@@ -386,14 +390,15 @@ function inspectContainer(engine, container, runFn) {
386
390
  return parsed[0];
387
391
  }
388
392
  function workspaceTopology(mountsByDestination) {
389
- if (mountsByDestination.has('/workspace/.agents/workspace'))
393
+ if (mountsByDestination.has('/workspace/.agents/workspace')
394
+ || mountsByDestination.has('/task-workspace'))
390
395
  return 'legacy-parent';
391
396
  const perState = [
392
- '/workspace/.agents/workspace/active',
393
- '/workspace/.agents/workspace/active/.short-ids.json',
394
- '/workspace/.agents/workspace/completed',
395
- '/workspace/.agents/workspace/blocked',
396
- '/workspace/.agents/workspace/archive'
397
+ '/task-workspace/active',
398
+ '/task-workspace/active/.short-ids.json',
399
+ '/task-workspace/completed',
400
+ '/task-workspace/blocked',
401
+ '/task-workspace/archive'
397
402
  ];
398
403
  return perState.some((destination) => mountsByDestination.has(destination))
399
404
  ? 'per-state'
@@ -421,8 +426,9 @@ function sourceCandidates(engine, hostPath) {
421
426
  return [...new Set(candidates.map((candidate) => normalizeMountSource(engine, candidate)))];
422
427
  }
423
428
  function recoveryTaskSources(repoRoot, taskId) {
424
- const activePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
425
- const completedPath = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId);
429
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
430
+ const activePath = path.join(taskRoot, 'active', taskId);
431
+ const completedPath = path.join(taskRoot, 'completed', taskId);
426
432
  if (fs.existsSync(activePath) && !fs.existsSync(completedPath)) {
427
433
  const source = assertSandboxTaskSource(repoRoot, taskId);
428
434
  return { mountPaths: [source], accessiblePaths: [source] };
@@ -435,12 +441,12 @@ function recoveryTaskSources(repoRoot, taskId) {
435
441
  if (task.state !== 'completed') {
436
442
  throw new Error(`SANDBOX_TASK_SOURCE_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
437
443
  }
438
- const completedRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'completed'));
444
+ const completedRoot = fs.realpathSync.native(path.join(taskRoot, 'completed'));
439
445
  const completedSource = fs.realpathSync.native(path.dirname(task.taskMd));
440
446
  if (path.dirname(completedSource) !== completedRoot) {
441
447
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${completedSource} is not under completed workspace`);
442
448
  }
443
- const historicalActivePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
449
+ const historicalActivePath = path.join(taskRoot, 'active', taskId);
444
450
  const mountPaths = [completedSource, historicalActivePath];
445
451
  if (fs.existsSync(historicalActivePath)) {
446
452
  try {
@@ -456,7 +462,7 @@ function recoveryTaskSources(repoRoot, taskId) {
456
462
  export function startCompletedSandboxContainer(params) {
457
463
  const taskSources = recoveryTaskSources(params.config.repoRoot, params.taskId);
458
464
  const completedSource = taskSources.accessiblePaths[0];
459
- const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
465
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(params.config.repoRoot), 'active');
460
466
  const historicalSource = path.join(activeRoot, params.taskId);
461
467
  fs.mkdirSync(activeRoot, { recursive: true });
462
468
  let replacedEmptyRoot = false;
@@ -559,11 +565,11 @@ function expectedMounts(params) {
559
565
  expectedType: 'bind',
560
566
  hostPaths: mount.hostPaths,
561
567
  ...(taskSources !== null && taskId !== null
562
- && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
568
+ && mount.containerPath === `/task-workspace/active/${taskId}`
563
569
  ? { sourceAccessiblePaths: taskSources.accessiblePaths }
564
570
  : {}),
565
571
  ...(completedTaskSource !== null && taskId !== null
566
- && mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
572
+ && mount.containerPath === `/task-workspace/completed/${taskId}`
567
573
  ? { sourceAccessiblePaths: [completedTaskSource] }
568
574
  : {}),
569
575
  expectedRW: !mount.readOnly
@@ -664,7 +670,7 @@ export function collectSandboxRecoverySnapshot(params) {
664
670
  const tools = [...capabilityPlan.tools];
665
671
  const topology = workspaceTopology(mountsByDestination);
666
672
  const taskViewPath = workspace.mode === 'task-bound'
667
- ? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
673
+ ? `/task-workspace/active/${workspace.taskId}/task.md`
668
674
  : null;
669
675
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
670
676
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
@@ -856,6 +862,10 @@ function canRetryFreshReadiness(findings) {
856
862
  && finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
857
863
  && finding.path !== undefined
858
864
  && (finding.path.startsWith('/workspace/.agents/workspace')
865
+ || finding.path.startsWith('/task-workspace/active')
866
+ || finding.path.startsWith('/task-workspace/blocked')
867
+ || finding.path.startsWith('/task-workspace/completed')
868
+ || finding.path.startsWith('/task-workspace/archive')
859
869
  || finding.path === '/home/devuser/.host-shell-config'
860
870
  || finding.path === '/home/devuser/.bash_aliases'
861
871
  || finding.path === '/run/agent-infra/runtime'));
@@ -17,7 +17,6 @@ import { acquireSandboxResourceLock } from './control/native-file-lock.js';
17
17
  import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.js';
18
18
  import { getProcessStartTime } from '../server/process-state.js';
19
19
  import { releaseStaleShortIdRegistry } from '../task/short-id.js';
20
- import { withRepositoryMutationLock } from '../task/task-execution-lock.js';
21
20
  import { fetchSandboxRows } from './commands/list-running.js';
22
21
  import { createSandboxControlBindingEvidence } from './task-cleanup.js';
23
22
  import { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktrees, verifyWorktreePermit } from './worktree-safety.js';
@@ -28,6 +27,40 @@ function isMissingPathError(error) {
28
27
  return Boolean(error && typeof error === 'object' && 'code' in error
29
28
  && error.code === 'ENOENT');
30
29
  }
30
+ function removalErrorCode(error) {
31
+ return error && typeof error === 'object' && 'code' in error
32
+ && typeof error.code === 'string'
33
+ ? error.code
34
+ : null;
35
+ }
36
+ function removalErrorMessage(error) {
37
+ const code = removalErrorCode(error);
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return code ? `${code}: ${message}` : message;
40
+ }
41
+ function addOwnerWriteToDirectories(root) {
42
+ const pending = [root];
43
+ while (pending.length > 0) {
44
+ const current = pending.pop();
45
+ let stat;
46
+ try {
47
+ stat = fs.lstatSync(current);
48
+ }
49
+ catch (error) {
50
+ if (isMissingPathError(error))
51
+ continue;
52
+ throw error;
53
+ }
54
+ if (!stat.isDirectory())
55
+ continue;
56
+ fs.chmodSync(current, stat.mode | 0o200);
57
+ for (const entry of fs.readdirSync(current))
58
+ pending.push(path.join(current, entry));
59
+ }
60
+ }
61
+ function isPermissionRemovalError(error) {
62
+ return ['EACCES', 'EPERM', 'ENOTEMPTY'].includes(removalErrorCode(error) ?? '');
63
+ }
31
64
  function canonicalPath(input) {
32
65
  try {
33
66
  return fs.realpathSync.native(input);
@@ -665,12 +698,12 @@ async function authorizeWorktrees(worktrees, { allowDirtyDiscard, assumeYes }, {
665
698
  }
666
699
  return permits;
667
700
  }
668
- async function runRmOneUnderRepositoryLock(config, tools, branch, options = {}) {
701
+ async function runRmOne(config, tools, branch, options = {}) {
669
702
  const target = options.target ?? resolveRmTarget(config, tools, options.cleanupTarget ?? resolveSandboxCleanupTarget(branch, config.repoRoot));
670
703
  return rmOneCore(config, tools, branch, { ...options, target });
671
704
  }
672
705
  async function rmOne(config, tools, branch, options = {}) {
673
- await withRepositoryMutationLock(config.repoRoot, () => runRmOneUnderRepositoryLock(config, tools, branch, options));
706
+ await runRmOne(config, tools, branch, options);
674
707
  }
675
708
  async function removeUncheckedSandbox(config, target, options) {
676
709
  const confirm = options.prompt?.confirm ?? p.confirm;
@@ -719,8 +752,28 @@ async function removeUncheckedSandbox(config, target, options) {
719
752
  fs.rmSync(root, { recursive: true, force: true });
720
753
  removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
721
754
  }
722
- for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates))
723
- fs.rmSync(candidate, { recursive: true, force: true });
755
+ const toolRemovalFailures = [];
756
+ for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) {
757
+ try {
758
+ fs.rmSync(candidate, { recursive: true, force: true });
759
+ continue;
760
+ }
761
+ catch (initialError) {
762
+ if (isPermissionRemovalError(initialError)) {
763
+ try {
764
+ addOwnerWriteToDirectories(candidate);
765
+ fs.rmSync(candidate, { recursive: true, force: true });
766
+ continue;
767
+ }
768
+ catch (retryError) {
769
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}; retry failed: ${removalErrorMessage(retryError)}`);
770
+ }
771
+ }
772
+ else {
773
+ toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}`);
774
+ }
775
+ }
776
+ }
724
777
  for (const shell of shellConfigDirCandidates(config, effectiveBranch))
725
778
  fs.rmSync(shell, { recursive: true, force: true });
726
779
  if (shouldRemoveShare)
@@ -737,6 +790,9 @@ async function removeUncheckedSandbox(config, target, options) {
737
790
  assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
738
791
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
739
792
  }
793
+ if (toolRemovalFailures.length > 0) {
794
+ throw new Error(`SANDBOX_TOOL_CLEANUP_INCOMPLETE:\n${toolRemovalFailures.map((failure) => ` - ${failure}`).join('\n')}`);
795
+ }
740
796
  releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
741
797
  if (!options.quiet)
742
798
  p.outro('Sandbox removed');
@@ -751,9 +807,6 @@ async function rmOneCore(config, tools, branch, options = {}) {
751
807
  return removeUncheckedSandbox(config, target, options);
752
808
  }
753
809
  async function rmPurge(config, tools, prompt = {}) {
754
- return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
755
- }
756
- async function rmPurgeCore(config, tools, prompt = {}) {
757
810
  const engine = detectEngine(config);
758
811
  const confirm = prompt.confirm ?? p.confirm;
759
812
  const isCancel = prompt.isCancel ?? p.isCancel;
@@ -829,9 +882,6 @@ async function rmPurgeCore(config, tools, prompt = {}) {
829
882
  p.outro(pc.green('All project sandboxes removed'));
830
883
  }
831
884
  async function rmUnbound(config, tools, options) {
832
- return withRepositoryMutationLock(config.repoRoot, () => rmUnboundCore(config, tools, options));
833
- }
834
- async function rmUnboundCore(config, tools, options) {
835
885
  const engine = detectEngine(config);
836
886
  const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
837
887
  const discovered = [...listed.running, ...listed.nonRunning];
@@ -892,7 +942,7 @@ async function rmUnboundCore(config, tools, options) {
892
942
  return;
893
943
  }
894
944
  for (const { row, cleanupTarget, target } of targets) {
895
- await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
945
+ await runRmOne(config, tools, cleanupTarget.branch, {
896
946
  assumeYes: options.assumeYes,
897
947
  target,
898
948
  cleanupTarget
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { isRemovedHashShortIdInput } from '../task/short-id.js';
5
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.js';
5
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
7
  const SHORT_ID_RE = /^\d+$/;
7
8
  export const TASK_WORKSPACE_STATES = ['active', 'completed', 'blocked', 'archive'];
@@ -20,10 +21,11 @@ function stripQuotes(value) {
20
21
  return value.replace(/^(["'])(.*)\1$/, '$2');
21
22
  }
22
23
  function readTaskContent(repoRoot, taskId) {
24
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
23
25
  for (const state of TASK_WORKSPACE_STATES) {
24
26
  const taskPath = state === 'archive'
25
- ? path.join(repoRoot, '.agents', 'workspace', state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
26
- : path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
27
+ ? path.join(taskRoot, state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
28
+ : path.join(taskRoot, state, taskId, 'task.md');
27
29
  if (fs.existsSync(taskPath)) {
28
30
  return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
29
31
  }
@@ -31,8 +33,9 @@ function readTaskContent(repoRoot, taskId) {
31
33
  throw new Error(`Task not found: ${taskId}`);
32
34
  }
33
35
  function taskWorkspaceCandidates(repoRoot, taskId) {
36
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
34
37
  const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
35
- const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
38
+ const taskMd = path.join(taskRoot, state, taskId, 'task.md');
36
39
  if (!fs.existsSync(taskMd))
37
40
  return [];
38
41
  const content = fs.readFileSync(taskMd, 'utf8');
@@ -46,7 +49,7 @@ function taskWorkspaceCandidates(repoRoot, taskId) {
46
49
  const parts = findArchiveTaskParts(repoRoot, taskId);
47
50
  if (parts.length === 0)
48
51
  return hot;
49
- const taskMd = path.join(repoRoot, '.agents', 'workspace', 'archive', ...parts, 'local', 'task.md');
52
+ const taskMd = path.join(taskRoot, 'archive', ...parts, 'local', 'task.md');
50
53
  if (!fs.existsSync(taskMd))
51
54
  return hot;
52
55
  const content = fs.readFileSync(taskMd, 'utf8');
@@ -58,7 +61,7 @@ function taskWorkspaceCandidates(repoRoot, taskId) {
58
61
  }];
59
62
  }
60
63
  function findArchiveTaskParts(repoRoot, taskId) {
61
- const archiveRoot = path.join(repoRoot, '.agents', 'workspace', 'archive');
64
+ const archiveRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'archive');
62
65
  if (!fs.existsSync(archiveRoot))
63
66
  return [];
64
67
  for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
@@ -113,7 +116,7 @@ export function resolveTaskWorkspace(taskId, repoRoot) {
113
116
  return matches[0];
114
117
  }
115
118
  export function listTaskWorkspaces(repoRoot, state) {
116
- const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
119
+ const stateRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), state);
117
120
  if (state === 'archive') {
118
121
  if (!fs.existsSync(stateRoot))
119
122
  return [];