@fitlab-ai/agent-infra 0.11.15 → 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 (253) 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-ledger.js +1 -12
  14. package/dist/lib/internal/task-operation-registry.js +2 -1
  15. package/dist/lib/internal/task-qualification.js +6 -15
  16. package/dist/lib/internal/task-review.js +3 -16
  17. package/dist/lib/internal/task-short-id.js +2 -1
  18. package/dist/lib/internal/task-verify.js +1 -11
  19. package/dist/lib/internal/task-warning.js +1 -12
  20. package/dist/lib/internal/task-workspace.js +43 -0
  21. package/dist/lib/merge.js +4 -3
  22. package/dist/lib/platform/pr-summary.js +6 -15
  23. package/dist/lib/platform/pull-requests.js +3 -15
  24. package/dist/lib/pr-review/evidence-grading.js +3 -2
  25. package/dist/lib/process-data/index.js +5 -4
  26. package/dist/lib/process-data/sources.js +6 -0
  27. package/dist/lib/run/index.js +3 -2
  28. package/dist/lib/sandbox/commands/create.js +5 -1
  29. package/dist/lib/sandbox/commands/enter.js +1 -1
  30. package/dist/lib/sandbox/commands/list-running.js +2 -1
  31. package/dist/lib/sandbox/control/client.js +8 -13
  32. package/dist/lib/sandbox/control/completed-reentry.js +2 -1
  33. package/dist/lib/sandbox/control/task-view.js +4 -2
  34. package/dist/lib/sandbox/mounts.js +4 -4
  35. package/dist/lib/sandbox/recovery.js +26 -16
  36. package/dist/lib/sandbox/removal.js +3 -10
  37. package/dist/lib/sandbox/task-resolver.js +9 -6
  38. package/dist/lib/sandbox/workspace-identity.js +6 -5
  39. package/dist/lib/sandbox/workspace-view.js +29 -22
  40. package/dist/lib/task/activity-intent.js +2 -12
  41. package/dist/lib/task/artifact-lifecycle.js +17 -11
  42. package/dist/lib/task/artifact-operations.js +41 -49
  43. package/dist/lib/task/capabilities.js +2 -2
  44. package/dist/lib/task/commands/ls.js +4 -3
  45. package/dist/lib/task/commands/status.js +1 -1
  46. package/dist/lib/task/commit-operation.js +6 -6
  47. package/dist/lib/task/control-authority.js +32 -32
  48. package/dist/lib/task/create-service.js +2 -4
  49. package/dist/lib/task/create.js +12 -65
  50. package/dist/lib/task/delegation-receipts.js +1 -1
  51. package/dist/lib/task/delivery.js +2 -8
  52. package/dist/lib/task/events.js +5 -31
  53. package/dist/lib/task/finalization.js +17 -22
  54. package/dist/lib/task/lifecycle.js +9 -48
  55. package/dist/lib/task/local-artifact-finalization.js +1 -2
  56. package/dist/lib/task/orchestration.js +24 -57
  57. package/dist/lib/task/platform-operation-recovery.js +1 -2
  58. package/dist/lib/task/resolve-ref.js +17 -12
  59. package/dist/lib/task/review-finalization.js +2 -35
  60. package/dist/lib/task/review-identity.js +3 -1
  61. package/dist/lib/task/short-id.js +10 -8
  62. package/dist/lib/task/verification-engine.js +19 -58
  63. package/dist/lib/task/workspace-root.js +59 -0
  64. package/dist/lib/task/workspace-snapshot.js +4 -3
  65. package/dist/lifecycle-build-manifest.json +3 -3
  66. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
  67. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
  68. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
  69. package/lib/decide.ts +2 -7
  70. package/lib/internal/cli-route-inventory.ts +2 -0
  71. package/lib/internal/manual-validation.ts +8 -19
  72. package/lib/internal/platform-comment.ts +3 -2
  73. package/lib/internal/task-activity.ts +1 -10
  74. package/lib/internal/task-event.ts +1 -10
  75. package/lib/internal/task-ledger.ts +1 -10
  76. package/lib/internal/task-operation-registry.ts +2 -1
  77. package/lib/internal/task-qualification.ts +5 -11
  78. package/lib/internal/task-review.ts +3 -14
  79. package/lib/internal/task-short-id.ts +2 -1
  80. package/lib/internal/task-verify.ts +1 -9
  81. package/lib/internal/task-warning.ts +1 -10
  82. package/lib/internal/task-workspace.ts +39 -0
  83. package/lib/merge.ts +4 -3
  84. package/lib/platform/pr-summary.ts +7 -16
  85. package/lib/platform/pull-requests.ts +3 -24
  86. package/lib/pr-review/evidence-grading.ts +3 -2
  87. package/lib/process-data/index.ts +4 -3
  88. package/lib/process-data/sources.ts +6 -0
  89. package/lib/run/index.ts +3 -2
  90. package/lib/sandbox/commands/create.ts +5 -1
  91. package/lib/sandbox/commands/enter.ts +1 -1
  92. package/lib/sandbox/commands/list-running.ts +2 -1
  93. package/lib/sandbox/control/client.ts +7 -12
  94. package/lib/sandbox/control/completed-reentry.ts +2 -1
  95. package/lib/sandbox/control/task-view.ts +4 -2
  96. package/lib/sandbox/mounts.ts +4 -4
  97. package/lib/sandbox/recovery.ts +26 -18
  98. package/lib/sandbox/removal.ts +3 -26
  99. package/lib/sandbox/task-resolver.ts +9 -6
  100. package/lib/sandbox/workspace-identity.ts +6 -5
  101. package/lib/sandbox/workspace-view.ts +28 -20
  102. package/lib/task/activity-intent.ts +3 -17
  103. package/lib/task/artifact-lifecycle.ts +39 -11
  104. package/lib/task/artifact-operations.ts +28 -35
  105. package/lib/task/capabilities.ts +2 -2
  106. package/lib/task/commands/ls.ts +4 -3
  107. package/lib/task/commands/status.ts +1 -1
  108. package/lib/task/commit-operation.ts +5 -6
  109. package/lib/task/control-authority.ts +29 -35
  110. package/lib/task/create-service.ts +2 -4
  111. package/lib/task/create.ts +10 -51
  112. package/lib/task/delegation-receipts.ts +1 -1
  113. package/lib/task/delivery.ts +2 -10
  114. package/lib/task/events.ts +5 -32
  115. package/lib/task/finalization.ts +16 -24
  116. package/lib/task/lifecycle.ts +9 -50
  117. package/lib/task/local-artifact-finalization.ts +1 -4
  118. package/lib/task/orchestration.ts +31 -80
  119. package/lib/task/platform-operation-recovery.ts +1 -2
  120. package/lib/task/resolve-ref.ts +18 -12
  121. package/lib/task/review-finalization.ts +2 -52
  122. package/lib/task/review-identity.ts +3 -1
  123. package/lib/task/short-id.ts +10 -8
  124. package/lib/task/verification-engine.ts +20 -62
  125. package/lib/task/workspace-root.ts +78 -0
  126. package/lib/task/workspace-snapshot.ts +4 -3
  127. package/package.json +1 -1
  128. package/templates/.agents/QUICKSTART.en.md +19 -8
  129. package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
  130. package/templates/.agents/README.en.md +10 -15
  131. package/templates/.agents/README.zh-CN.md +9 -13
  132. package/templates/.agents/rules/issue-sync.en.md +1 -1
  133. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  134. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  135. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  136. package/templates/.agents/rules/next-step-output.en.md +2 -2
  137. package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
  138. package/templates/.agents/rules/pr-sync.en.md +1 -1
  139. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  140. package/templates/.agents/rules/release-commands.en.md +3 -0
  141. package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
  142. package/templates/.agents/rules/task-management.en.md +0 -2
  143. package/templates/.agents/rules/task-management.zh-CN.md +0 -2
  144. package/templates/.agents/rules/task-short-id.en.md +2 -2
  145. package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
  146. package/templates/.agents/scripts/task-short-id.js +2 -8
  147. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  148. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  149. package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
  150. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
  151. package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
  152. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
  153. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
  154. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
  156. package/templates/.agents/skills/block-task/config/verify.json +0 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -6
  158. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -6
  159. package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
  160. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  161. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  162. package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
  163. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
  164. package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  168. package/templates/.agents/skills/commit/config/verify.json +0 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  175. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
  176. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
  177. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  178. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  179. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -7
  180. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +8 -8
  181. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  182. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  183. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  184. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  185. package/templates/.agents/skills/create-pr/config/verify.json +0 -1
  186. package/templates/.agents/skills/create-pr/reference/change-report.en.md +1 -1
  187. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +1 -1
  188. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  189. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  190. package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
  191. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
  192. package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/create-task/config/verify.json +0 -1
  195. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  196. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  197. package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
  198. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
  199. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  200. package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
  201. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  202. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  203. package/templates/.agents/skills/import-issue/config/verify.json +0 -1
  204. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  205. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  206. package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
  207. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
  208. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  209. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
  210. package/templates/.agents/skills/restore-task/config/verify.json +0 -1
  211. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  212. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  213. package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
  214. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
  215. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  216. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  217. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  218. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  219. package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
  220. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
  221. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  222. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  223. package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
  224. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
  225. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  226. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  227. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
  230. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
  231. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
  232. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
  233. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
  234. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
  235. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  236. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  237. package/templates/.agents/skills/test/SKILL.en.md +0 -3
  238. package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
  239. package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
  240. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
  241. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
  242. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
  243. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
  244. package/templates/.agents/skills/watch-pr/SKILL.en.md +3 -3
  245. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +3 -3
  246. package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
  247. package/templates/.agents/templates/handoff.en.md +1 -1
  248. package/templates/.agents/templates/handoff.zh-CN.md +1 -1
  249. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  250. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  251. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  252. package/dist/lib/task/task-execution-lock.js +0 -250
  253. package/lib/task/task-execution-lock.ts +0 -320
@@ -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[] = [];
@@ -6,6 +6,7 @@ import {
6
6
  isRemovedHashShortIdInput,
7
7
  normalizeShortIdInput
8
8
  } from '../task/short-id.ts';
9
+ import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
9
10
  import {
10
11
  TASK_WORKSPACE_STATES,
11
12
  listTaskWorkspaces,
@@ -57,7 +58,7 @@ function taskBranch(taskMd: string, taskId: string): string {
57
58
  }
58
59
 
59
60
  function activeRegistry(repoRoot: string): Record<string, string> {
60
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
61
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
61
62
  if (!fs.existsSync(registryPath)) return {};
62
63
  const value = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as unknown;
63
64
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -74,7 +75,7 @@ function activeRegistry(repoRoot: string): Record<string, string> {
74
75
  }
75
76
 
76
77
  function taskIdentity(repoRoot: string, taskId: string, registry: Record<string, string>): SandboxWorkspaceIdentity {
77
- const taskMd = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
78
+ const taskMd = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md');
78
79
  if (!fs.existsSync(taskMd)) throw new Error(`Active task not found: ${taskId}`);
79
80
  const shortIds = Object.entries(registry).filter(([, id]) => id === taskId).map(([shortId]) => shortId);
80
81
  if (shortIds.length !== 1) {
@@ -126,14 +127,14 @@ export function resolveSandboxTarget(requestedRef: string, repoRoot: string): Sa
126
127
  const taskId = canonicalShortId ? registry[canonicalShortId] : undefined;
127
128
  if (!taskId) throw new Error(`Short id '${requestedRef}' not found in active task registry`);
128
129
  const workspace = taskIdentity(repoRoot, taskId, registry);
129
- 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 };
130
131
  }
131
132
  if (TASK_ID_RE.test(requestedRef)) {
132
133
  const workspace = taskIdentity(repoRoot, requestedRef, registry);
133
- 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 };
134
135
  }
135
136
 
136
- const activeRoot = path.join(repoRoot, '.agents', 'workspace', 'active');
137
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active');
137
138
  const matches = fs.existsSync(activeRoot)
138
139
  ? fs.readdirSync(activeRoot, { withFileTypes: true }).flatMap((entry) => {
139
140
  if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) return [];
@@ -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
  }
@@ -10,8 +10,6 @@ import {
10
10
  } from './artifact-lifecycle.ts';
11
11
  import { parseArtifactName } from './artifact-name.ts';
12
12
  import type { ArtifactIdentity } from './artifact-lifecycle.ts';
13
- import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
- import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
15
13
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
16
14
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
15
 
@@ -95,7 +93,7 @@ export type PrReviewInspectionResult = {
95
93
  error: { code: string; message: string } | null;
96
94
  };
97
95
 
98
- type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions; lockAlreadyHeld?: boolean };
96
+ type ActivityIntentOptions = TaskWriteOptions;
99
97
  type ArtifactRecord = { identity: ArtifactIdentity; head: string; status: PrReviewArtifactStatus };
100
98
  type RoundState = ArtifactRecord & {
101
99
  startedAgent: string | null;
@@ -299,7 +297,7 @@ function terminalNote(intent: PrReviewCompleteIntent | PrReviewTerminateIntent):
299
297
  return `Outcome: ${outcomeLabel(intent.outcome)}, reason: ${intent.reason} → ${intent.artifact}`;
300
298
  }
301
299
 
302
- function applyLocked(intent: PrReviewActivityIntent, normalizedAgent: string, options: ActivityIntentOptions): PrReviewActivityResult {
300
+ function applyActivityIntent(intent: PrReviewActivityIntent, normalizedAgent: string, options: ActivityIntentOptions): PrReviewActivityResult {
303
301
  const state = readSnapshot(intent.taskRef, options.repoRoot);
304
302
  if (!state.ok) return failed(intent, state.code, state.message, state.taskId);
305
303
  const parsed = parseArtifactName(intent.artifact)!;
@@ -401,19 +399,7 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
401
399
  if (resolved.state !== 'active') {
402
400
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
403
401
  }
404
- if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
405
- try {
406
- return withTaskExecutionLock(
407
- resolved.repoRoot,
408
- resolved.taskId,
409
- `task-activity:${intent.kind}`,
410
- () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }),
411
- options.lockOptions
412
- );
413
- } catch (error) {
414
- if (error instanceof TaskExecutionLockError) return failed(intent, error.code, error.message, resolved.taskId);
415
- return failed(intent, 'ACTIVITY_LOCK_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
416
- }
402
+ return applyActivityIntent(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
417
403
  }
418
404
 
419
405
  export type { ActivityIntentOptions };
@@ -134,6 +134,8 @@ type InspectOptions = {
134
134
  sourceSha256?: string;
135
135
  };
136
136
 
137
+ type ResolvedTaskRef = Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>;
138
+
137
139
  const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
138
140
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
139
141
  'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
@@ -160,10 +162,15 @@ function diagnostic(code: ArtifactDiagnosticCode, family: ArtifactFamily, name:
160
162
  return { code, family, name, message };
161
163
  }
162
164
 
163
- function inspectTaskArtifacts(taskRef: string, family: string, options: InspectOptions = {}): ArtifactInventoryResult {
165
+ function inspectTaskArtifacts(
166
+ taskRef: string,
167
+ family: string,
168
+ options: InspectOptions = {},
169
+ resolvedTask?: ResolvedTaskRef
170
+ ): ArtifactInventoryResult {
164
171
  const spec = familySpec(family);
165
172
  if (!spec) return failure(taskRef, family, { code: 'ARTIFACT_FAMILY_UNKNOWN', message: `unknown artifact family '${family}'` });
166
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
173
+ const resolved = resolvedTask ?? resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
167
174
  if (!resolved.ok) return failure(taskRef, family, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
168
175
  return inspectArtifactDirectory(resolved.taskDir, spec.family, {
169
176
  requestRef: taskRef, taskId: resolved.taskId, taskState: resolved.state
@@ -452,8 +459,13 @@ function attachArtifactSelection(
452
459
  }
453
460
  }
454
461
 
455
- function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
456
- const inventory = inspectTaskArtifacts(taskRef, family, options);
462
+ function resolveArtifactContextFromTask(
463
+ taskRef: string,
464
+ family: string,
465
+ options: InspectOptions,
466
+ resolvedTask?: ResolvedTaskRef
467
+ ): ArtifactContextResult {
468
+ const inventory = inspectTaskArtifacts(taskRef, family, options, resolvedTask);
457
469
  if (inventory.status === 'failed') return { ...inventory, inputs: [], codeMode: null };
458
470
  const writableError = assertWritableInventory(inventory);
459
471
  if (writableError) return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
@@ -462,7 +474,7 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
462
474
  return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
463
475
  }
464
476
  if (inventory.family === 'code') {
465
- const code = resolveCodeContext(inventory, options);
477
+ const code = resolveCodeContext(inventory, options, resolvedTask);
466
478
  const selected = attachArtifactSelection(code, pathState, options);
467
479
  if (selected.status !== 'ready' || !selected.selection) return selected;
468
480
  if (selected.selection.disposition === 'reuse') return selected;
@@ -496,7 +508,7 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
496
508
  const required = REQUIRED_INPUT[inventory.family as ArtifactFamily];
497
509
  const inputs: ArtifactIdentity[] = [];
498
510
  if (required) {
499
- const input = inspectTaskArtifacts(taskRef, required, options);
511
+ const input = inspectTaskArtifacts(taskRef, required, options, resolvedTask);
500
512
  if (input.status === 'failed' || !input.latest) {
501
513
  return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
502
514
  }
@@ -505,7 +517,7 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
505
517
  const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
506
518
  if (optional) {
507
519
  for (const family of optional) {
508
- const context = inspectTaskArtifacts(taskRef, family, options);
520
+ const context = inspectTaskArtifacts(taskRef, family, options, resolvedTask);
509
521
  if (context.status === 'ready' && context.latest) inputs.push(context.latest);
510
522
  }
511
523
  }
@@ -515,14 +527,30 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
515
527
  return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
516
528
  }
517
529
 
518
- function resolveCodeContext(inventory: ArtifactInventoryResult, options: InspectOptions): ArtifactContextResult {
530
+ function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
531
+ return resolveArtifactContextFromTask(taskRef, family, options);
532
+ }
533
+
534
+ export function resolveArtifactContextForResolvedTask(
535
+ resolvedTask: ResolvedTaskRef,
536
+ family: string,
537
+ options: InspectOptions = {}
538
+ ): ArtifactContextResult {
539
+ return resolveArtifactContextFromTask(resolvedTask.taskId, family, options, resolvedTask);
540
+ }
541
+
542
+ function resolveCodeContext(
543
+ inventory: ArtifactInventoryResult,
544
+ options: InspectOptions,
545
+ resolvedTask?: ResolvedTaskRef
546
+ ): ArtifactContextResult {
519
547
  const taskRef = inventory.requestRef;
520
548
  const pathState = lifecyclePathState(inventory.taskDir!);
521
549
  if (pathState.status !== 'valid') return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
522
550
  const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
523
- const source = inspectTaskArtifacts(taskRef, inputFamily, options);
551
+ const source = inspectTaskArtifacts(taskRef, inputFamily, options, resolvedTask);
524
552
  if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
525
- const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
553
+ const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options, resolvedTask);
526
554
  try {
527
555
  const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
528
556
  const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
@@ -538,7 +566,7 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
538
566
  const reviewMax = reviewCode.latest?.round ?? 0;
539
567
  const inputs = [source.latest];
540
568
  const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
541
- const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
569
+ const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options, resolvedTask);
542
570
  if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name) inputs.push(sourceReview.latest);
543
571
  if (inventory.openRounds.length > 0) {
544
572
  try {
@@ -9,7 +9,6 @@ import {
9
9
  getArtifactSchema,
10
10
  renderArtifactSkeleton
11
11
  } from './artifact-schema.ts';
12
- import { withTaskExecutionLock } from './task-execution-lock.ts';
13
12
  import type {
14
13
  ArtifactSchema,
15
14
  ArtifactSchemaFamily,
@@ -352,42 +351,36 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
352
351
  const target = path.join(request.taskDir, request.artifact);
353
352
  if (path.dirname(path.resolve(target)) !== path.resolve(request.taskDir)) return resultFailure('ARTIFACT_INIT_TARGET_INVALID', 'artifact must be inside the current task directory');
354
353
  try {
355
- return withTaskExecutionLock(request.repoRoot, request.taskId, 'task-artifact.init', () => {
356
- try {
357
- const existing = fs.lstatSync(target);
358
- if (existing.isSymbolicLink() || !existing.isFile()) return resultFailure('ARTIFACT_INIT_TARGET_INVALID', 'artifact target is not a regular file');
359
- return resultNoOp(readStableFileSync(target, { maxBytes: 1024 * 1024 }).bytes.toString('utf8'));
360
- } catch (error) {
361
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
362
- }
363
- const skeleton = renderArtifactSkeleton({
364
- taskId: request.taskId,
365
- family: request.family,
366
- artifact: request.artifact,
367
- ...(request.locale ? { locale: request.locale } : {})
368
- });
369
- const content = skeleton.trimEnd();
370
- const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
371
- try {
372
- fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
373
- try { fs.lstatSync(target); fs.unlinkSync(tempPath); return resultFailure('ARTIFACT_INIT_TARGET_EXISTS', 'artifact was created concurrently'); }
374
- catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
375
- fs.renameSync(tempPath, target);
376
- } catch (error) {
377
- try { fs.unlinkSync(tempPath); } catch { /* preserve primary error */ }
378
- return resultFailure('ARTIFACT_INIT_WRITE_FAILED', String(error));
379
- }
380
- return {
381
- status: 'applied',
382
- changed: true,
383
- artifactSha256: sha256Content(content),
384
- semanticDigest: canonicalSemanticDigest(content),
385
- error: null
386
- };
387
- });
354
+ const existing = fs.lstatSync(target);
355
+ if (existing.isSymbolicLink() || !existing.isFile()) return resultFailure('ARTIFACT_INIT_TARGET_INVALID', 'artifact target is not a regular file');
356
+ return resultNoOp(readStableFileSync(target, { maxBytes: 1024 * 1024 }).bytes.toString('utf8'));
357
+ } catch (error) {
358
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
359
+ }
360
+ const skeleton = renderArtifactSkeleton({
361
+ taskId: request.taskId,
362
+ family: request.family,
363
+ artifact: request.artifact,
364
+ ...(request.locale ? { locale: request.locale } : {})
365
+ });
366
+ const content = skeleton.trimEnd();
367
+ const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
368
+ try {
369
+ fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
370
+ try { fs.lstatSync(target); fs.unlinkSync(tempPath); return resultFailure('ARTIFACT_INIT_TARGET_EXISTS', 'artifact was created concurrently'); }
371
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
372
+ fs.renameSync(tempPath, target);
388
373
  } catch (error) {
389
- return resultFailure('ARTIFACT_INIT_LOCK_FAILED', String(error));
374
+ try { fs.unlinkSync(tempPath); } catch { /* preserve primary error */ }
375
+ return resultFailure('ARTIFACT_INIT_WRITE_FAILED', String(error));
390
376
  }
377
+ return {
378
+ status: 'applied',
379
+ changed: true,
380
+ artifactSha256: sha256Content(content),
381
+ semanticDigest: canonicalSemanticDigest(content),
382
+ error: null
383
+ };
391
384
  }
392
385
 
393
386
  export {
@@ -302,7 +302,7 @@ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
302
302
  return { action: null, reasonCode: 'LIFECYCLE_REVIEWED', evidence: ['latest code review is approved'] };
303
303
  }
304
304
 
305
- function buildLifecycleFacts(taskDir: string, content: string, taskState = 'active', executionBusy = false): LifecycleFactsResult {
305
+ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'active', executionBusy = false, repositoryRoot?: string): LifecycleFactsResult {
306
306
  try {
307
307
  const metadata = parseTypedTaskFrontmatter(content);
308
308
  const rework = parseReworkIntentDocument(content);
@@ -393,7 +393,7 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
393
393
  && artifactHashes[input] === receipt.inputSha256
394
394
  ) reviewedInputs[family] = input;
395
395
  if (family === 'review-code') {
396
- const identity = inspectReviewIdentity(taskDir, reviewContent);
396
+ const identity = inspectReviewIdentity(taskDir, reviewContent, repositoryRoot);
397
397
  reviewCodeIdentityValid = identity.status === 'matched'
398
398
  && hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
399
399
  && hasArtifactCompletionLog(content, latest, 'review-code.completed');
@@ -4,10 +4,11 @@ import { execFileSync } from 'node:child_process';
4
4
  import { formatTable } from '../../table.ts';
5
5
  import { parseTaskFrontmatter, extractTitle } from '../frontmatter.ts';
6
6
  import { loadShortIdByTaskId } from '../short-id.ts';
7
+ import { resolveProjectTaskWorkspaceRoot } from '../workspace-root.ts';
7
8
 
8
9
  const USAGE = `Usage: ai task ls [--all | --blocked | --completed]
9
10
 
10
- Lists tasks under .agents/workspace/. Defaults to active tasks only.
11
+ Lists tasks under the project task workspace. Defaults to active tasks only.
11
12
  --all Include active + blocked + completed (excludes archive)
12
13
  --blocked Only blocked tasks
13
14
  --completed Only completed tasks
@@ -73,7 +74,7 @@ type TaskRow = {
73
74
  };
74
75
 
75
76
  function collectTasks(repoRoot: string, state: 'active' | 'blocked' | 'completed'): TaskRow[] {
76
- const dir = path.join(repoRoot, '.agents', 'workspace', state);
77
+ const dir = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), state);
77
78
  if (!fs.existsSync(dir)) return [];
78
79
  const shortIdByTaskId = state === 'active' || state === 'completed'
79
80
  ? loadShortIdByTaskId(repoRoot)
@@ -117,7 +118,7 @@ function ls(args: string[] = []): void {
117
118
  rows.push(...collectTasks(repoRoot, state));
118
119
  }
119
120
  if (rows.length === 0) {
120
- process.stdout.write(`No tasks under .agents/workspace/${selection.join('|')}\n`);
121
+ process.stdout.write(`No tasks under project task workspace state ${selection.join('|')}\n`);
121
122
  return;
122
123
  }
123
124
  const tableRows = rows.map((r, i) => [
@@ -521,7 +521,7 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
521
521
  const run = input.run ?? makeRunner(input.repoRoot);
522
522
  const artifacts = enumerateArtifacts(input.taskDir);
523
523
  const workflow = collectWorkflow(content, input.now);
524
- const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
524
+ const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active', false, input.repoRoot);
525
525
  const next = facts.ok ? recommendNext(facts.facts) : null;
526
526
  const recommendation = next?.action ?? DASH;
527
527
 
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { execFileSync } from 'node:child_process';
3
4
 
4
5
  import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.ts';
@@ -7,7 +8,6 @@ import { checkpointCommitMatches } from './commit-identity.ts';
7
8
  import { resolveTaskContext, resolveTaskRef } from './resolve-ref.ts';
8
9
  import { commitPushDecision } from './commit-policy.ts';
9
10
  import { mergeOperationWarnings, type OperationWarning } from './operation-outcome.ts';
10
- import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
11
11
  import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
12
12
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
13
13
  import type { TaskMutation } from './write.ts';
@@ -135,6 +135,7 @@ function resolveBoundTask(input: CommitOperationInput):
135
135
  };
136
136
  return boundTask(input, resolved.taskId, resolved.taskDir, resolved.taskMdPath, resolved.repoRoot, resolved.state);
137
137
  }
138
+ if (!fs.existsSync(path.join(input.cwd, '.agents', '.airc.json'))) return { kind: 'none' };
138
139
  const context = resolveTaskContext(undefined, { repoRoot: input.cwd });
139
140
  if (context.ok) return boundTask(input, context.taskId, context.taskDir, context.taskMdPath, context.repoRoot, context.state);
140
141
  if (context.code === 'TASK_CONTEXT_NOT_FOUND') return { kind: 'none' };
@@ -280,7 +281,7 @@ function checkpointTimestamp(): string {
280
281
  return new Date().toISOString();
281
282
  }
282
283
 
283
- function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
284
+ function executeCommitOperationCore(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
284
285
  const inspected = inspectGitWorkflow(input.cwd);
285
286
  if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
286
287
  if (mode === 'orchestrated') return failure(
@@ -516,11 +517,9 @@ function executeCommitOperation(input: CommitOperationInput): CommitOperationRes
516
517
  const task = resolved.kind === 'bound' ? resolved.task : null;
517
518
  if (mode === 'orchestrated' && !task) return failure(normalizedInput, mode, null, 'ORCHESTRATION_TASK_REQUIRED', 'orchestrated commit requires a task context', 'blocked');
518
519
  try {
519
- return task
520
- ? withRepositoryMutationLock(normalizedInput.cwd, () => withTaskExecutionLock(normalizedInput.cwd, task.taskId, 'commit-operation', () => executeUnlocked(normalizedInput, task, mode)))
521
- : withRepositoryMutationLock(normalizedInput.cwd, () => executeUnlocked(normalizedInput, null, mode));
520
+ return executeCommitOperationCore(normalizedInput, task, mode);
522
521
  } catch (error) {
523
- const code = error instanceof TaskExecutionLockError ? error.code : 'COMMIT_OPERATION_FAILED';
522
+ const code = 'COMMIT_OPERATION_FAILED';
524
523
  return failure(normalizedInput, mode, task?.taskId ?? null, code, error instanceof Error ? error.message : String(error), 'blocked');
525
524
  }
526
525
  }