@fitlab-ai/agent-infra 0.9.10 → 0.9.11

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 (269) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +50 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  8. package/dist/lib/agent-clients/adapters/traecli.js +7 -8
  9. package/dist/lib/agent-clients/config.js +14 -99
  10. package/dist/lib/agent-clients/reconcile.js +1 -11
  11. package/dist/lib/agent-clients/schema.js +5 -4
  12. package/dist/lib/agent-clients/tokens.js +5 -15
  13. package/dist/lib/decide.js +14 -30
  14. package/dist/lib/defaults.json +4 -0
  15. package/dist/lib/init.js +2 -2
  16. package/dist/lib/internal/release-workflow.js +0 -1
  17. package/dist/lib/internal/task-finalization.js +19 -9
  18. package/dist/lib/internal/task-ledger.js +8 -2
  19. package/dist/lib/internal/task-lifecycle.js +17 -93
  20. package/dist/lib/internal/task-orchestration.js +30 -216
  21. package/dist/lib/internal/task-validate.js +2 -1
  22. package/dist/lib/merge.js +205 -103
  23. package/dist/lib/process-data/normalize.js +2 -9
  24. package/dist/lib/run/host.js +46 -16
  25. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  26. package/dist/lib/sandbox/commands/create.js +3 -3
  27. package/dist/lib/sandbox/commands/enter.js +18 -4
  28. package/dist/lib/sandbox/config.js +3 -12
  29. package/dist/lib/sandbox/control/executor.js +131 -39
  30. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  31. package/dist/lib/sandbox/control/server.js +18 -16
  32. package/dist/lib/sandbox/mounts.js +3 -2
  33. package/dist/lib/sandbox/recovery.js +123 -22
  34. package/dist/lib/sandbox/workspace-identity.js +14 -0
  35. package/dist/lib/sandbox/workspace-view.js +8 -9
  36. package/dist/lib/self-update.js +278 -0
  37. package/dist/lib/server/protocol.js +5 -5
  38. package/dist/lib/task/activity-log.js +4 -1
  39. package/dist/lib/task/artifact-lifecycle.js +4 -2
  40. package/dist/lib/task/codex-orchestration.js +1 -1
  41. package/dist/lib/task/command-options.js +1 -3
  42. package/dist/lib/task/commands/cat.js +0 -5
  43. package/dist/lib/task/commands/decisions.js +76 -92
  44. package/dist/lib/task/commands/files.js +6 -6
  45. package/dist/lib/task/commands/grep.js +25 -61
  46. package/dist/lib/task/commands/issue-body.js +6 -6
  47. package/dist/lib/task/commands/log.js +25 -10
  48. package/dist/lib/task/commands/show.js +5 -6
  49. package/dist/lib/task/commands/status.js +8 -8
  50. package/dist/lib/task/commit-operation.js +21 -25
  51. package/dist/lib/task/commit-policy.js +1 -1
  52. package/dist/lib/task/control-authority.js +431 -0
  53. package/dist/lib/task/create.js +6 -1
  54. package/dist/lib/task/current-contract.js +35 -0
  55. package/dist/lib/task/decision-details.js +263 -0
  56. package/dist/lib/task/decision-intents.js +5 -2
  57. package/dist/lib/task/events.js +11 -5
  58. package/dist/lib/task/index.js +19 -20
  59. package/dist/lib/task/ledger-intents.js +8 -9
  60. package/dist/lib/task/ledger.js +13 -6
  61. package/dist/lib/task/lifecycle.js +16 -0
  62. package/dist/lib/task/orchestration.js +50 -13
  63. package/dist/lib/task/review-artifacts.js +31 -12
  64. package/dist/lib/task/review-finalization.js +19 -6
  65. package/dist/lib/task/verification-engine.js +38 -15
  66. package/dist/lib/task/verification-review.js +1 -1
  67. package/dist/lib/task/workspace-snapshot.js +5 -11
  68. package/dist/lib/task/write.js +11 -1
  69. package/dist/lib/update.js +6 -37
  70. package/dist/lib/version.js +5 -1
  71. package/dist/lifecycle-build-manifest.json +3 -3
  72. package/lib/agent-client.ts +0 -1
  73. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  74. package/lib/agent-clients/adapters/traecli.ts +7 -8
  75. package/lib/agent-clients/config.ts +14 -144
  76. package/lib/agent-clients/reconcile.ts +1 -12
  77. package/lib/agent-clients/schema.ts +5 -4
  78. package/lib/agent-clients/tokens.ts +5 -18
  79. package/lib/decide.ts +12 -22
  80. package/lib/defaults.json +4 -0
  81. package/lib/init.ts +2 -2
  82. package/lib/internal/release-workflow.ts +0 -1
  83. package/lib/internal/task-finalization.ts +27 -13
  84. package/lib/internal/task-ledger.ts +8 -2
  85. package/lib/internal/task-lifecycle.ts +33 -76
  86. package/lib/internal/task-orchestration.ts +33 -190
  87. package/lib/internal/task-validate.ts +3 -2
  88. package/lib/merge.ts +236 -120
  89. package/lib/process-data/normalize.ts +2 -8
  90. package/lib/run/host.ts +63 -14
  91. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  92. package/lib/sandbox/commands/create.ts +4 -4
  93. package/lib/sandbox/commands/enter.ts +24 -4
  94. package/lib/sandbox/config.ts +2 -14
  95. package/lib/sandbox/control/container-identity.ts +2 -2
  96. package/lib/sandbox/control/executor.ts +148 -51
  97. package/lib/sandbox/control/lifecycle.ts +34 -28
  98. package/lib/sandbox/control/protocol.ts +1 -3
  99. package/lib/sandbox/control/server.ts +15 -12
  100. package/lib/sandbox/control/state.ts +7 -7
  101. package/lib/sandbox/mounts.ts +4 -3
  102. package/lib/sandbox/recovery.ts +162 -34
  103. package/lib/sandbox/tools.ts +1 -1
  104. package/lib/sandbox/workspace-identity.ts +28 -0
  105. package/lib/sandbox/workspace-view.ts +14 -10
  106. package/lib/self-update.ts +341 -0
  107. package/lib/server/protocol.ts +5 -5
  108. package/lib/task/activity-log.ts +6 -1
  109. package/lib/task/artifact-lifecycle.ts +5 -3
  110. package/lib/task/codex-orchestration.ts +2 -1
  111. package/lib/task/command-options.ts +1 -2
  112. package/lib/task/commands/cat.ts +0 -4
  113. package/lib/task/commands/decisions.ts +80 -75
  114. package/lib/task/commands/files.ts +6 -6
  115. package/lib/task/commands/grep.ts +23 -51
  116. package/lib/task/commands/issue-body.ts +6 -6
  117. package/lib/task/commands/log.ts +22 -10
  118. package/lib/task/commands/show.ts +5 -6
  119. package/lib/task/commands/status.ts +8 -8
  120. package/lib/task/commit-operation.ts +25 -30
  121. package/lib/task/commit-policy.ts +1 -2
  122. package/lib/task/control-authority.ts +563 -0
  123. package/lib/task/create.ts +6 -1
  124. package/lib/task/current-contract.ts +49 -0
  125. package/lib/task/decision-details.ts +340 -0
  126. package/lib/task/decision-intents.ts +7 -2
  127. package/lib/task/events.ts +10 -5
  128. package/lib/task/index.ts +19 -20
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/ledger.ts +17 -7
  131. package/lib/task/lifecycle.ts +20 -0
  132. package/lib/task/orchestration.ts +40 -15
  133. package/lib/task/review-artifacts.ts +53 -15
  134. package/lib/task/review-finalization.ts +33 -7
  135. package/lib/task/verification-engine.ts +33 -18
  136. package/lib/task/verification-review.ts +1 -1
  137. package/lib/task/workspace-snapshot.ts +4 -13
  138. package/lib/task/write.ts +20 -1
  139. package/lib/update.ts +6 -40
  140. package/lib/version.ts +6 -1
  141. package/package.json +1 -1
  142. package/templates/.agents/README.en.md +5 -3
  143. package/templates/.agents/README.zh-CN.md +5 -3
  144. package/templates/.agents/rules/compatibility-policy.en.md +5 -4
  145. package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
  146. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  147. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  148. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  149. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  150. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  151. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  152. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  153. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  154. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  155. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  156. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  157. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  158. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  159. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  160. package/templates/.agents/rules/next-step-output.en.md +3 -3
  161. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  162. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  163. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  164. package/templates/.agents/rules/pr-sync.en.md +1 -1
  165. package/templates/.agents/rules/pr-sync.github.en.md +1 -1
  166. package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
  167. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  168. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  169. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  170. package/templates/.agents/rules/review-handshake.en.md +1 -1
  171. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  172. package/templates/.agents/rules/task-management.en.md +1 -2
  173. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  174. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  175. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  176. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  177. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  178. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  179. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  180. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  183. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  184. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  187. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  188. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  189. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  190. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  191. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  192. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  193. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
  195. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
  198. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  199. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  200. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  201. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  202. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  206. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  207. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  208. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  209. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  210. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  211. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  212. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  213. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  214. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  215. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  216. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  217. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
  218. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
  219. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  220. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  221. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  222. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  223. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
  224. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
  225. package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
  226. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
  227. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
  230. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  231. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  232. package/templates/.agents/templates/task.en.md +1 -1
  233. package/templates/.agents/templates/task.zh-CN.md +1 -1
  234. package/templates/.claude/commands/analyze-task.en.md +1 -1
  235. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  236. package/templates/.claude/commands/block-task.en.md +1 -1
  237. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  238. package/templates/.claude/commands/cancel-task.en.md +1 -1
  239. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  240. package/templates/.claude/commands/check-task.en.md +1 -1
  241. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  242. package/templates/.claude/commands/code-task.en.md +1 -1
  243. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  244. package/templates/.claude/commands/commit.en.md +1 -1
  245. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  246. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  247. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  248. package/templates/.claude/commands/complete-task.en.md +1 -1
  249. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  250. package/templates/.claude/commands/create-pr.en.md +1 -1
  251. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  252. package/templates/.claude/commands/plan-task.en.md +1 -1
  253. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  254. package/templates/.claude/commands/review-analysis.en.md +1 -1
  255. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  256. package/templates/.claude/commands/review-code.en.md +1 -1
  257. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  258. package/templates/.claude/commands/review-plan.en.md +1 -1
  259. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  260. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  261. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  262. package/templates/.claude/commands/run-task.en.md +1 -1
  263. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  264. package/templates/.claude/commands/watch-pr.en.md +1 -1
  265. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  266. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
  267. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
  268. package/dist/lib/builtin-tuis.js +0 -40
  269. package/lib/builtin-tuis.ts +0 -56
package/lib/merge.ts CHANGED
@@ -1,19 +1,18 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok } from './log.ts';
4
3
  import { removeDirRecursive } from './remove-dir.ts';
4
+ import { validateCurrentTaskContract } from './task/current-contract.ts';
5
5
 
6
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
7
7
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
8
8
  const TITLE_RE = /^# (.+)$/m;
9
- const DATE_FROM_PATH_RE = /(?:^|[/\\])(\d{4})[/\\](\d{2})[/\\](\d{2})(?:[/\\]|$)/;
10
9
  const MUTABLE_SECTIONS = ['active', 'blocked', 'completed'] as const;
11
10
  const ALL_SECTIONS = [...MUTABLE_SECTIONS, 'archive'] as const;
12
11
  type MutableSection = typeof MUTABLE_SECTIONS[number];
13
12
  type Section = typeof ALL_SECTIONS[number];
14
13
  type MutableAction = 'copied' | 'updated' | 'moved' | 'skipped';
15
14
  type ArchiveAction = 'copied' | 'skipped';
16
- type SourceMode = 'workspace' | 'legacy-archive';
15
+ type SourceMode = 'workspace';
17
16
  type DateParts = {
18
17
  year: string;
19
18
  month: string;
@@ -129,40 +128,6 @@ function normalizeTaskRecord(taskDir: string, taskFile: string, dateParts: DateP
129
128
  };
130
129
  }
131
130
 
132
- function fallbackDateParts(taskDir: string, content: string): DateParts | null {
133
- const pathMatch = taskDir.match(DATE_FROM_PATH_RE);
134
- if (pathMatch) {
135
- const [, year, month, day] = pathMatch;
136
- if (!year || !month || !day) {
137
- return null;
138
- }
139
- return {
140
- year,
141
- month,
142
- day
143
- };
144
- }
145
-
146
- const completedAt = extractField(content, 'completed_at');
147
- const updatedAt = extractField(content, 'updated_at');
148
- const source = completedAt || updatedAt;
149
- const dateMatch = source?.match(/^(\d{4})-(\d{2})-(\d{2})/);
150
-
151
- if (dateMatch) {
152
- const [, year, month, day] = dateMatch;
153
- if (!year || !month || !day) {
154
- return null;
155
- }
156
- return {
157
- year,
158
- month,
159
- day
160
- };
161
- }
162
-
163
- return null;
164
- }
165
-
166
131
  function scanSourceTasks(sourceDir: string): TaskRecord[] {
167
132
  const tasks: TaskRecord[] = [];
168
133
  const years = fs.existsSync(sourceDir) ? fs.readdirSync(sourceDir, { withFileTypes: true }) : [];
@@ -206,44 +171,6 @@ function scanSourceTasks(sourceDir: string): TaskRecord[] {
206
171
  }
207
172
  }
208
173
 
209
- if (tasks.length > 0) {
210
- return tasks;
211
- }
212
-
213
- // Fall back to a deeper scan if the source layout is unusual but still contains archived tasks.
214
- const stack = [sourceDir];
215
- while (stack.length > 0) {
216
- const currentDir = stack.pop();
217
- if (!currentDir || !fs.existsSync(currentDir)) {
218
- continue;
219
- }
220
-
221
- for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
222
- const entryPath = path.join(currentDir, entry.name);
223
- if (!entry.isDirectory()) {
224
- continue;
225
- }
226
-
227
- if (TASK_ID_RE.test(entry.name)) {
228
- const taskFile = path.join(entryPath, 'task.md');
229
- if (!fs.existsSync(taskFile)) {
230
- continue;
231
- }
232
-
233
- const content = fs.readFileSync(taskFile, 'utf8');
234
- const dateParts = fallbackDateParts(entryPath, content);
235
- if (!dateParts) {
236
- continue;
237
- }
238
-
239
- tasks.push(normalizeTaskRecord(entryPath, taskFile, dateParts));
240
- continue;
241
- }
242
-
243
- stack.push(entryPath);
244
- }
245
- }
246
-
247
174
  return tasks.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
248
175
  }
249
176
 
@@ -624,15 +551,230 @@ function copyTaskToSection(sourceTask: WorkspaceRecord, workspaceDir: string): s
624
551
  return destinationDir;
625
552
  }
626
553
 
554
+ function invalidSource(message: string): never {
555
+ throw new Error(`Invalid merge source: ${message}`);
556
+ }
557
+
558
+ function sourceRelativePath(sourceRoot: string, targetPath: string): string {
559
+ return path.relative(sourceRoot, targetPath).split(path.sep).join('/') || '.';
560
+ }
561
+
562
+ function isMissingPathError(error: unknown): boolean {
563
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
564
+ }
565
+
566
+ function assertSourcePathContained(sourceRoot: string, targetPath: string): void {
567
+ let sourceRealPath: string;
568
+ let targetRealPath: string;
569
+ try {
570
+ sourceRealPath = fs.realpathSync(sourceRoot);
571
+ targetRealPath = fs.realpathSync(targetPath);
572
+ } catch (error) {
573
+ invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} cannot be resolved: ${error instanceof Error ? error.message : String(error)}`);
574
+ }
575
+
576
+ const relativePath = path.relative(sourceRealPath, targetRealPath);
577
+ if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
578
+ invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} resolves outside the source workspace`);
579
+ }
580
+ }
581
+
582
+ function sourcePathStat(sourceRoot: string, targetPath: string): fs.Stats | null {
583
+ let stat: fs.Stats;
584
+ try {
585
+ stat = fs.lstatSync(targetPath);
586
+ } catch (error) {
587
+ if (isMissingPathError(error)) {
588
+ return null;
589
+ }
590
+ throw error;
591
+ }
592
+
593
+ if (stat.isSymbolicLink()) {
594
+ invalidSource(`${sourceRelativePath(sourceRoot, targetPath)} must not be a symbolic link`);
595
+ }
596
+ assertSourcePathContained(sourceRoot, targetPath);
597
+ return stat;
598
+ }
599
+
600
+ function validateTaskTree(sourceRoot: string, taskDir: string): void {
601
+ for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
602
+ const entryPath = path.join(taskDir, entry.name);
603
+ const entryStat = sourcePathStat(sourceRoot, entryPath);
604
+ if (!entryStat) {
605
+ invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} disappeared during validation`);
606
+ }
607
+ if (entryStat.isDirectory()) {
608
+ validateTaskTree(sourceRoot, entryPath);
609
+ continue;
610
+ }
611
+ if (!entryStat.isFile()) {
612
+ invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} must be a regular file or directory`);
613
+ }
614
+ }
615
+ }
616
+
617
+ function validateSourceRoot(sourceRoot: string): void {
618
+ const stat = sourcePathStat(sourceRoot, sourceRoot);
619
+ if (!stat || !stat.isDirectory()) {
620
+ invalidSource('source path must be a real directory');
621
+ }
622
+ }
623
+
624
+ function validateTaskDirectory(
625
+ sourceRoot: string,
626
+ taskDir: string,
627
+ seenTaskIds: Map<string, string>
628
+ ): void {
629
+ const taskId = path.basename(taskDir);
630
+ const taskFile = path.join(taskDir, 'task.md');
631
+ const taskDirStat = sourcePathStat(sourceRoot, taskDir);
632
+ if (!taskDirStat || !taskDirStat.isDirectory()) {
633
+ invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
634
+ }
635
+ const taskFileStat = sourcePathStat(sourceRoot, taskFile);
636
+ if (!taskFileStat || !taskFileStat.isFile()) {
637
+ invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} requires a regular task.md file`);
638
+ }
639
+
640
+ const content = fs.readFileSync(taskFile, 'utf8');
641
+ const contract = validateCurrentTaskContract(content);
642
+ if (!contract.ok) {
643
+ invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} is not a valid current task: ${contract.message}`);
644
+ }
645
+ if (contract.metadata.id !== taskId) {
646
+ invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
647
+ }
648
+ validateTaskTree(sourceRoot, taskDir);
649
+
650
+ const previousPath = seenTaskIds.get(taskId);
651
+ if (previousPath) {
652
+ invalidSource(`duplicate task ID ${taskId} at ${previousPath} and ${sourceRelativePath(sourceRoot, taskDir)}`);
653
+ }
654
+ seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
655
+ }
656
+
657
+ function validateMutableSection(
658
+ sourceRoot: string,
659
+ section: MutableSection,
660
+ seenTaskIds: Map<string, string>
661
+ ): void {
662
+ const sectionDir = path.join(sourceRoot, section);
663
+ const sectionStat = sourcePathStat(sourceRoot, sectionDir);
664
+ if (!sectionStat) {
665
+ return;
666
+ }
667
+ if (!sectionStat.isDirectory()) {
668
+ invalidSource(`${section} must be a directory`);
669
+ }
670
+
671
+ for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) {
672
+ const entryPath = path.join(sectionDir, entry.name);
673
+ const entryStat = sourcePathStat(sourceRoot, entryPath);
674
+ if (!entryStat) {
675
+ continue;
676
+ }
677
+ if (!entryStat.isDirectory()) {
678
+ if (TASK_ID_RE.test(entry.name)) {
679
+ invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} is not a directory`);
680
+ }
681
+ continue;
682
+ }
683
+ if (!TASK_ID_RE.test(entry.name)) {
684
+ invalidSource(`${sourceRelativePath(sourceRoot, entryPath)} is not a direct TASK-YYYYMMDD-HHMMSS directory`);
685
+ }
686
+ validateTaskDirectory(sourceRoot, entryPath, seenTaskIds);
687
+ }
688
+ }
689
+
690
+ function validateArchiveSection(
691
+ sourceRoot: string,
692
+ seenTaskIds: Map<string, string>
693
+ ): void {
694
+ const archiveDir = path.join(sourceRoot, 'archive');
695
+ const archiveStat = sourcePathStat(sourceRoot, archiveDir);
696
+ if (!archiveStat) {
697
+ return;
698
+ }
699
+ if (!archiveStat.isDirectory()) {
700
+ invalidSource('archive must be a directory');
701
+ }
702
+
703
+ const allowManifest = (directory: string, entry: fs.Dirent): boolean => {
704
+ const entryPath = path.join(directory, entry.name);
705
+ const entryStat = sourcePathStat(sourceRoot, entryPath);
706
+ return Boolean(entryStat?.isFile() && entry.name === 'manifest.md');
707
+ };
708
+ const years = fs.readdirSync(archiveDir, { withFileTypes: true });
709
+ for (const yearEntry of years) {
710
+ const yearPath = path.join(archiveDir, yearEntry.name);
711
+ if (allowManifest(archiveDir, yearEntry)) {
712
+ continue;
713
+ }
714
+ const yearStat = sourcePathStat(sourceRoot, yearPath);
715
+ if (!yearStat?.isDirectory() || !/^\d{4}$/.test(yearEntry.name)) {
716
+ invalidSource(`${sourceRelativePath(sourceRoot, yearPath)} is not a YYYY directory or manifest.md`);
717
+ }
718
+
719
+ const yearDir = yearPath;
720
+ for (const monthEntry of fs.readdirSync(yearDir, { withFileTypes: true })) {
721
+ const monthPath = path.join(yearDir, monthEntry.name);
722
+ if (allowManifest(yearDir, monthEntry)) {
723
+ continue;
724
+ }
725
+ const monthStat = sourcePathStat(sourceRoot, monthPath);
726
+ if (!monthStat?.isDirectory() || !/^\d{2}$/.test(monthEntry.name)) {
727
+ invalidSource(`${sourceRelativePath(sourceRoot, monthPath)} is not a MM directory or manifest.md`);
728
+ }
729
+
730
+ const monthDir = monthPath;
731
+ for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
732
+ const dayPath = path.join(monthDir, dayEntry.name);
733
+ if (allowManifest(monthDir, dayEntry)) {
734
+ continue;
735
+ }
736
+ const dayStat = sourcePathStat(sourceRoot, dayPath);
737
+ if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
738
+ invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
739
+ }
740
+
741
+ const dayDir = dayPath;
742
+ for (const taskEntry of fs.readdirSync(dayDir, { withFileTypes: true })) {
743
+ const taskPath = path.join(dayDir, taskEntry.name);
744
+ const taskStat = sourcePathStat(sourceRoot, taskPath);
745
+ if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
746
+ invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
747
+ }
748
+ validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
749
+ }
750
+ }
751
+ }
752
+ }
753
+ }
754
+
755
+ function validateSourceWorkspace(sourcePath: string): void {
756
+ validateSourceRoot(sourcePath);
757
+ const seenTaskIds = new Map<string, string>();
758
+ for (const section of MUTABLE_SECTIONS) {
759
+ validateMutableSection(sourcePath, section, seenTaskIds);
760
+ }
761
+ validateArchiveSection(sourcePath, seenTaskIds);
762
+ }
763
+
627
764
  function detectSourceMode(sourcePath: string): SourceMode {
765
+ validateSourceRoot(sourcePath);
628
766
  for (const section of ALL_SECTIONS) {
629
767
  const sectionDir = path.join(sourcePath, section);
630
- if (fs.existsSync(sectionDir) && fs.statSync(sectionDir).isDirectory()) {
768
+ const sectionStat = sourcePathStat(sourcePath, sectionDir);
769
+ if (sectionStat?.isDirectory()) {
631
770
  return 'workspace';
632
771
  }
772
+ if (sectionStat) {
773
+ invalidSource(`${section} must be a directory`);
774
+ }
633
775
  }
634
776
 
635
- return 'legacy-archive';
777
+ invalidSource('expected a current workspace containing active, blocked, completed, or archive sections');
636
778
  }
637
779
 
638
780
  function createReport(sourcePath: string, backupRoot: string): MergeReport {
@@ -778,29 +920,6 @@ function mergeArchiveSection(sourceArchive: string, localArchive: string, report
778
920
  return sourceTasks.length;
779
921
  }
780
922
 
781
- function printLegacyArchiveMessages(report: MergeReport, sourcePath: string): void {
782
- const merged = report.sections.archive.copied;
783
- const skipped = report.sections.archive.skipped;
784
-
785
- if (merged.length === 0 && skipped.length === 0) {
786
- info(`No archived tasks found in ${sourcePath}`);
787
- }
788
-
789
- for (const task of merged) {
790
- ok(`Merged ${task.taskId} -> ${task.relativePath}`);
791
- }
792
-
793
- for (const task of skipped) {
794
- info(`Skipped ${task.taskId} (already exists at ${task.relativePath})`);
795
- }
796
-
797
- process.stdout.write('\n');
798
- info('Merge summary');
799
- info(`- Merged: ${merged.length}`);
800
- info(`- Skipped: ${skipped.length}`);
801
- process.stdout.write('\n');
802
- }
803
-
804
923
  function printSection(lines: string[], name: MutableSection, counts: MutableCounts): void {
805
924
  const title = `${SECTION_LABELS[name].padEnd(9, ' ')} (.agents/workspace/${name}/):`;
806
925
  lines.push(title);
@@ -910,7 +1029,11 @@ async function cmdMerge(args: string[]): Promise<void> {
910
1029
  throw new Error(`Source path does not exist: ${sourcePath}`);
911
1030
  }
912
1031
 
913
- if (!fs.statSync(resolvedSource).isDirectory()) {
1032
+ if (fs.lstatSync(resolvedSource).isSymbolicLink()) {
1033
+ throw new Error(`Invalid merge source: source path must be a real directory: ${sourcePath}`);
1034
+ }
1035
+
1036
+ if (!fs.lstatSync(resolvedSource).isDirectory()) {
914
1037
  throw new Error(`Source path is not a directory: ${sourcePath}`);
915
1038
  }
916
1039
 
@@ -920,35 +1043,28 @@ async function cmdMerge(args: string[]): Promise<void> {
920
1043
  const backupRootRelative = `.agents/workspace/.merge-backup/${backupStamp}/`;
921
1044
  const backupRoot = path.join(workspaceDir, '.merge-backup', backupStamp);
922
1045
  const report = createReport(resolvedSource, backupRootRelative);
923
- const mode = detectSourceMode(resolvedSource);
1046
+ detectSourceMode(resolvedSource);
1047
+ validateSourceWorkspace(resolvedSource);
924
1048
 
925
1049
  for (const section of ALL_SECTIONS) {
926
1050
  fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
927
1051
  }
928
1052
 
929
- if (mode === 'legacy-archive') {
930
- info('Detected legacy archive source; treating the input as archive-only for backward compatibility.');
931
- mergeArchiveSection(resolvedSource, archiveDir, report);
932
- } else {
933
- mergeMutableSections({
934
- sourceWorkspace: resolvedSource,
935
- localWorkspace: workspaceDir,
936
- backupRoot,
937
- report
938
- });
1053
+ mergeMutableSections({
1054
+ sourceWorkspace: resolvedSource,
1055
+ localWorkspace: workspaceDir,
1056
+ backupRoot,
1057
+ report
1058
+ });
939
1059
 
940
- const sourceArchive = path.join(resolvedSource, 'archive');
941
- if (fs.existsSync(sourceArchive) && fs.statSync(sourceArchive).isDirectory()) {
942
- mergeArchiveSection(sourceArchive, archiveDir, report);
943
- }
1060
+ const sourceArchive = path.join(resolvedSource, 'archive');
1061
+ const sourceArchiveStat = sourcePathStat(resolvedSource, sourceArchive);
1062
+ if (sourceArchiveStat?.isDirectory()) {
1063
+ mergeArchiveSection(sourceArchive, archiveDir, report);
944
1064
  }
945
1065
 
946
1066
  rebuildManifests(archiveDir);
947
1067
 
948
- if (mode === 'legacy-archive') {
949
- printLegacyArchiveMessages(report, sourcePath);
950
- }
951
-
952
1068
  printReport(report);
953
1069
  }
954
1070
 
@@ -1,4 +1,5 @@
1
1
  import { sha256 } from './store.ts';
2
+ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
2
3
 
3
4
  import type { CapturedObject, JsonValue, NormalizedKind, NormalizedRecord } from './types.ts';
4
5
 
@@ -14,14 +15,7 @@ function recordId(kind: NormalizedKind, identity: string, suffix = ''): string {
14
15
 
15
16
  function canonicalActor(raw: string): string | null {
16
17
  const lowered = raw.toLowerCase();
17
- const aliases: Record<string, string> = {
18
- 'claude-code': 'claude',
19
- 'antigravity-cli': 'antigravity',
20
- 'gemini-cli': 'gemini',
21
- gemini: 'gemini'
22
- };
23
- if (['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'human'].includes(lowered)) return lowered;
24
- return aliases[lowered] ?? null;
18
+ return normalizeAgentToken(lowered);
25
19
  }
26
20
 
27
21
  function nextOccurrence(occurrences: Map<string, number>, identity: string): string {
package/lib/run/host.ts CHANGED
@@ -9,27 +9,74 @@ export type RunProcessResult = {
9
9
  stderr?: string;
10
10
  };
11
11
 
12
- function resolveCommand(file: string): string {
13
- if (process.platform !== 'win32' || path.extname(file)) {
14
- return file;
15
- }
12
+ type CommandResolution = Readonly<{
13
+ path: string;
14
+ source: 'absolute' | 'path' | 'current-directory' | 'unresolved';
15
+ pathEntry?: string;
16
+ }>;
17
+
18
+ function resolveCommandDetails(
19
+ file: string,
20
+ platform: NodeJS.Platform = process.platform,
21
+ env: NodeJS.ProcessEnv = process.env
22
+ ): CommandResolution {
23
+ if (path.isAbsolute(file)) return { path: file, source: 'absolute' };
16
24
 
17
- const pathValue = process.env.Path || process.env.PATH || '';
18
- const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
19
- for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
25
+ const pathValue = platform === 'win32'
26
+ ? env.Path || env.PATH || ''
27
+ : env.PATH || '';
28
+ const extensions = platform === 'win32'
29
+ ? (path.extname(file)
30
+ ? ['']
31
+ : (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean))
32
+ : [''];
33
+ for (const dir of pathValue.split(path.delimiter)) {
20
34
  for (const extension of extensions) {
21
- const lowerCandidate = path.join(dir, `${file}${extension.toLowerCase()}`);
22
- if (fs.existsSync(lowerCandidate)) return lowerCandidate;
23
- const upperCandidate = path.join(dir, `${file}${extension.toUpperCase()}`);
24
- if (fs.existsSync(upperCandidate)) return upperCandidate;
35
+ const candidates = extension
36
+ ? [`${file}${extension.toLowerCase()}`, `${file}${extension.toUpperCase()}`]
37
+ : [file];
38
+ for (const candidateName of candidates) {
39
+ const candidate = path.resolve(dir || '.', candidateName);
40
+ try {
41
+ const stat = fs.statSync(candidate);
42
+ if (!stat.isFile()) continue;
43
+ if (platform !== 'win32' && (stat.mode & 0o111) === 0) continue;
44
+ return {
45
+ path: candidate,
46
+ source: dir ? 'path' : 'current-directory',
47
+ pathEntry: dir
48
+ };
49
+ } catch {
50
+ // Keep searching the remaining PATH entries.
51
+ }
52
+ }
25
53
  }
26
54
  }
27
55
 
28
- return file;
56
+ return { path: file, source: 'unresolved' };
57
+ }
58
+
59
+ function resolveCommand(
60
+ file: string,
61
+ platform: NodeJS.Platform = process.platform,
62
+ env: NodeJS.ProcessEnv = process.env
63
+ ): string {
64
+ return resolveCommandDetails(file, platform, env).path;
29
65
  }
30
66
 
31
- function needsShell(file: string): boolean {
32
- return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
67
+ function resolveCommandFromAbsolutePath(
68
+ file: string,
69
+ platform: NodeJS.Platform = process.platform,
70
+ env: NodeJS.ProcessEnv = process.env
71
+ ): string | null {
72
+ const resolution = resolveCommandDetails(file, platform, env);
73
+ return resolution.source === 'path' && resolution.pathEntry && path.isAbsolute(resolution.pathEntry)
74
+ ? resolution.path
75
+ : null;
76
+ }
77
+
78
+ function needsShell(file: string, platform: NodeJS.Platform = process.platform): boolean {
79
+ return platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
33
80
  }
34
81
 
35
82
  export async function runHostCommand(command: string[]): Promise<RunProcessResult> {
@@ -45,3 +92,5 @@ export async function runHostCommand(command: string[]): Promise<RunProcessResul
45
92
  child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
46
93
  });
47
94
  }
95
+
96
+ export { needsShell, resolveCommand, resolveCommandFromAbsolutePath };
@@ -28,7 +28,7 @@ type SandboxCapabilityConfig = Readonly<{
28
28
  project: string;
29
29
  tools: readonly string[];
30
30
  customTools?: readonly SandboxTool[];
31
- agentClientState?: AgentClientState;
31
+ agentClientState: AgentClientState;
32
32
  }>;
33
33
 
34
34
  type SandboxRuntimeCapabilityProjection = Readonly<{
@@ -98,18 +98,6 @@ function agentInfraTool(home: string): SandboxTool {
98
98
  };
99
99
  }
100
100
 
101
- function compatAgentClientState(tools: readonly string[]): AgentClientState {
102
- return Object.fromEntries(
103
- listAgentClientAdapters().map((adapter) => [
104
- adapter.id,
105
- {
106
- enabled: true,
107
- installInSandbox: tools.includes(adapter.id)
108
- }
109
- ])
110
- ) as AgentClientState;
111
- }
112
-
113
101
  function stableValue(value: unknown): unknown {
114
102
  if (Array.isArray(value)) {
115
103
  return value.map(stableValue);
@@ -206,7 +194,7 @@ function hooksByPhase(
206
194
  function createSandboxCapabilityPlan(
207
195
  config: SandboxCapabilityConfig
208
196
  ): SandboxCapabilityPlan {
209
- const state = config.agentClientState ?? compatAgentClientState(config.tools);
197
+ const state = config.agentClientState;
210
198
  const selectedAgentClients = listInstalledAgentClientAdapters(state);
211
199
  const context = { home: config.home, project: config.project };
212
200
  const selectedTools = selectedAgentClients.map((adapter) =>
@@ -228,7 +216,9 @@ function createSandboxCapabilityPlan(
228
216
  const nonClientTools: SandboxTool[] = [];
229
217
  const selectedCustomTools: SandboxTool[] = [];
230
218
  for (const id of config.tools) {
231
- if (isAgentClientId(id)) continue;
219
+ if (isAgentClientId(id)) {
220
+ throw new Error(`Agent Client '${id}' must be provided by agentClientState`);
221
+ }
232
222
  if (id === 'agent-infra') {
233
223
  if (!nonClientTools.some((tool) => tool.id === id)) {
234
224
  nonClientTools.push(agentInfraTool(config.home));
@@ -77,7 +77,7 @@ import {
77
77
  commitSandboxControlReplacement,
78
78
  quiesceSandboxControlRoot,
79
79
  recoverSandboxControlReplacement,
80
- readSandboxControlManifestForTransition
80
+ readSandboxControlManifest
81
81
  } from '../control/lifecycle.ts';
82
82
  import { inspectSandboxControlContainer } from '../control/container-identity.ts';
83
83
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
@@ -1250,7 +1250,7 @@ export async function create(args: string[]): Promise<void> {
1250
1250
  const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
1251
1251
  const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
1252
1252
  const previousManifest = fs.existsSync(controlPaths.manifestPath)
1253
- ? readSandboxControlManifestForTransition(controlPaths.manifestPath)
1253
+ ? readSandboxControlManifest(controlPaths.manifestPath)
1254
1254
  : null;
1255
1255
  if (hadExistingControlRoot) {
1256
1256
  await quiesceSandboxControlRoot(controlPaths.root);
@@ -1423,10 +1423,10 @@ export async function create(args: string[]): Promise<void> {
1423
1423
  ...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
1424
1424
  ...(target.workspace.mode === 'task-bound'
1425
1425
  ? {
1426
- taskSource: assertSandboxTaskSource(
1426
+ taskSources: [assertSandboxTaskSource(
1427
1427
  effectiveConfig.repoRoot,
1428
1428
  target.workspace.taskId
1429
- ),
1429
+ )],
1430
1430
  taskId: target.workspace.taskId
1431
1431
  }
1432
1432
  : {})
@@ -3,7 +3,9 @@ import {
3
3
  assertValidBranchName,
4
4
  containerNameCandidates,
5
5
  sandboxBranchLabel,
6
- sandboxLabel
6
+ sandboxLabel,
7
+ sandboxTaskIdLabel,
8
+ sandboxWorkspaceModeLabel
7
9
  } from '../constants.ts';
8
10
  import { detectEngine } from '../engine.ts';
9
11
  import { ensureSandboxReady } from '../recovery.ts';
@@ -21,7 +23,10 @@ import {
21
23
  fetchSandboxRows,
22
24
  selectSandboxContainer,
23
25
  } from './list-running.ts';
24
- import { resolveSandboxTarget } from '../workspace-identity.ts';
26
+ import {
27
+ resolveSandboxReentryContext,
28
+ resolveSandboxTarget
29
+ } from '../workspace-identity.ts';
25
30
 
26
31
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
27
32
 
@@ -128,7 +133,11 @@ export async function enter(args: string[]): Promise<number> {
128
133
  const { running, nonRunning } = fetchSandboxRows(
129
134
  engine,
130
135
  sandboxLabel(config),
131
- sandboxBranchLabel(config)
136
+ sandboxBranchLabel(config),
137
+ {
138
+ mode: sandboxWorkspaceModeLabel(config),
139
+ taskId: sandboxTaskIdLabel(config)
140
+ }
132
141
  );
133
142
  const found = selectSandboxContainer(
134
143
  [...running, ...nonRunning],
@@ -140,11 +149,22 @@ export async function enter(args: string[]): Promise<number> {
140
149
  `No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`
141
150
  );
142
151
  }
152
+ const containerWorkspace = found.workspaceMode === 'task-bound' && found.taskId
153
+ ? { mode: 'task-bound' as const, taskId: found.taskId }
154
+ : found.workspaceMode === 'branch-only'
155
+ ? { mode: 'branch-only' as const }
156
+ : { mode: 'legacy-invalid' as const };
157
+ const reentry = resolveSandboxReentryContext({
158
+ target,
159
+ containerWorkspace,
160
+ repoRoot: config.repoRoot
161
+ });
143
162
  const ready = await ensureSandboxReady({
144
163
  config,
145
164
  engine,
146
165
  branch,
147
- workspace: target.workspace,
166
+ workspace: reentry.workspace,
167
+ reentry: reentry.reentry,
148
168
  row: found,
149
169
  allowRecreate: parsed.recreate,
150
170
  recreate: async () => {