@fitlab-ai/agent-infra 0.11.14 → 0.12.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 (271) 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 +8 -16
  10. package/dist/lib/internal/platform-comment.js +3 -2
  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-finalization.js +2 -2
  14. package/dist/lib/internal/task-ledger.js +1 -12
  15. package/dist/lib/internal/task-operation-registry.js +2 -1
  16. package/dist/lib/internal/task-qualification.js +6 -15
  17. package/dist/lib/internal/task-review.js +3 -16
  18. package/dist/lib/internal/task-short-id.js +2 -1
  19. package/dist/lib/internal/task-verify.js +1 -11
  20. package/dist/lib/internal/task-warning.js +1 -12
  21. package/dist/lib/internal/task-workspace.js +43 -0
  22. package/dist/lib/merge.js +4 -3
  23. package/dist/lib/platform/pr-change-report.js +10 -3
  24. package/dist/lib/platform/pr-summary.js +6 -15
  25. package/dist/lib/platform/pull-requests.js +3 -15
  26. package/dist/lib/pr-review/evidence-grading.js +3 -2
  27. package/dist/lib/process-data/index.js +18 -6
  28. package/dist/lib/process-data/sources.js +6 -0
  29. package/dist/lib/run/index.js +3 -2
  30. package/dist/lib/sandbox/commands/create.js +5 -1
  31. package/dist/lib/sandbox/commands/enter.js +1 -1
  32. package/dist/lib/sandbox/commands/list-running.js +2 -1
  33. package/dist/lib/sandbox/control/client.js +8 -13
  34. package/dist/lib/sandbox/control/completed-reentry.js +2 -1
  35. package/dist/lib/sandbox/control/lifecycle.js +2 -21
  36. package/dist/lib/sandbox/control/task-view.js +4 -2
  37. package/dist/lib/sandbox/mounts.js +4 -4
  38. package/dist/lib/sandbox/recovery.js +26 -16
  39. package/dist/lib/sandbox/removal.js +3 -10
  40. package/dist/lib/sandbox/task-resolver.js +9 -6
  41. package/dist/lib/sandbox/workspace-identity.js +36 -20
  42. package/dist/lib/sandbox/workspace-view.js +29 -22
  43. package/dist/lib/task/activity-intent.js +2 -12
  44. package/dist/lib/task/artifact-lifecycle.js +17 -11
  45. package/dist/lib/task/artifact-operations.js +41 -49
  46. package/dist/lib/task/capabilities.js +2 -2
  47. package/dist/lib/task/commands/ls.js +4 -3
  48. package/dist/lib/task/commands/status.js +1 -1
  49. package/dist/lib/task/commit-operation.js +6 -6
  50. package/dist/lib/task/control-authority.js +32 -32
  51. package/dist/lib/task/create-service.js +2 -4
  52. package/dist/lib/task/create.js +12 -65
  53. package/dist/lib/task/delegation-receipts.js +1 -1
  54. package/dist/lib/task/delivery.js +2 -8
  55. package/dist/lib/task/events.js +5 -31
  56. package/dist/lib/task/finalization.js +17 -22
  57. package/dist/lib/task/lifecycle.js +38 -49
  58. package/dist/lib/task/local-artifact-finalization.js +1 -2
  59. package/dist/lib/task/orchestration.js +24 -57
  60. package/dist/lib/task/platform-operation-recovery.js +1 -2
  61. package/dist/lib/task/resolve-ref.js +17 -12
  62. package/dist/lib/task/review-finalization.js +2 -35
  63. package/dist/lib/task/review-identity.js +3 -1
  64. package/dist/lib/task/short-id.js +12 -10
  65. package/dist/lib/task/verification-engine.js +19 -58
  66. package/dist/lib/task/workspace-root.js +59 -0
  67. package/dist/lib/task/workspace-snapshot.js +4 -3
  68. package/dist/lifecycle-build-manifest.json +3 -3
  69. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
  70. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
  71. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
  72. package/lib/decide.ts +2 -7
  73. package/lib/internal/cli-route-inventory.ts +2 -0
  74. package/lib/internal/manual-validation.ts +8 -19
  75. package/lib/internal/platform-comment.ts +3 -2
  76. package/lib/internal/task-activity.ts +1 -10
  77. package/lib/internal/task-event.ts +1 -10
  78. package/lib/internal/task-finalization.ts +2 -2
  79. package/lib/internal/task-ledger.ts +1 -10
  80. package/lib/internal/task-operation-registry.ts +2 -1
  81. package/lib/internal/task-qualification.ts +5 -11
  82. package/lib/internal/task-review.ts +3 -14
  83. package/lib/internal/task-short-id.ts +2 -1
  84. package/lib/internal/task-verify.ts +1 -9
  85. package/lib/internal/task-warning.ts +1 -10
  86. package/lib/internal/task-workspace.ts +39 -0
  87. package/lib/merge.ts +4 -3
  88. package/lib/platform/pr-change-report.ts +9 -3
  89. package/lib/platform/pr-summary.ts +7 -16
  90. package/lib/platform/pull-requests.ts +3 -24
  91. package/lib/pr-review/evidence-grading.ts +3 -2
  92. package/lib/process-data/index.ts +17 -5
  93. package/lib/process-data/sources.ts +6 -0
  94. package/lib/run/index.ts +3 -2
  95. package/lib/sandbox/commands/create.ts +5 -1
  96. package/lib/sandbox/commands/enter.ts +1 -1
  97. package/lib/sandbox/commands/list-running.ts +2 -1
  98. package/lib/sandbox/control/client.ts +7 -12
  99. package/lib/sandbox/control/completed-reentry.ts +2 -1
  100. package/lib/sandbox/control/lifecycle.ts +4 -21
  101. package/lib/sandbox/control/task-view.ts +4 -2
  102. package/lib/sandbox/mounts.ts +4 -4
  103. package/lib/sandbox/recovery.ts +26 -18
  104. package/lib/sandbox/removal.ts +3 -26
  105. package/lib/sandbox/task-resolver.ts +9 -6
  106. package/lib/sandbox/workspace-identity.ts +42 -18
  107. package/lib/sandbox/workspace-view.ts +28 -20
  108. package/lib/task/activity-intent.ts +3 -17
  109. package/lib/task/artifact-lifecycle.ts +39 -11
  110. package/lib/task/artifact-operations.ts +28 -35
  111. package/lib/task/capabilities.ts +2 -2
  112. package/lib/task/commands/ls.ts +4 -3
  113. package/lib/task/commands/status.ts +1 -1
  114. package/lib/task/commit-operation.ts +5 -6
  115. package/lib/task/control-authority.ts +29 -35
  116. package/lib/task/create-service.ts +2 -4
  117. package/lib/task/create.ts +10 -51
  118. package/lib/task/delegation-receipts.ts +1 -1
  119. package/lib/task/delivery.ts +2 -10
  120. package/lib/task/events.ts +5 -32
  121. package/lib/task/finalization.ts +16 -24
  122. package/lib/task/lifecycle.ts +40 -51
  123. package/lib/task/local-artifact-finalization.ts +1 -4
  124. package/lib/task/orchestration.ts +31 -80
  125. package/lib/task/platform-operation-recovery.ts +1 -2
  126. package/lib/task/resolve-ref.ts +18 -12
  127. package/lib/task/review-finalization.ts +2 -52
  128. package/lib/task/review-identity.ts +3 -1
  129. package/lib/task/short-id.ts +15 -10
  130. package/lib/task/verification-engine.ts +20 -62
  131. package/lib/task/workspace-root.ts +78 -0
  132. package/lib/task/workspace-snapshot.ts +4 -3
  133. package/package.json +1 -1
  134. package/templates/.agents/QUICKSTART.en.md +19 -8
  135. package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
  136. package/templates/.agents/README.en.md +10 -15
  137. package/templates/.agents/README.zh-CN.md +9 -13
  138. package/templates/.agents/rules/issue-sync.en.md +1 -1
  139. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  140. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  141. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  142. package/templates/.agents/rules/next-step-output.en.md +2 -2
  143. package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
  144. package/templates/.agents/rules/pr-sync.en.md +1 -1
  145. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  146. package/templates/.agents/rules/release-commands.en.md +3 -0
  147. package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
  148. package/templates/.agents/rules/task-management.en.md +0 -2
  149. package/templates/.agents/rules/task-management.zh-CN.md +0 -2
  150. package/templates/.agents/rules/task-short-id.en.md +2 -2
  151. package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
  152. package/templates/.agents/scripts/task-short-id.js +2 -8
  153. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  154. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  155. package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
  156. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
  157. package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
  158. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
  159. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
  160. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  161. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
  162. package/templates/.agents/skills/block-task/config/verify.json +0 -1
  163. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -6
  164. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -6
  165. package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
  166. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  167. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  168. package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
  169. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
  170. package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
  171. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  172. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/commit/config/verify.json +0 -1
  175. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
  176. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
  177. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  178. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  179. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  180. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  181. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
  182. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
  183. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  184. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  185. package/templates/.agents/skills/complete-task/SKILL.en.md +17 -9
  186. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +18 -10
  187. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  188. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  189. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  190. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  191. package/templates/.agents/skills/create-pr/config/verify.json +0 -1
  192. package/templates/.agents/skills/create-pr/reference/change-report.en.md +13 -3
  193. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +13 -3
  194. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  195. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  196. package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
  197. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
  198. package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
  199. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
  200. package/templates/.agents/skills/create-task/config/verify.json +0 -1
  201. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  202. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  203. package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
  204. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
  205. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  206. package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
  207. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  208. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  209. package/templates/.agents/skills/import-issue/config/verify.json +0 -1
  210. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  211. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  212. package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
  213. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
  214. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  215. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
  216. package/templates/.agents/skills/restore-task/config/verify.json +0 -1
  217. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  218. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  219. package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
  220. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
  221. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  222. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  223. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  224. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  225. package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
  226. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
  227. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  228. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
  230. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
  231. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  232. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  233. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
  236. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
  237. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
  238. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
  239. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
  240. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
  241. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
  242. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
  243. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
  244. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
  245. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  246. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  247. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  248. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  249. package/templates/.agents/skills/test/SKILL.en.md +0 -3
  250. package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
  251. package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
  252. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
  253. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
  254. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
  255. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
  256. package/templates/.agents/skills/watch-pr/SKILL.en.md +21 -4
  257. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +22 -5
  258. package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
  259. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
  260. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
  261. package/templates/.agents/templates/handoff.en.md +1 -1
  262. package/templates/.agents/templates/handoff.zh-CN.md +1 -1
  263. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  264. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  265. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  266. package/dist/lib/runtime/agent-runtime.js +0 -33
  267. package/dist/lib/task/runtime-paths.js +0 -10
  268. package/dist/lib/task/task-execution-lock.js +0 -250
  269. package/lib/runtime/agent-runtime.ts +0 -41
  270. package/lib/task/runtime-paths.ts +0 -12
  271. package/lib/task/task-execution-lock.ts +0 -320
@@ -5,6 +5,7 @@ import { runSafeEngine, runVerboseEngine } from '../shell.ts';
5
5
  import { resolveTaskBranch } from '../task-resolver.ts';
6
6
  import { isRemovedHashShortIdInput } from '../../task/short-id.ts';
7
7
  import { parseSandboxWorkspaceIdentity } from '../workspace-identity.ts';
8
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
8
9
 
9
10
  export type SandboxRow = {
10
11
  name: string;
@@ -143,7 +144,7 @@ function tryResolveFromRegistry(arg: string, repoRoot: string): RegistryLookup {
143
144
  );
144
145
  }
145
146
  for (const sub of ['active', 'completed', 'blocked', 'archive']) {
146
- const taskMdPath = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
147
+ const taskMdPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), sub, taskId, 'task.md');
147
148
  if (!fs.existsSync(taskMdPath)) continue;
148
149
  const content = fs.readFileSync(taskMdPath, 'utf8');
149
150
  const fm = content.match(/^branch:\s*(.+)$/m);
@@ -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.ts';
5
+ import { detectRepoRoot } from '../../task/resolve-ref.ts';
4
6
  import {
5
7
  SANDBOX_CONTROL_MAX_BYTES,
6
8
  SANDBOX_CONTROL_STATUS_STALE_MS,
@@ -158,18 +160,11 @@ type CanonicalRequestTask =
158
160
 
159
161
  // Use the current short-id contract for the first visible active registry.
160
162
  export function resolveVisibleActiveShortId(ref: string): string | null {
161
- let dir = process.cwd();
162
- for (let depth = 0; depth < 64; depth += 1) {
163
- const registryPath = path.join(dir, '.agents', 'workspace', 'active', '.short-ids.json');
164
- if (fs.existsSync(registryPath)) {
165
- const resolved = resolveShortIdReadOnly(ref, dir, { shortIdLength: configuredShortIdLength(dir) });
166
- return resolved.ok ? resolved.taskId : null;
167
- }
168
- const parent = path.dirname(dir);
169
- if (parent === dir) break;
170
- dir = parent;
171
- }
172
- return null;
163
+ const repoRoot = detectRepoRoot();
164
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
165
+ if (!fs.existsSync(registryPath)) return null;
166
+ const resolved = resolveShortIdReadOnly(ref, repoRoot, { shortIdLength: configuredShortIdLength(repoRoot) });
167
+ return resolved.ok ? resolved.taskId : null;
173
168
  }
174
169
 
175
170
  // A request's leading argument is only a task reference for some families and
@@ -4,6 +4,7 @@ import { resolveTaskWorkspace } from '../task-resolver.ts';
4
4
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
5
5
  import { parseTypedTaskFrontmatter } from '../../task/frontmatter.ts';
6
6
  import { readTaskFinalizationReceipt } from '../../task/finalization.ts';
7
+ import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
7
8
  import { inspectSandboxControlContainer } from './container-identity.ts';
8
9
  import type { SandboxControlManifest } from './protocol.ts';
9
10
  import { atomicWriteJson, readSandboxControlStatus } from './state.ts';
@@ -53,7 +54,7 @@ export async function prepareCompletedReentry(
53
54
  || previous.receipt.generation !== view.receipt.generation
54
55
  || previous.receipt.requestId !== view.receipt.requestId
55
56
  || previous.receipt.revision > view.receipt.revision) throw invalid();
56
- const completedRoot = fs.realpathSync.native(path.join(manifest.repoRoot, '.agents', 'workspace', 'completed'));
57
+ const completedRoot = fs.realpathSync.native(path.join(resolveProjectTaskWorkspaceRoot(manifest.repoRoot), 'completed'));
57
58
  const source = fs.realpathSync.native(path.join(completedRoot, manifest.taskId));
58
59
  if (source !== path.join(completedRoot, manifest.taskId)) throw invalid();
59
60
  const stat = fs.statSync(source, { bigint: true });
@@ -661,28 +661,11 @@ function readTaskCompletionManifest(
661
661
  return manifest;
662
662
  }
663
663
 
664
- export async function quiesceSandboxControlForTaskCompletion(
664
+ export function validateSandboxControlTaskIdentity(
665
665
  root: string,
666
- binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>,
667
- timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS
668
- ): Promise<void> {
669
- const resolvedRoot = path.resolve(root);
670
- const manifest = readTaskCompletionManifest(resolvedRoot, binding);
671
- markSandboxControlRootQuiescing(resolvedRoot);
672
- const deadline = Date.now() + timeoutMs;
673
- const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
674
- const brokerPath = path.join(resolvedRoot, 'broker.json');
675
- while (true) {
676
- const startup = readStartupOwner(startupPath);
677
- if (fs.existsSync(startupPath) && !startup) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
678
- const broker = readBrokerOwner(brokerPath);
679
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
680
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
681
- }
682
- if (!startup && !broker) return;
683
- if (Date.now() >= deadline) throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
684
- await new Promise((resolve) => setTimeout(resolve, 25));
685
- }
666
+ binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>
667
+ ): void {
668
+ readTaskCompletionManifest(path.resolve(root), binding);
686
669
  }
687
670
 
688
671
  function recordSandboxRemovalPending(
@@ -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.ts';
3
4
 
4
5
  export const SANDBOX_TASK_VIEW_STATES = ['not-applicable', 'current', 'finalized-stale', 'unknown'] as const;
5
6
  export const SANDBOX_TASK_VIEW_SOURCES = ['active', 'completed', 'unknown'] as const;
@@ -119,8 +120,9 @@ export function taskViewForManifest(params: Readonly<{
119
120
  if (params.mode !== 'task-bound' || !params.taskId) {
120
121
  return { state: 'not-applicable', taskId: null, observedSource: null, receipt: null, reasonCode: null };
121
122
  }
122
- const active = path.join(params.repoRoot, '.agents', 'workspace', 'active', params.taskId, 'task.md');
123
- const completed = path.join(params.repoRoot, '.agents', 'workspace', 'completed', params.taskId, 'task.md');
123
+ const taskRoot = resolveProjectTaskWorkspaceRoot(params.repoRoot);
124
+ const active = path.join(taskRoot, 'active', params.taskId, 'task.md');
125
+ const completed = path.join(taskRoot, 'completed', params.taskId, 'task.md');
124
126
  const activeExists = fs.existsSync(active);
125
127
  const completedExists = fs.existsSync(completed);
126
128
  if (activeExists === completedExists) {
@@ -24,12 +24,12 @@ function workspaceStateMounts(workspaceViewRoot: string, taskBound: boolean): Sa
24
24
  .map(({ state, hostPath }) => taskBound && state === 'active'
25
25
  ? {
26
26
  hostPaths: [path.join(hostPath, '.short-ids.json')],
27
- containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
27
+ containerPath: '/task-workspace/active/.short-ids.json',
28
28
  readOnly: true
29
29
  }
30
30
  : {
31
31
  hostPaths: [hostPath],
32
- containerPath: path.posix.join('/workspace/.agents/workspace', state),
32
+ containerPath: path.posix.join('/task-workspace', state),
33
33
  readOnly: true
34
34
  }
35
35
  );
@@ -60,12 +60,12 @@ function taskWorkspaceMounts(taskSources: string[], taskBound: boolean, override
60
60
  const taskId = overrides.taskId!;
61
61
  const mounts: SandboxBindMountDeclaration[] = [{
62
62
  hostPaths: taskSources,
63
- containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
63
+ containerPath: path.posix.join('/task-workspace/active', taskId),
64
64
  readOnly: false
65
65
  }];
66
66
  if (overrides.completedTaskSource) mounts.push({
67
67
  hostPaths: [overrides.completedTaskSource],
68
- containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
68
+ containerPath: path.posix.join('/task-workspace/completed', taskId),
69
69
  readOnly: false
70
70
  });
71
71
  return mounts;
@@ -30,6 +30,7 @@ import {
30
30
  sandboxWorkspaceViewPaths
31
31
  } from './workspace-view.ts';
32
32
  import { resolveTaskWorkspace } from './task-resolver.ts';
33
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
33
34
  import {
34
35
  parseSandboxWorkspaceIdentity,
35
36
  sameSandboxWorkspaceIdentity,
@@ -450,11 +451,13 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
450
451
  });
451
452
  }
452
453
  if (snapshot.workspaceTopology === 'legacy-parent') {
454
+ const legacyRoot = snapshot.mounts.some((mount) => mount.path === '/task-workspace')
455
+ ? '/task-workspace' : '/workspace/.agents/workspace';
453
456
  findings.push({
454
457
  repairKind: 'hard-failure',
455
458
  code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
456
459
  message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
457
- path: '/workspace/.agents/workspace'
460
+ path: legacyRoot
458
461
  });
459
462
  }
460
463
  if (snapshot.taskView && !snapshot.taskView.readable) {
@@ -476,7 +479,8 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
476
479
  for (const mount of snapshot.mounts) {
477
480
  if (
478
481
  snapshot.workspaceTopology === 'legacy-parent'
479
- && mount.path.startsWith('/workspace/.agents/workspace')
482
+ && (mount.path.startsWith('/workspace/.agents/workspace')
483
+ || mount.path.startsWith('/task-workspace'))
480
484
  ) continue;
481
485
  if (
482
486
  mount.actualType !== mount.expectedType
@@ -571,13 +575,14 @@ function inspectContainer(
571
575
  }
572
576
 
573
577
  function workspaceTopology(mountsByDestination: ReadonlyMap<string, DockerMount>): SandboxRecoverySnapshot['workspaceTopology'] {
574
- if (mountsByDestination.has('/workspace/.agents/workspace')) return 'legacy-parent';
578
+ if (mountsByDestination.has('/workspace/.agents/workspace')
579
+ || mountsByDestination.has('/task-workspace')) return 'legacy-parent';
575
580
  const perState = [
576
- '/workspace/.agents/workspace/active',
577
- '/workspace/.agents/workspace/active/.short-ids.json',
578
- '/workspace/.agents/workspace/completed',
579
- '/workspace/.agents/workspace/blocked',
580
- '/workspace/.agents/workspace/archive'
581
+ '/task-workspace/active',
582
+ '/task-workspace/active/.short-ids.json',
583
+ '/task-workspace/completed',
584
+ '/task-workspace/blocked',
585
+ '/task-workspace/archive'
581
586
  ];
582
587
  return perState.some((destination) => mountsByDestination.has(destination))
583
588
  ? 'per-state'
@@ -618,8 +623,9 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
618
623
  mountPaths: string[];
619
624
  accessiblePaths: string[];
620
625
  } {
621
- const activePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
622
- const completedPath = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId);
626
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
627
+ const activePath = path.join(taskRoot, 'active', taskId);
628
+ const completedPath = path.join(taskRoot, 'completed', taskId);
623
629
  if (fs.existsSync(activePath) && !fs.existsSync(completedPath)) {
624
630
  const source = assertSandboxTaskSource(repoRoot, taskId);
625
631
  return { mountPaths: [source], accessiblePaths: [source] };
@@ -635,7 +641,7 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
635
641
  }
636
642
 
637
643
  const completedRoot = fs.realpathSync.native(
638
- path.join(repoRoot, '.agents', 'workspace', 'completed')
644
+ path.join(taskRoot, 'completed')
639
645
  );
640
646
  const completedSource = fs.realpathSync.native(path.dirname(task.taskMd));
641
647
  if (path.dirname(completedSource) !== completedRoot) {
@@ -643,9 +649,7 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
643
649
  }
644
650
 
645
651
  const historicalActivePath = path.join(
646
- repoRoot,
647
- '.agents',
648
- 'workspace',
652
+ taskRoot,
649
653
  'active',
650
654
  taskId
651
655
  );
@@ -670,7 +674,7 @@ export function startCompletedSandboxContainer(params: {
670
674
  }): void {
671
675
  const taskSources = recoveryTaskSources(params.config.repoRoot, params.taskId);
672
676
  const completedSource = taskSources.accessiblePaths[0]!;
673
- const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
677
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(params.config.repoRoot), 'active');
674
678
  const historicalSource = path.join(activeRoot, params.taskId);
675
679
  fs.mkdirSync(activeRoot, { recursive: true });
676
680
  let replacedEmptyRoot = false;
@@ -791,11 +795,11 @@ function expectedMounts(params: {
791
795
  expectedType: 'bind' as const,
792
796
  hostPaths: mount.hostPaths,
793
797
  ...(taskSources !== null && taskId !== null
794
- && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
798
+ && mount.containerPath === `/task-workspace/active/${taskId}`
795
799
  ? { sourceAccessiblePaths: taskSources!.accessiblePaths }
796
800
  : {}),
797
801
  ...(completedTaskSource !== null && taskId !== null
798
- && mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
802
+ && mount.containerPath === `/task-workspace/completed/${taskId}`
799
803
  ? { sourceAccessiblePaths: [completedTaskSource] }
800
804
  : {}),
801
805
  expectedRW: !mount.readOnly
@@ -929,7 +933,7 @@ export function collectSandboxRecoverySnapshot(params: {
929
933
  const tools = [...capabilityPlan.tools];
930
934
  const topology = workspaceTopology(mountsByDestination);
931
935
  const taskViewPath = workspace.mode === 'task-bound'
932
- ? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
936
+ ? `/task-workspace/active/${workspace.taskId}/task.md`
933
937
  : null;
934
938
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
935
939
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
@@ -1215,6 +1219,10 @@ function canRetryFreshReadiness(findings: SandboxRecoveryFinding[]): boolean {
1215
1219
  && finding.path !== undefined
1216
1220
  && (
1217
1221
  finding.path.startsWith('/workspace/.agents/workspace')
1222
+ || finding.path.startsWith('/task-workspace/active')
1223
+ || finding.path.startsWith('/task-workspace/blocked')
1224
+ || finding.path.startsWith('/task-workspace/completed')
1225
+ || finding.path.startsWith('/task-workspace/archive')
1218
1226
  || finding.path === '/home/devuser/.host-shell-config'
1219
1227
  || finding.path === '/home/devuser/.bash_aliases'
1220
1228
  || finding.path === '/run/agent-infra/runtime'
@@ -48,7 +48,6 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
48
48
  import type { SandboxTool } from './tools.ts';
49
49
  import { getProcessStartTime } from '../server/process-state.ts';
50
50
  import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
51
- import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
52
51
  import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
53
52
  import { createSandboxControlBindingEvidence } from './task-cleanup.ts';
54
53
  import {
@@ -933,7 +932,7 @@ async function authorizeWorktrees(
933
932
  return permits;
934
933
  }
935
934
 
936
- async function runRmOneUnderRepositoryLock(
935
+ async function runRmOne(
937
936
  config: SandboxConfig,
938
937
  tools: SandboxTool[],
939
938
  branch: string,
@@ -953,10 +952,7 @@ async function rmOne(
953
952
  branch: string,
954
953
  options: RmOneOptions = {}
955
954
  ): Promise<void> {
956
- await withRepositoryMutationLock(
957
- config.repoRoot,
958
- () => runRmOneUnderRepositoryLock(config, tools, branch, options)
959
- );
955
+ await runRmOne(config, tools, branch, options);
960
956
  }
961
957
 
962
958
  async function removeUncheckedSandbox(
@@ -1060,14 +1056,6 @@ async function rmPurge(
1060
1056
  config: SandboxConfig,
1061
1057
  tools: SandboxTool[],
1062
1058
  prompt: PromptDependencies = {}
1063
- ): Promise<void> {
1064
- return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
1065
- }
1066
-
1067
- async function rmPurgeCore(
1068
- config: SandboxConfig,
1069
- tools: SandboxTool[],
1070
- prompt: PromptDependencies = {}
1071
1059
  ): Promise<void> {
1072
1060
  const engine = detectEngine(config);
1073
1061
  const confirm = prompt.confirm ?? p.confirm;
@@ -1142,17 +1130,6 @@ async function rmUnbound(
1142
1130
  config: SandboxConfig,
1143
1131
  tools: SandboxTool[],
1144
1132
  options: { dryRun: boolean; assumeYes: boolean }
1145
- ): Promise<void> {
1146
- return withRepositoryMutationLock(
1147
- config.repoRoot,
1148
- () => rmUnboundCore(config, tools, options)
1149
- );
1150
- }
1151
-
1152
- async function rmUnboundCore(
1153
- config: SandboxConfig,
1154
- tools: SandboxTool[],
1155
- options: { dryRun: boolean; assumeYes: boolean }
1156
1133
  ): Promise<void> {
1157
1134
  const engine = detectEngine(config);
1158
1135
  const listed = fetchSandboxRows(
@@ -1215,7 +1192,7 @@ async function rmUnboundCore(
1215
1192
  return;
1216
1193
  }
1217
1194
  for (const { row, cleanupTarget, target } of targets) {
1218
- await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
1195
+ await runRmOne(config, tools, cleanupTarget.branch, {
1219
1196
  assumeYes: options.assumeYes,
1220
1197
  target,
1221
1198
  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.ts';
5
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
5
6
 
6
7
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
8
  const SHORT_ID_RE = /^\d+$/;
@@ -35,10 +36,11 @@ function stripQuotes(value: string): string {
35
36
  }
36
37
 
37
38
  function readTaskContent(repoRoot: string, taskId: string): { content: string; state: TaskWorkspaceState; taskMd: string } {
39
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
38
40
  for (const state of TASK_WORKSPACE_STATES) {
39
41
  const taskPath = state === 'archive'
40
- ? path.join(repoRoot, '.agents', 'workspace', state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
41
- : path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
42
+ ? path.join(taskRoot, state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
43
+ : path.join(taskRoot, state, taskId, 'task.md');
42
44
  if (fs.existsSync(taskPath)) {
43
45
  return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
44
46
  }
@@ -47,8 +49,9 @@ function readTaskContent(repoRoot: string, taskId: string): { content: string; s
47
49
  }
48
50
 
49
51
  function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspace[] {
52
+ const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
50
53
  const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
51
- const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
54
+ const taskMd = path.join(taskRoot, state, taskId, 'task.md');
52
55
  if (!fs.existsSync(taskMd)) return [];
53
56
  const content = fs.readFileSync(taskMd, 'utf8');
54
57
  return [{
@@ -60,7 +63,7 @@ function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspac
60
63
  });
61
64
  const parts = findArchiveTaskParts(repoRoot, taskId);
62
65
  if (parts.length === 0) return hot;
63
- const taskMd = path.join(repoRoot, '.agents', 'workspace', 'archive', ...parts, 'local', 'task.md');
66
+ const taskMd = path.join(taskRoot, 'archive', ...parts, 'local', 'task.md');
64
67
  if (!fs.existsSync(taskMd)) return hot;
65
68
  const content = fs.readFileSync(taskMd, 'utf8');
66
69
  return [...hot, {
@@ -72,7 +75,7 @@ function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspac
72
75
  }
73
76
 
74
77
  function findArchiveTaskParts(repoRoot: string, taskId: string): string[] {
75
- const archiveRoot = path.join(repoRoot, '.agents', 'workspace', 'archive');
78
+ const archiveRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'archive');
76
79
  if (!fs.existsSync(archiveRoot)) return [];
77
80
  for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
78
81
  if (!/^\d{4}$/.test(year)) continue;
@@ -129,7 +132,7 @@ export function resolveTaskWorkspace(taskId: string, repoRoot: string): TaskWork
129
132
  }
130
133
 
131
134
  export function listTaskWorkspaces(repoRoot: string, state: TaskWorkspaceState): TaskWorkspace[] {
132
- const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
135
+ const stateRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), state);
133
136
  if (state === 'archive') {
134
137
  if (!fs.existsSync(stateRoot)) return [];
135
138
  const rows: TaskWorkspace[] = [];
@@ -1,8 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRemovedHashShortIdInput } from '../task/short-id.ts';
4
- import { resolveTaskRef } from '../task/resolve-ref.ts';
5
3
  import {
4
+ configuredShortIdLength,
5
+ inspectShortIdRegistry,
6
+ isRemovedHashShortIdInput,
7
+ normalizeShortIdInput
8
+ } from '../task/short-id.ts';
9
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
10
+ import {
11
+ TASK_WORKSPACE_STATES,
12
+ listTaskWorkspaces,
6
13
  resolveTaskWorkspace,
7
14
  type TaskWorkspaceState
8
15
  } from './task-resolver.ts';
@@ -51,7 +58,7 @@ function taskBranch(taskMd: string, taskId: string): string {
51
58
  }
52
59
 
53
60
  function activeRegistry(repoRoot: string): Record<string, string> {
54
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
61
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
55
62
  if (!fs.existsSync(registryPath)) return {};
56
63
  const value = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as unknown;
57
64
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -68,7 +75,7 @@ function activeRegistry(repoRoot: string): Record<string, string> {
68
75
  }
69
76
 
70
77
  function taskIdentity(repoRoot: string, taskId: string, registry: Record<string, string>): SandboxWorkspaceIdentity {
71
- const taskMd = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
78
+ const taskMd = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md');
72
79
  if (!fs.existsSync(taskMd)) throw new Error(`Active task not found: ${taskId}`);
73
80
  const shortIds = Object.entries(registry).filter(([, id]) => id === taskId).map(([shortId]) => shortId);
74
81
  if (shortIds.length !== 1) {
@@ -79,10 +86,29 @@ function taskIdentity(repoRoot: string, taskId: string, registry: Record<string,
79
86
 
80
87
  function cleanupTaskId(requestedRef: string, repoRoot: string): string | null {
81
88
  if (TASK_ID_RE.test(requestedRef)) return requestedRef;
82
- if (!SHORT_ID_RE.test(requestedRef)) return null;
83
- const task = resolveTaskRef(requestedRef, { repoRoot });
84
- if (!task.ok) throw new Error(task.message);
85
- return task.taskId;
89
+ const shortIdLength = configuredShortIdLength(repoRoot);
90
+ const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
91
+ if (normalized.kind === 'error') throw new Error(normalized.message);
92
+ if (normalized.kind !== 'shortId') return null;
93
+
94
+ const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
95
+ if (registry.status !== 'valid') throw new Error(registry.error.message);
96
+ const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
97
+ if (!match) {
98
+ throw new Error(`Short id '${normalized.value}' not found in active task registry`);
99
+ }
100
+ return match[0];
101
+ }
102
+
103
+ function cleanupTaskIdByBranch(requestedRef: string, repoRoot: string): string | null {
104
+ const taskIds = new Set(TASK_WORKSPACE_STATES
105
+ .flatMap((state) => listTaskWorkspaces(repoRoot, state))
106
+ .filter((task) => task.branch === requestedRef)
107
+ .map((task) => task.taskId));
108
+ if (taskIds.size > 1) {
109
+ throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
110
+ }
111
+ return taskIds.values().next().value ?? null;
86
112
  }
87
113
 
88
114
  export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
@@ -101,14 +127,14 @@ export function resolveSandboxTarget(requestedRef: string, repoRoot: string): Sa
101
127
  const taskId = canonicalShortId ? registry[canonicalShortId] : undefined;
102
128
  if (!taskId) throw new Error(`Short id '${requestedRef}' not found in active task registry`);
103
129
  const workspace = taskIdentity(repoRoot, taskId, registry);
104
- return { requestedRef, branch: taskBranch(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), taskId), workspace };
130
+ return { requestedRef, branch: taskBranch(path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md'), taskId), workspace };
105
131
  }
106
132
  if (TASK_ID_RE.test(requestedRef)) {
107
133
  const workspace = taskIdentity(repoRoot, requestedRef, registry);
108
- return { requestedRef, branch: taskBranch(path.join(repoRoot, '.agents', 'workspace', 'active', requestedRef, 'task.md'), requestedRef), workspace };
134
+ return { requestedRef, branch: taskBranch(path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', requestedRef, 'task.md'), requestedRef), workspace };
109
135
  }
110
136
 
111
- const activeRoot = path.join(repoRoot, '.agents', 'workspace', 'active');
137
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active');
112
138
  const matches = fs.existsSync(activeRoot)
113
139
  ? fs.readdirSync(activeRoot, { withFileTypes: true }).flatMap((entry) => {
114
140
  if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) return [];
@@ -139,7 +165,8 @@ export function resolveSandboxCleanupTarget(
139
165
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
140
166
  }
141
167
 
142
- const taskId = cleanupTaskId(requestedRef, repoRoot);
168
+ const taskId = cleanupTaskId(requestedRef, repoRoot)
169
+ ?? cleanupTaskIdByBranch(requestedRef, repoRoot);
143
170
  if (taskId) {
144
171
  let task: ReturnType<typeof resolveTaskWorkspace>;
145
172
  try {
@@ -166,14 +193,11 @@ export function resolveSandboxCleanupTarget(
166
193
  };
167
194
  }
168
195
 
169
- const target = resolveSandboxTarget(requestedRef, repoRoot);
170
196
  return {
171
197
  requestedRef,
172
- branch: target.branch,
173
- workspace: target.workspace.mode === 'task-bound'
174
- ? { mode: 'task-bound', taskId: target.workspace.taskId }
175
- : { mode: 'branch-only' },
176
- taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
198
+ branch: requestedRef,
199
+ workspace: { mode: 'branch-only' },
200
+ taskState: 'branch-only'
177
201
  };
178
202
  }
179
203
 
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
5
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
4
6
  import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
5
7
  import { resolveTaskWorkspace } from './task-resolver.ts';
6
8
  import type { SandboxControlManifest } from './control/protocol.ts';
@@ -147,7 +149,8 @@ function assertSafeMountTargetPath(
147
149
 
148
150
  export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
149
151
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
150
- const activeSource = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
152
+ const workspaceRootPath = resolveProjectTaskWorkspaceRoot(repoRoot);
153
+ const activeSource = path.join(workspaceRootPath, 'active', taskId);
151
154
  try {
152
155
  const stat = fs.lstatSync(activeSource);
153
156
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
@@ -155,7 +158,7 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
155
158
  }
156
159
  const canonical = fs.realpathSync.native(activeSource);
157
160
  const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
158
- const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
161
+ const workspaceRoot = fs.realpathSync.native(workspaceRootPath);
159
162
  const relative = path.relative(workspaceRoot, canonical);
160
163
  if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
161
164
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
@@ -173,7 +176,7 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
173
176
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
174
177
  }
175
178
  const canonical = fs.realpathSync.native(source);
176
- const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
179
+ const workspaceRoot = fs.realpathSync.native(workspaceRootPath);
177
180
  const relative = path.relative(workspaceRoot, canonical);
178
181
  if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
179
182
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
@@ -181,30 +184,38 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
181
184
  return canonical;
182
185
  }
183
186
 
184
- export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string, taskId?: string): void {
185
- const resolvedWorktreeRoot = path.resolve(worktreeRoot);
186
- const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
187
- const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
187
+ export function prepareSandboxWorkspaceMountTargets(repoRoot: string): void {
188
+ const resolvedHome = path.resolve(process.env.HOME || os.homedir());
189
+ const canonicalHome = fs.realpathSync.native(resolvedHome);
190
+ const workspaceRoot = path.resolve(resolveProjectTaskWorkspaceRoot(repoRoot));
191
+ const relativeRoot = path.relative(resolvedHome, workspaceRoot);
192
+ if (relativeRoot.startsWith('..') || path.isAbsolute(relativeRoot)) {
193
+ throw new Error(`Sandbox workspace mount target escapes the host home: ${workspaceRoot}`);
194
+ }
188
195
  const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
189
196
  const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
190
- const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
191
- if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
192
- for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
193
- assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
197
+ for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
198
+ assertSafeMountTargetPath(target, resolvedHome, canonicalHome);
194
199
  }
195
200
  fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
196
201
  for (const { hostPath } of statePaths) {
197
202
  fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
198
203
  fs.chmodSync(hostPath, 0o700);
199
204
  }
200
- if (completedTaskTarget) fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
201
205
  fs.chmodSync(workspaceRoot, 0o700);
202
- fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
206
+ const registryFd = fs.openSync(registryTarget, 'a', 0o600);
207
+ try {
208
+ if (fs.fstatSync(registryFd).size === 0) {
209
+ fs.writeFileSync(registryFd, `${JSON.stringify({ version: 1, ids: {} })}\n`);
210
+ }
211
+ } finally {
212
+ fs.closeSync(registryFd);
213
+ }
203
214
  }
204
215
 
205
216
  export function prepareSandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
206
217
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
207
- const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
218
+ const workspaceRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
208
219
  fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
209
220
  const completedRoot = path.join(workspaceRoot, 'completed');
210
221
  const completedStat = fs.lstatSync(completedRoot);
@@ -224,16 +235,13 @@ export function prepareSandboxCompletedTaskMountSource(repoRoot: string, taskId:
224
235
 
225
236
  export function sandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
226
237
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
227
- const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
238
+ const source = path.resolve(resolveProjectTaskWorkspaceRoot(repoRoot), 'completed', taskId);
228
239
  const stat = fs.lstatSync(source);
229
240
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
230
- const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
241
+ const canonicalWorkspace = fs.realpathSync.native(resolveProjectTaskWorkspaceRoot(repoRoot));
231
242
  const canonicalSource = fs.realpathSync.native(source);
232
- const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
233
- const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
234
243
  const relative = path.relative(canonicalWorkspace, canonicalSource);
235
- if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
236
- || path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
244
+ if (path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
237
245
  || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
238
246
  return canonicalSource;
239
247
  }