@fitlab-ai/agent-infra 0.11.15 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/bin/cli.ts +1 -1
  2. package/dist/bin/cli.js +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +6 -6
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +8 -18
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -1
  7. package/dist/lib/decide.js +2 -7
  8. package/dist/lib/internal/cli-route-inventory.js +3 -0
  9. package/dist/lib/internal/manual-validation.js +19 -33
  10. package/dist/lib/internal/platform-comment.js +59 -9
  11. package/dist/lib/internal/task-activity.js +1 -12
  12. package/dist/lib/internal/task-event.js +1 -12
  13. package/dist/lib/internal/task-ledger.js +1 -12
  14. package/dist/lib/internal/task-operation-registry.js +2 -1
  15. package/dist/lib/internal/task-qualification.js +6 -15
  16. package/dist/lib/internal/task-review.js +3 -16
  17. package/dist/lib/internal/task-short-id.js +2 -1
  18. package/dist/lib/internal/task-verify.js +1 -11
  19. package/dist/lib/internal/task-warning.js +1 -12
  20. package/dist/lib/internal/task-workspace.js +43 -0
  21. package/dist/lib/merge.js +4 -3
  22. package/dist/lib/platform/issue-comments.js +373 -267
  23. package/dist/lib/platform/issue-summary-content.js +112 -0
  24. package/dist/lib/platform/pr-change-report.js +175 -26
  25. package/dist/lib/platform/pr-summary-content.js +54 -0
  26. package/dist/lib/platform/pr-summary.js +321 -296
  27. package/dist/lib/platform/pull-requests.js +3 -15
  28. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  29. package/dist/lib/pr-review/evidence-grading.js +3 -2
  30. package/dist/lib/process-data/index.js +5 -4
  31. package/dist/lib/process-data/sources.js +6 -0
  32. package/dist/lib/run/index.js +3 -2
  33. package/dist/lib/sandbox/commands/create.js +5 -1
  34. package/dist/lib/sandbox/commands/enter.js +1 -1
  35. package/dist/lib/sandbox/commands/list-running.js +2 -1
  36. package/dist/lib/sandbox/control/client.js +8 -13
  37. package/dist/lib/sandbox/control/completed-reentry.js +2 -1
  38. package/dist/lib/sandbox/control/task-view.js +4 -2
  39. package/dist/lib/sandbox/mounts.js +4 -4
  40. package/dist/lib/sandbox/recovery.js +26 -16
  41. package/dist/lib/sandbox/removal.js +62 -12
  42. package/dist/lib/sandbox/task-resolver.js +9 -6
  43. package/dist/lib/sandbox/workspace-identity.js +6 -5
  44. package/dist/lib/sandbox/workspace-view.js +29 -22
  45. package/dist/lib/task/activity-intent.js +2 -12
  46. package/dist/lib/task/artifact-lifecycle.js +17 -11
  47. package/dist/lib/task/artifact-operations.js +41 -49
  48. package/dist/lib/task/capabilities.js +2 -2
  49. package/dist/lib/task/commands/ls.js +4 -3
  50. package/dist/lib/task/commands/status.js +1 -1
  51. package/dist/lib/task/commit-operation.js +6 -6
  52. package/dist/lib/task/control-authority.js +32 -32
  53. package/dist/lib/task/create-service.js +2 -4
  54. package/dist/lib/task/create.js +12 -65
  55. package/dist/lib/task/delegation-receipts.js +1 -1
  56. package/dist/lib/task/delivery.js +2 -8
  57. package/dist/lib/task/events.js +5 -31
  58. package/dist/lib/task/finalization.js +33 -29
  59. package/dist/lib/task/lifecycle.js +9 -48
  60. package/dist/lib/task/local-artifact-finalization.js +1 -2
  61. package/dist/lib/task/orchestration.js +24 -57
  62. package/dist/lib/task/platform-operation-recovery.js +87 -88
  63. package/dist/lib/task/resolve-ref.js +17 -12
  64. package/dist/lib/task/review-finalization.js +2 -35
  65. package/dist/lib/task/review-identity.js +3 -1
  66. package/dist/lib/task/short-id.js +10 -8
  67. package/dist/lib/task/verification-engine.js +19 -58
  68. package/dist/lib/task/workspace-root.js +59 -0
  69. package/dist/lib/task/workspace-snapshot.js +4 -3
  70. package/dist/lifecycle-build-manifest.json +3 -3
  71. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
  72. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
  73. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
  74. package/lib/decide.ts +2 -7
  75. package/lib/internal/cli-route-inventory.ts +2 -0
  76. package/lib/internal/manual-validation.ts +20 -37
  77. package/lib/internal/platform-comment.ts +49 -6
  78. package/lib/internal/task-activity.ts +1 -10
  79. package/lib/internal/task-event.ts +1 -10
  80. package/lib/internal/task-ledger.ts +1 -10
  81. package/lib/internal/task-operation-registry.ts +2 -1
  82. package/lib/internal/task-qualification.ts +5 -11
  83. package/lib/internal/task-review.ts +3 -14
  84. package/lib/internal/task-short-id.ts +2 -1
  85. package/lib/internal/task-verify.ts +1 -9
  86. package/lib/internal/task-warning.ts +1 -10
  87. package/lib/internal/task-workspace.ts +39 -0
  88. package/lib/merge.ts +4 -3
  89. package/lib/platform/issue-comments.ts +478 -280
  90. package/lib/platform/issue-summary-content.ts +132 -0
  91. package/lib/platform/pr-change-report.ts +165 -20
  92. package/lib/platform/pr-summary-content.ts +60 -0
  93. package/lib/platform/pr-summary.ts +367 -269
  94. package/lib/platform/pull-requests.ts +3 -24
  95. package/lib/platform/summary-content-pipeline.ts +23 -0
  96. package/lib/pr-review/evidence-grading.ts +3 -2
  97. package/lib/process-data/index.ts +4 -3
  98. package/lib/process-data/sources.ts +6 -0
  99. package/lib/run/index.ts +3 -2
  100. package/lib/sandbox/commands/create.ts +5 -1
  101. package/lib/sandbox/commands/enter.ts +1 -1
  102. package/lib/sandbox/commands/list-running.ts +2 -1
  103. package/lib/sandbox/control/client.ts +7 -12
  104. package/lib/sandbox/control/completed-reentry.ts +2 -1
  105. package/lib/sandbox/control/task-view.ts +4 -2
  106. package/lib/sandbox/mounts.ts +4 -4
  107. package/lib/sandbox/recovery.ts +26 -18
  108. package/lib/sandbox/removal.ts +60 -27
  109. package/lib/sandbox/task-resolver.ts +9 -6
  110. package/lib/sandbox/workspace-identity.ts +6 -5
  111. package/lib/sandbox/workspace-view.ts +28 -20
  112. package/lib/task/activity-intent.ts +3 -17
  113. package/lib/task/artifact-lifecycle.ts +39 -11
  114. package/lib/task/artifact-operations.ts +28 -35
  115. package/lib/task/capabilities.ts +2 -2
  116. package/lib/task/commands/ls.ts +4 -3
  117. package/lib/task/commands/status.ts +1 -1
  118. package/lib/task/commit-operation.ts +5 -6
  119. package/lib/task/control-authority.ts +29 -35
  120. package/lib/task/create-service.ts +2 -4
  121. package/lib/task/create.ts +10 -51
  122. package/lib/task/delegation-receipts.ts +1 -1
  123. package/lib/task/delivery.ts +2 -10
  124. package/lib/task/events.ts +5 -32
  125. package/lib/task/finalization.ts +27 -30
  126. package/lib/task/lifecycle.ts +9 -50
  127. package/lib/task/local-artifact-finalization.ts +1 -4
  128. package/lib/task/orchestration.ts +31 -80
  129. package/lib/task/platform-operation-recovery.ts +70 -82
  130. package/lib/task/resolve-ref.ts +18 -12
  131. package/lib/task/review-finalization.ts +2 -52
  132. package/lib/task/review-identity.ts +3 -1
  133. package/lib/task/short-id.ts +10 -8
  134. package/lib/task/verification-engine.ts +20 -62
  135. package/lib/task/workspace-root.ts +78 -0
  136. package/lib/task/workspace-snapshot.ts +4 -3
  137. package/package.json +1 -1
  138. package/templates/.agents/QUICKSTART.en.md +19 -8
  139. package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
  140. package/templates/.agents/README.en.md +10 -15
  141. package/templates/.agents/README.zh-CN.md +9 -13
  142. package/templates/.agents/rules/issue-sync.en.md +1 -1
  143. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  144. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  145. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  146. package/templates/.agents/rules/next-step-output.en.md +2 -2
  147. package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
  148. package/templates/.agents/rules/pr-sync.en.md +5 -3
  149. package/templates/.agents/rules/pr-sync.zh-CN.md +7 -10
  150. package/templates/.agents/rules/release-commands.en.md +3 -0
  151. package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
  152. package/templates/.agents/rules/task-management.en.md +0 -2
  153. package/templates/.agents/rules/task-management.zh-CN.md +0 -2
  154. package/templates/.agents/rules/task-short-id.en.md +2 -2
  155. package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
  156. package/templates/.agents/scripts/task-short-id.js +2 -8
  157. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  158. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  159. package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
  160. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
  161. package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
  162. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
  163. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
  164. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
  166. package/templates/.agents/skills/block-task/config/verify.json +0 -1
  167. package/templates/.agents/skills/cancel-task/SKILL.en.md +7 -7
  168. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +7 -7
  169. package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
  170. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  171. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  172. package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
  173. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
  174. package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
  175. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  176. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  177. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  178. package/templates/.agents/skills/commit/config/verify.json +0 -1
  179. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
  180. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
  181. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +2 -2
  182. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +2 -2
  183. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  184. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  185. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
  186. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
  187. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +3 -3
  188. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +3 -3
  189. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -30
  190. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -32
  191. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  192. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  195. package/templates/.agents/skills/create-pr/config/verify.json +0 -1
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +10 -3
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +10 -3
  198. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +3 -3
  199. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +3 -3
  200. package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
  201. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
  202. package/templates/.agents/skills/create-task/SKILL.en.md +4 -22
  203. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -22
  204. package/templates/.agents/skills/create-task/config/verify.json +0 -1
  205. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  206. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  207. package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
  208. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
  209. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  210. package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
  211. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  212. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  213. package/templates/.agents/skills/import-issue/config/verify.json +0 -1
  214. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  215. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  216. package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
  217. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
  218. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  219. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
  220. package/templates/.agents/skills/restore-task/config/verify.json +0 -1
  221. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  222. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  223. package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
  224. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
  225. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  226. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  227. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
  230. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
  231. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  232. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  233. package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
  234. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
  235. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  236. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  237. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
  238. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
  239. package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
  240. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
  241. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
  242. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
  243. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
  244. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
  245. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  246. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  247. package/templates/.agents/skills/test/SKILL.en.md +0 -3
  248. package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
  249. package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
  250. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
  251. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
  252. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
  253. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
  254. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  255. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  256. package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
  257. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  258. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
  259. package/templates/.agents/templates/handoff.en.md +1 -1
  260. package/templates/.agents/templates/handoff.zh-CN.md +1 -1
  261. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  262. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  263. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  264. package/dist/lib/task/task-execution-lock.js +0 -250
  265. package/lib/task/task-execution-lock.ts +0 -320
@@ -3,12 +3,15 @@ import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { normalizeShortIdInput, resolveShortIdReadOnly } from './short-id.ts';
5
5
  import { parseTaskFrontmatter } from './frontmatter.ts';
6
+ import { resolveProjectTaskWorkspaceRoot } from './workspace-root.ts';
6
7
 
7
8
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
8
9
  // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
9
10
  // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
10
11
  const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'] as const;
11
12
 
13
+ function taskRoot(repoRoot: string): string { return resolveProjectTaskWorkspaceRoot(repoRoot); }
14
+
12
15
  type ResolveRefResult =
13
16
  | {
14
17
  ok: true;
@@ -80,12 +83,12 @@ function listSortedNumeric(dir: string, width: number): string[] {
80
83
  .reverse();
81
84
  }
82
85
 
83
- function findInArchive(repoRoot: string, taskId: string): string | null {
84
- // archive-tasks stores local material at .agents/workspace/archive/YYYY/MM/DD/{taskId}/local/task.md
86
+ function findInArchive(workspaceRoot: string, taskId: string): string | null {
87
+ // archive-tasks stores local material at the project task workspace archive/YYYY/MM/DD/{taskId}/local/task.md
85
88
  // where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
86
89
  // the task id's creation date. So we cannot derive the path from taskId alone;
87
90
  // walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
88
- const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
91
+ const archiveDir = path.join(workspaceRoot, 'archive');
89
92
  for (const year of listSortedNumeric(archiveDir, 4)) {
90
93
  const yearDir = path.join(archiveDir, year);
91
94
  for (const month of listSortedNumeric(yearDir, 2)) {
@@ -100,14 +103,14 @@ function findInArchive(repoRoot: string, taskId: string): string | null {
100
103
  }
101
104
 
102
105
  function findTaskMd(
103
- repoRoot: string,
106
+ workspaceRoot: string,
104
107
  taskId: string
105
108
  ): { taskMdPath: string; state: TaskWorkspaceState } | null {
106
109
  for (const sub of FLAT_WORKSPACE_DIRS) {
107
- const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
110
+ const candidate = path.join(workspaceRoot, sub, taskId, 'task.md');
108
111
  if (fs.existsSync(candidate)) return { taskMdPath: candidate, state: sub };
109
112
  }
110
- const archived = findInArchive(repoRoot, taskId);
113
+ const archived = findInArchive(workspaceRoot, taskId);
111
114
  return archived ? { taskMdPath: archived, state: 'archive' } : null;
112
115
  }
113
116
 
@@ -119,8 +122,9 @@ function findTaskMd(
119
122
  */
120
123
  function enumerateTaskDirs(repoRoot: string): { taskId: string; taskDir: string }[] {
121
124
  const out: { taskId: string; taskDir: string }[] = [];
125
+ const workspaceRoot = taskRoot(repoRoot);
122
126
  for (const sub of FLAT_WORKSPACE_DIRS) {
123
- const base = path.join(repoRoot, '.agents', 'workspace', sub);
127
+ const base = path.join(workspaceRoot, sub);
124
128
  if (!fs.existsSync(base)) continue;
125
129
  for (const entry of fs.readdirSync(base).sort()) {
126
130
  if (!TASK_ID_RE.test(entry)) continue;
@@ -139,6 +143,7 @@ function enumerateTaskDirs(repoRoot: string): { taskId: string; taskDir: string
139
143
  */
140
144
  function enumerateAllTaskDirs(repoRoot: string): { taskId: string; taskDir: string; state: TaskWorkspaceState }[] {
141
145
  const out: { taskId: string; taskDir: string; state: TaskWorkspaceState }[] = [];
146
+ const workspaceRoot = taskRoot(repoRoot);
142
147
  const seen = new Map<string, string>();
143
148
  const add = (taskId: string, taskDir: string, state: TaskWorkspaceState) => {
144
149
  if (!TASK_ID_RE.test(taskId)) return;
@@ -155,12 +160,12 @@ function enumerateAllTaskDirs(repoRoot: string): { taskId: string; taskDir: stri
155
160
  };
156
161
 
157
162
  for (const state of FLAT_WORKSPACE_DIRS) {
158
- const base = path.join(repoRoot, '.agents', 'workspace', state);
163
+ const base = path.join(workspaceRoot, state);
159
164
  if (!fs.existsSync(base)) continue;
160
165
  for (const taskId of fs.readdirSync(base).sort()) add(taskId, path.join(base, taskId), state);
161
166
  }
162
167
 
163
- const archive = path.join(repoRoot, '.agents', 'workspace', 'archive');
168
+ const archive = path.join(workspaceRoot, 'archive');
164
169
  for (const year of listSortedNumeric(archive, 4).reverse()) {
165
170
  const yearDir = path.join(archive, year);
166
171
  for (const month of listSortedNumeric(yearDir, 2).reverse()) {
@@ -178,8 +183,9 @@ function locateHotTaskDirs(
178
183
  repoRoot: string,
179
184
  taskId: string
180
185
  ): { taskId: string; taskDir: string; taskMdPath: string; state: Exclude<TaskWorkspaceState, 'archive'> }[] {
186
+ const workspaceRoot = taskRoot(repoRoot);
181
187
  return FLAT_WORKSPACE_DIRS.flatMap((state) => {
182
- const taskDir = path.join(repoRoot, '.agents', 'workspace', state, taskId);
188
+ const taskDir = path.join(workspaceRoot, state, taskId);
183
189
  const taskMdPath = path.join(taskDir, 'task.md');
184
190
  return fs.existsSync(taskMdPath) ? [{ taskId, taskDir, taskMdPath, state }] : [];
185
191
  });
@@ -244,7 +250,7 @@ function resolveTaskRef(arg: string, options: ResolveTaskRefOptions = {}): Resol
244
250
  }
245
251
  taskId = shortResult.taskId;
246
252
  }
247
- const located = findTaskMd(repoRoot, taskId);
253
+ const located = findTaskMd(taskRoot(repoRoot), taskId);
248
254
  if (!located) {
249
255
  return {
250
256
  ok: false,
@@ -300,7 +306,7 @@ function resolveTaskContext(
300
306
  };
301
307
  }
302
308
 
303
- const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
309
+ const activeDir = path.join(taskRoot(repoRoot), 'active');
304
310
  const matches: { taskId: string; taskDir: string; taskMdPath: string }[] = [];
305
311
  if (fs.existsSync(activeDir)) {
306
312
  for (const entry of fs.readdirSync(activeDir).sort()) {
@@ -10,7 +10,6 @@ import { finalizeReviewSummaryContent } from './review-artifacts.ts';
10
10
  import { inspectDecisionDetailDuplicates } from './decision-details.ts';
11
11
  import { resolveTaskRef } from './resolve-ref.ts';
12
12
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
13
- import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
13
  import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
15
14
  import { getArtifactSchema } from './artifact-schema.ts';
16
15
  import { validateQualificationAudit } from './qualification-audit.ts';
@@ -69,7 +68,6 @@ type ReviewFileSystem = {
69
68
  type ReviewFinalizationOptions = {
70
69
  repoRoot?: string;
71
70
  fileSystem?: Partial<ReviewFileSystem>;
72
- lockAlreadyHeld?: boolean;
73
71
  };
74
72
 
75
73
  const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
@@ -123,7 +121,7 @@ function preflightFailed(
123
121
  }
124
122
 
125
123
  /** Validate the current review draft before any finding-ledger write. */
126
- function preflightReviewSummaryUnlocked(
124
+ function preflightReviewSummary(
127
125
  request: ReviewFinalizationRequest,
128
126
  options: ReviewFinalizationOptions = {}
129
127
  ): ReviewPreflightResult {
@@ -178,30 +176,9 @@ function preflightReviewSummaryUnlocked(
178
176
  };
179
177
  }
180
178
 
181
- function preflightReviewSummary(
182
- request: ReviewFinalizationRequest,
183
- options: ReviewFinalizationOptions = {}
184
- ): ReviewPreflightResult {
185
- if (request.dryRun || options.lockAlreadyHeld) return preflightReviewSummaryUnlocked(request, options);
186
- const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
187
- if (!resolved.ok) return preflightReviewSummaryUnlocked(request, options);
188
- try {
189
- return withTaskExecutionLock(
190
- resolved.repoRoot,
191
- resolved.taskId,
192
- 'task-review.preflight',
193
- () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true })
194
- );
195
- } catch (error) {
196
- if (!(error instanceof TaskExecutionLockError)) throw error;
197
- return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
198
- }
199
- }
200
-
201
179
  type ReviewSummaryCandidatePreparation = Readonly<{
202
180
  result: ReviewFinalizationResult;
203
181
  content: string;
204
- lockAlreadyHeld?: boolean;
205
182
  }>;
206
183
 
207
184
  /** Prepare domain validation, summary bytes and provenance before any publication. */
@@ -279,7 +256,6 @@ function prepareReviewSummaryCandidate(
279
256
  operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
280
257
  error: null
281
258
  },
282
- lockAlreadyHeld: options.lockAlreadyHeld
283
259
  };
284
260
  }
285
261
 
@@ -303,7 +279,7 @@ function replaceCurrentArtifactAtomically(file: string, content: string): void {
303
279
  }
304
280
  }
305
281
 
306
- function finalizeReviewSummaryUnlocked(
282
+ function finalizeReviewSummary(
307
283
  request: ReviewFinalizationRequest,
308
284
  options: ReviewFinalizationOptions = {}
309
285
  ): ReviewFinalizationResult {
@@ -340,32 +316,6 @@ function finalizeReviewSummaryUnlocked(
340
316
  }
341
317
  }
342
318
 
343
- function finalizeReviewSummary(
344
- request: ReviewFinalizationRequest,
345
- options: ReviewFinalizationOptions = {}
346
- ): ReviewFinalizationResult {
347
- const effectiveOptions = options;
348
- if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
349
- const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
350
- if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
351
- try {
352
- return withTaskExecutionLock(
353
- resolved.repoRoot,
354
- resolved.taskId,
355
- 'task-review.finalize-summary',
356
- () => finalizeReviewSummaryUnlocked(request, { ...effectiveOptions, lockAlreadyHeld: true })
357
- );
358
- } catch (error) {
359
- if (!(error instanceof TaskExecutionLockError)) throw error;
360
- return failed(
361
- request,
362
- 'REVIEW_PROVENANCE_INVALID',
363
- `${error.code}: ${error.message}`,
364
- resolved.taskId
365
- );
366
- }
367
- }
368
-
369
319
  export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
370
320
  export type {
371
321
  ReviewFinalizationError,
@@ -45,7 +45,9 @@ export function inspectReviewIdentity(taskDir: string, reportContent: string, re
45
45
  }
46
46
  try {
47
47
  const metadata = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
48
- const taskRepositoryRoot = git(taskDir, ['rev-parse', '--show-toplevel']);
48
+ const taskRepositoryRoot = repositoryRoot
49
+ ? fs.realpathSync.native(repositoryRoot)
50
+ : git(taskDir, ['rev-parse', '--show-toplevel']);
49
51
  const branch = String(metadata.branch ?? '').trim();
50
52
  const gitRoot = branch ? resolveBranchWorktree(taskRepositoryRoot, branch) : taskRepositoryRoot;
51
53
  if (!gitRoot) return result('indeterminate', `Task branch '${branch}' is not checked out in a registered worktree`, reviewedHead);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
4
+ import { resolveProjectTaskWorkspaceRoot } from './workspace-root.ts';
4
5
 
5
6
  const REGISTRY_NAME = '.short-ids.json';
6
7
  const LOCK_NAME = '.short-ids.json.lock';
@@ -85,7 +86,7 @@ function resolveShortIdReadOnly(
85
86
  }
86
87
 
87
88
  const key = normalized.value;
88
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
89
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', REGISTRY_NAME);
89
90
  if (!fs.existsSync(registryPath)) {
90
91
  return shortIdFailure(
91
92
  'SHORT_ID_REGISTRY_NOT_FOUND',
@@ -164,8 +165,9 @@ function resolveShortIdReadOnly(
164
165
  : `short id '${key}' not found in task short-id registry.`;
165
166
  return shortIdFailure('SHORT_ID_NOT_FOUND', message);
166
167
  }
167
- const activeTask = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
168
- const completedTask = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId, 'task.md');
168
+ const workspaceRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
169
+ const activeTask = path.join(workspaceRoot, 'active', taskId, 'task.md');
170
+ const completedTask = path.join(workspaceRoot, 'completed', taskId, 'task.md');
169
171
  let isValidCompletedTask = false;
170
172
  try {
171
173
  const task = parseTypedTaskFrontmatter(fs.readFileSync(completedTask, 'utf8'));
@@ -300,7 +302,7 @@ function mutateShortIdRegistry(
300
302
  taskId: string,
301
303
  requested: ShortIdMutationEffect
302
304
  ): ShortIdMutationResult {
303
- const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
305
+ const activeDir = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active');
304
306
  return mutateShortIdRegistryAt(activeDir, configuredShortIdLength(repoRoot), taskId, requested);
305
307
  }
306
308
 
@@ -369,7 +371,7 @@ function mutateShortIdRegistryAt(
369
371
  }
370
372
 
371
373
  function releaseStaleShortIdRegistry(repoRoot: string, taskId: string): ShortIdMutationResult {
372
- const activeDir = path.join(repoRoot, '.agents', 'workspace', 'active');
374
+ const activeDir = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active');
373
375
  return mutateShortIdRegistryAt(
374
376
  activeDir,
375
377
  configuredShortIdLength(repoRoot),
@@ -433,7 +435,7 @@ function executeShortIdCommand(request: ShortIdCommandRequest): ShortIdCommandRe
433
435
  }
434
436
 
435
437
  function readRegistry(repoRoot: string): RegistrySchema | null {
436
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
438
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', REGISTRY_NAME);
437
439
  if (!fs.existsSync(registryPath)) return null;
438
440
  try {
439
441
  const raw = fs.readFileSync(registryPath, 'utf8');
@@ -446,7 +448,7 @@ function readRegistry(repoRoot: string): RegistrySchema | null {
446
448
  }
447
449
 
448
450
  function readBranchFromTaskMd(repoRoot: string, taskId: string): string | null {
449
- const taskMdPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
451
+ const taskMdPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md');
450
452
  if (!fs.existsSync(taskMdPath)) return null;
451
453
  const content = fs.readFileSync(taskMdPath, 'utf8');
452
454
  const m = content.match(/^branch:\s*(.+)$/m);
@@ -468,7 +470,7 @@ function inspectShortIdRegistry(
468
470
  repoRoot: string,
469
471
  shortIdLength = configuredShortIdLength(repoRoot)
470
472
  ): ShortIdRegistryInspection {
471
- const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
473
+ const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', REGISTRY_NAME);
472
474
  if (!fs.existsSync(registryPath)) {
473
475
  return {
474
476
  status: 'missing',
@@ -69,9 +69,7 @@ const DEFAULT_REQUIRED_FIELDS = [
69
69
  ];
70
70
 
71
71
  const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?$/;
72
- // Start markers (action suffixed with ` [started]`) are excluded from the
73
- // "latest action" / freshness computation so a step's in-flight marker never
74
- // satisfies a skill's expected_action_pattern; the matching done entry does.
72
+ // Started rows do not represent completed lifecycle actions.
75
73
  const ACTIVITY_LOG_STARTED_RE = /\s*\[started\]\s*$/;
76
74
  const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
77
75
 
@@ -358,16 +356,10 @@ function checkTaskMeta({ taskDir, config, repositoryRoot, skillName, artifactFil
358
356
  if (expectedStep && metadata.current_step !== expectedStep) {
359
357
  let validLifecycleArtifact = false;
360
358
  if (currentTask) {
361
- const activityConfig = loadVerificationConfig(repositoryRoot, skillName).checks?.["activity-log"];
362
359
  const { section, invalidEntries } = inspectActivityLog(task.content);
363
- if (activityConfig && section && invalidEntries.length === 0) {
364
- let latestAction = "";
365
- for (const entry of section.entries) {
366
- if (!ACTIVITY_LOG_STARTED_RE.test(entry.step)) latestAction = entry.step;
367
- }
360
+ if (section && invalidEntries.length === 0) {
368
361
  validLifecycleArtifact = checkArtifactLifecycleActivityLog(
369
- taskDir, artifactFile, skillName,
370
- activityConfig, task, section.entries, latestAction
362
+ taskDir, artifactFile, skillName, task, section.entries
371
363
  )?.status === "pass";
372
364
  }
373
365
  }
@@ -725,16 +717,16 @@ function checkImplementationInput({ taskDir, artifactFile }: any): any {
725
717
  return passResult("implementation-input", `${actionDecision} matches Activity Log, report, and task table`);
726
718
  }
727
719
 
728
- const ARTIFACT_ACTIVITY_STAGES: Readonly<Record<string, { family: string; event: string }>> = {
729
- "analyze-task": { family: "analysis", event: "analyze.completed" },
730
- "plan-task": { family: "plan", event: "plan.completed" },
731
- "review-analysis": { family: "review-analysis", event: "review-analysis.completed" },
732
- "review-plan": { family: "review-plan", event: "review-plan.completed" },
733
- "code-task": { family: "code", event: "code.completed" },
734
- "review-code": { family: "review-code", event: "review-code.completed" }
720
+ const ARTIFACT_ACTIVITY_STAGES: Readonly<Record<string, { family: string; event: string; action: string }>> = {
721
+ "analyze-task": { family: "analysis", event: "analyze.completed", action: "Analyze Task" },
722
+ "plan-task": { family: "plan", event: "plan.completed", action: "Plan Task" },
723
+ "review-analysis": { family: "review-analysis", event: "review-analysis.completed", action: "Review Analysis" },
724
+ "review-plan": { family: "review-plan", event: "review-plan.completed", action: "Review Plan" },
725
+ "code-task": { family: "code", event: "code.completed", action: "Code Task" },
726
+ "review-code": { family: "review-code", event: "review-code.completed", action: "Review Code" }
735
727
  };
736
728
 
737
- function checkArtifactLifecycleActivityLog(taskDir: string, artifactFile: string | undefined, skillName: string, config: any, task: any, entries: any[], latestAction: string): any {
729
+ function checkArtifactLifecycleActivityLog(taskDir: string, artifactFile: string | undefined, skillName: string, task: any, entries: any[]): any {
738
730
  const stage = ARTIFACT_ACTIVITY_STAGES[skillName];
739
731
  if (!stage) return null;
740
732
  const inventory = inspectArtifactDirectory(taskDir, stage.family as any);
@@ -772,25 +764,19 @@ function checkArtifactLifecycleActivityLog(taskDir: string, artifactFile: string
772
764
  }
773
765
  }
774
766
 
775
- const expectedAction = new RegExp(config.expected_action_pattern);
767
+ const roundActionPattern = new RegExp(`^${stage.action} \\(Round ${identity.round}(?:, [^)]+)?\\)$`);
776
768
  const roundAction = entries.find((entry) => {
777
- if (ACTIVITY_LOG_STARTED_RE.test(entry.step)) return false;
778
- if (!expectedAction.test(entry.step)) return false;
779
- expectedAction.lastIndex = 0;
780
- const actionRound = /\(Round (\d+)/.exec(entry.step)?.[1];
781
- return actionRound === String(identity.round) && entry.note.includes(`→ ${output}`);
769
+ return !ACTIVITY_LOG_STARTED_RE.test(entry.step)
770
+ && roundActionPattern.test(entry.step)
771
+ && entry.note.includes(`→ ${output}`);
782
772
  });
783
773
  if (!roundAction) {
784
774
  return failResult("activity-log", `No completed Activity Log row records '${output}' for ${skillName} Round ${identity.round}`);
785
775
  }
786
- if (skillName === "plan-task" && /^(?:Human Decision|Decision)\b/.test(latestAction)
787
- && expectedAction.test(latestAction)) {
788
- return passResult("activity-log", `Plan completion '${output}' is current; latest action '${latestAction}' is a human decision`);
789
- }
790
776
  return passResult("activity-log", `Current ${stage.family} completion '${output}' is valid for Round ${identity.round}`);
791
777
  }
792
778
 
793
- function checkActivityLog({ taskDir, config, skillName, artifactFile }: any): any {
779
+ function checkActivityLog({ taskDir, skillName, artifactFile }: any): any {
794
780
  const task = loadTask(taskDir);
795
781
  if (!task.ok) {
796
782
  return failResult("activity-log", task.message);
@@ -802,44 +788,16 @@ function checkActivityLog({ taskDir, config, skillName, artifactFile }: any): an
802
788
  const entries = section.entries;
803
789
  if (entries.length === 0) return failResult("activity-log", "Activity Log has no entries");
804
790
 
805
- let latestAction = "";
806
- let latestTimestamp = "";
807
- const doneActions: string[] = [];
808
-
809
791
  for (const entry of entries) {
810
792
  if (!entry.note.trim()) return failResult("activity-log", "Activity Log entry note is required");
811
- const { time: timestamp, step: action } = entry;
812
- // A `[started]` marker is not a terminal action: keep latestAction/latestTimestamp
813
- // on the most recent done entry so expected_action_pattern sees it.
814
- if (!ACTIVITY_LOG_STARTED_RE.test(action)) {
815
- latestTimestamp = timestamp;
816
- latestAction = action;
817
- doneActions.push(action);
818
- }
819
793
  }
820
794
 
821
795
  const artifactLifecycleResult = checkArtifactLifecycleActivityLog(
822
- taskDir, artifactFile, skillName, config, task, entries, latestAction
796
+ taskDir, artifactFile, skillName, task, entries
823
797
  );
824
798
  if (artifactLifecycleResult) return artifactLifecycleResult;
825
799
 
826
- if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
827
- const expected = new RegExp(config.expected_action_pattern);
828
- let index = doneActions.length - 1;
829
- while (index >= 0 && doneActions[index] === 'Commit' && !expected.test(doneActions[index]!)) index -= 1;
830
- if (index >= 0 && expected.test(doneActions[index]!)) {
831
- latestAction = doneActions[index]!;
832
- }
833
- }
834
-
835
- if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
836
- return failResult(
837
- "activity-log",
838
- `Latest action '${latestAction}' does not match '${config.expected_action_pattern}'`
839
- );
840
- }
841
-
842
- return passResult("activity-log", `Latest entry '${latestAction}' at ${latestTimestamp}`);
800
+ return passResult("activity-log", "Activity Log is valid");
843
801
  }
844
802
 
845
803
  function checkCompletionChecklist({ taskDir, config }: any): any {
@@ -1042,9 +1000,9 @@ async function checkPostReviewCommit({ taskDir, config, repositoryRoot }: any):
1042
1000
 
1043
1001
  let gitRoot;
1044
1002
  try {
1045
- gitRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1003
+ gitRoot = execFileSync("git", ["-C", repositoryRoot, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1046
1004
  } catch {
1047
- return blockedResult("post-review-commit", "git unavailable or task directory is not inside a git repository");
1005
+ return blockedResult("post-review-commit", "git unavailable or repository root is not inside a git repository");
1048
1006
  }
1049
1007
 
1050
1008
  const task = loadTask(taskDir);
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import { isSandbox } from '../sandbox/environment.ts';
5
+
6
+ export type ExecutionBoundary = 'host' | 'container';
7
+
8
+ /**
9
+ * Resolve the physical task-workspace boundary from the verified process
10
+ * execution context. Sandbox mode (task-bound / branch-only) does not affect
11
+ * the task workspace mount location.
12
+ */
13
+ export function resolveTaskWorkspaceExecutionBoundary(
14
+ env: NodeJS.ProcessEnv = process.env
15
+ ): ExecutionBoundary {
16
+ if (env.AGENT_INFRA_SANDBOX !== '1') return 'host';
17
+ const statusMountPath = env.AGENT_INFRA_CONTROL_STATUS_DIR;
18
+ if (!statusMountPath || !isSandbox(env, { statusMountPath })) {
19
+ throw new Error('task workspace boundary requires a verified sandbox execution context');
20
+ }
21
+ return 'container';
22
+ }
23
+
24
+ export interface TaskWorkspaceLayout {
25
+ projectKey: string;
26
+ boundary: ExecutionBoundary;
27
+ root: string;
28
+ active: string;
29
+ blocked: string;
30
+ completed: string;
31
+ archive: string;
32
+ shortIds: string;
33
+ }
34
+
35
+ export function resolveTaskWorkspaceLayout(input: {
36
+ project: string;
37
+ boundary: ExecutionBoundary;
38
+ home?: string;
39
+ }): TaskWorkspaceLayout {
40
+ const { project, boundary } = input;
41
+ if (!project || project === '.' || project === '..' || /[\\/\u0000-\u001f\u007f]/u.test(project)) {
42
+ throw new TypeError('project must be a safe single path segment');
43
+ }
44
+ let root: string;
45
+ if (boundary === 'container') {
46
+ root = '/task-workspace';
47
+ } else if (boundary === 'host') {
48
+ if (!input.home) throw new TypeError('home is required for host task workspace resolution');
49
+ if (!path.isAbsolute(input.home)) throw new TypeError('home must be an absolute path for host task workspace resolution');
50
+ root = path.join(input.home, '.agent-infra', 'task-workspaces', project);
51
+ } else {
52
+ throw new TypeError(`unsupported task workspace boundary: ${String(boundary)}`);
53
+ }
54
+ return {
55
+ projectKey: project,
56
+ boundary,
57
+ root,
58
+ active: path.join(root, 'active'),
59
+ blocked: path.join(root, 'blocked'),
60
+ completed: path.join(root, 'completed'),
61
+ archive: path.join(root, 'archive'),
62
+ shortIds: path.join(root, 'active', '.short-ids.json')
63
+ };
64
+ }
65
+
66
+ export function resolveProjectTaskWorkspaceLayout(
67
+ repoRoot: string,
68
+ boundary: ExecutionBoundary = resolveTaskWorkspaceExecutionBoundary()
69
+ ): TaskWorkspaceLayout {
70
+ const configPath = path.join(repoRoot, '.agents', '.airc.json');
71
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as { project?: unknown };
72
+ if (typeof config.project !== 'string') throw new TypeError(`project is missing from ${configPath}`);
73
+ return resolveTaskWorkspaceLayout({ project: config.project, boundary, home: process.env.HOME || os.homedir() });
74
+ }
75
+
76
+ export function resolveProjectTaskWorkspaceRoot(repoRoot: string): string {
77
+ return resolveProjectTaskWorkspaceLayout(repoRoot).root;
78
+ }
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { resolveProjectTaskWorkspaceRoot } from './workspace-root.ts';
6
7
 
7
8
  function git(repoRoot: string, indexFile: string | null, args: string[]): string {
8
9
  const env = indexFile ? { ...process.env, GIT_INDEX_FILE: indexFile } : process.env;
@@ -55,7 +56,7 @@ function captureWorktreeTree(repoRoot: string, forcedPath: string | null): strin
55
56
  }
56
57
 
57
58
  function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFileFingerprint[] {
58
- const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
59
+ const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(stateRoot), 'active');
59
60
  const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
60
61
  const records: TaskFileFingerprint[] = [];
61
62
  const visit = (absolute: string, relative: string): void => {
@@ -67,7 +68,7 @@ function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFil
67
68
  return;
68
69
  }
69
70
  if (!stat.isFile() && !stat.isSymbolicLink()) return;
70
- if (/^\.agents\/workspace\/(?:active|blocked|completed)\/TASK-[0-9]{8}-[0-9]{6}\/\.runtime\/orchestration\.json$/u.test(relative)) return;
71
+ if (/^task-workspace\/(?:active|blocked|completed)\/TASK-[0-9]{8}-[0-9]{6}\/\.runtime\/orchestration\.json$/u.test(relative)) return;
71
72
  const content = stat.isSymbolicLink()
72
73
  ? Buffer.from(fs.readlinkSync(absolute))
73
74
  : fs.readFileSync(absolute);
@@ -80,7 +81,7 @@ function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFil
80
81
  };
81
82
  for (const root of roots) {
82
83
  if (!fs.existsSync(root)) continue;
83
- const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
84
+ const relative = path.posix.join('task-workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
84
85
  visit(root, relative.replace(/\/$/, ''));
85
86
  }
86
87
  return records.sort((left, right) => left.path.localeCompare(right.path));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.15",
3
+ "version": "0.12.1",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,16 +53,23 @@ Built-in templates take priority over external templates. Later external templat
53
53
 
54
54
  ## Creating Your First Task
55
55
 
56
+ Resolve the host task root once for these examples:
57
+
58
+ ```bash
59
+ TASK_WORKSPACE_ROOT=$(agent-infra-internal task-workspace resolve --boundary host | node -e 'let input="";process.stdin.setEncoding("utf8");process.stdin.on("data",chunk=>input+=chunk);process.stdin.on("end",()=>process.stdout.write(JSON.parse(input).root));')
60
+ ```
61
+
56
62
  1. Copy the task template to the active workspace:
57
63
 
58
64
  ```bash
59
- cp .agents/templates/task.md .agents/workspace/active/task-001.md
65
+ mkdir -p "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120000"
66
+ cp .agents/templates/task.md "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120000/task.md"
60
67
  ```
61
68
 
62
69
  2. Fill in the task metadata:
63
70
 
64
71
  ```yaml
65
- id: task-001
72
+ id: TASK-20260101-120000
66
73
  type: feature # feature | bugfix | refactor | docs | review
67
74
  status: open # open | in-progress | review | blocked | completed
68
75
  assigned_to: claude # claude | codex | antigravity | opencode | human
@@ -92,7 +99,7 @@ Claude Code excels at codebase exploration and understanding complex relationshi
92
99
  agy
93
100
 
94
101
  # Example prompt:
95
- # "Based on the analysis in .agents/workspace/active/task-001.md,
102
+ # "Based on the analysis in $TASK_WORKSPACE_ROOT/active/TASK-20260101-120000/task.md,
96
103
  # create a technical design. Define interfaces and outline the approach."
97
104
  ```
98
105
 
@@ -103,7 +110,7 @@ agy
103
110
  codex
104
111
 
105
112
  # Example prompt:
106
- # "Implement the changes described in .agents/workspace/active/task-001.md.
113
+ # "Implement the changes described in $TASK_WORKSPACE_ROOT/active/TASK-20260101-120000/task.md.
107
114
  # Follow the design section. Create a new branch for this work."
108
115
  ```
109
116
 
@@ -130,7 +137,8 @@ claude
130
137
 
131
138
  ```bash
132
139
  # Quick bug fix workflow
133
- cp .agents/templates/task.md .agents/workspace/active/bugfix-001.md
140
+ mkdir -p "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120000"
141
+ cp .agents/templates/task.md "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120000/task.md"
134
142
  # Edit the task, then:
135
143
  # 1. Use Claude Code to analyze
136
144
  # 2. Use Codex/OpenCode to fix
@@ -144,7 +152,8 @@ cp .agents/templates/task.md .agents/workspace/active/bugfix-001.md
144
152
  3. **Report**: Generate review report from template.
145
153
 
146
154
  ```bash
147
- cp .agents/templates/review-report.md .agents/workspace/active/review-pr-42.md
155
+ mkdir -p "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120001"
156
+ cp .agents/templates/review-report.md "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120001/review-report.md"
148
157
  # Use Claude Code to fill in the review
149
158
  ```
150
159
 
@@ -156,7 +165,8 @@ cp .agents/templates/review-report.md .agents/workspace/active/review-pr-42.md
156
165
  4. **Verify** (Claude Code): Ensure no regressions, run tests.
157
166
 
158
167
  ```bash
159
- cp .agents/templates/task.md .agents/workspace/active/refactor-001.md
168
+ mkdir -p "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120002"
169
+ cp .agents/templates/task.md "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120002/task.md"
160
170
  # Follow the refactoring workflow in .agents/workflows/refactoring.yaml
161
171
  ```
162
172
 
@@ -165,7 +175,8 @@ cp .agents/templates/task.md .agents/workspace/active/refactor-001.md
165
175
  When switching between AI tools, create a handoff document:
166
176
 
167
177
  ```bash
168
- cp .agents/templates/handoff.md .agents/workspace/active/handoff-task-001-phase2.md
178
+ mkdir -p "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120003"
179
+ cp .agents/templates/handoff.md "$TASK_WORKSPACE_ROOT/active/TASK-20260101-120003/handoff.md"
169
180
  ```
170
181
 
171
182
  Fill in: