@fitlab-ai/agent-infra 0.11.6 → 0.11.8

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 (192) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/git/primary-worktree.js +58 -0
  6. package/dist/lib/internal/cli-route-inventory.js +3 -0
  7. package/dist/lib/internal/platform-pr-review.js +20 -3
  8. package/dist/lib/internal/task-operation-registry.js +3 -1
  9. package/dist/lib/internal/task-platform-recovery.js +56 -0
  10. package/dist/lib/merge.js +25 -5
  11. package/dist/lib/platform/completion-backfill.js +2 -1
  12. package/dist/lib/platform/in-label-sync.js +12 -1
  13. package/dist/lib/platform/issue-comments.js +58 -4
  14. package/dist/lib/platform/issues.js +105 -5
  15. package/dist/lib/platform/operation-coordinator.js +74 -0
  16. package/dist/lib/platform/pr-review.js +107 -1
  17. package/dist/lib/platform/pr-summary.js +79 -57
  18. package/dist/lib/platform/pull-requests.js +125 -7
  19. package/dist/lib/sandbox/commands/create.js +6 -3
  20. package/dist/lib/sandbox/commands/ls.js +4 -3
  21. package/dist/lib/sandbox/commands/rm.js +1 -1
  22. package/dist/lib/sandbox/config.js +22 -4
  23. package/dist/lib/sandbox/control/server.js +6 -2
  24. package/dist/lib/sandbox/git-metadata.js +50 -0
  25. package/dist/lib/sandbox/index.js +1 -1
  26. package/dist/lib/sandbox/mounts.js +12 -0
  27. package/dist/lib/sandbox/recovery.js +11 -0
  28. package/dist/lib/sandbox/removal.js +58 -20
  29. package/dist/lib/sandbox/tools.js +29 -7
  30. package/dist/lib/server/config.js +7 -27
  31. package/dist/lib/server/daemon.js +4 -3
  32. package/dist/lib/server/process-control.js +4 -2
  33. package/dist/lib/server/runner.js +4 -3
  34. package/dist/lib/task/activity-intent.js +4 -3
  35. package/dist/lib/task/artifact-lifecycle.js +42 -10
  36. package/dist/lib/task/artifact-receipts.js +62 -22
  37. package/dist/lib/task/capabilities.js +22 -4
  38. package/dist/lib/task/completion-facts.js +56 -0
  39. package/dist/lib/task/create-service.js +11 -5
  40. package/dist/lib/task/create.js +60 -79
  41. package/dist/lib/task/events.js +268 -195
  42. package/dist/lib/task/finalization-artifacts.js +4 -1
  43. package/dist/lib/task/finalization-handoff.js +2 -1
  44. package/dist/lib/task/finalization.js +157 -24
  45. package/dist/lib/task/invalidation-command.js +4 -2
  46. package/dist/lib/task/invalidation.js +38 -9
  47. package/dist/lib/task/lifecycle.js +38 -2
  48. package/dist/lib/task/local-artifact-finalization.js +17 -3
  49. package/dist/lib/task/platform-operation-journal.js +152 -0
  50. package/dist/lib/task/platform-operation-recovery.js +459 -0
  51. package/dist/lib/task/qualification-audit.js +23 -105
  52. package/dist/lib/task/review-finalization.js +7 -0
  53. package/dist/lib/task/review-identity.js +69 -0
  54. package/dist/lib/task/sections.js +6 -5
  55. package/dist/lib/task/task-execution-lock.js +6 -3
  56. package/dist/lib/task/verification-engine.js +14 -63
  57. package/dist/lib/task/verification.js +35 -38
  58. package/dist/lib/task/workflow-warning-intents.js +7 -1
  59. package/dist/lifecycle-build-manifest.json +3 -3
  60. package/lib/agent-clients/adapters/codex.ts +1 -1
  61. package/lib/agent-clients/config.ts +9 -3
  62. package/lib/defaults.json +6 -3
  63. package/lib/git/primary-worktree.ts +62 -0
  64. package/lib/internal/cli-route-inventory.ts +2 -0
  65. package/lib/internal/platform-pr-review.ts +14 -3
  66. package/lib/internal/task-operation-registry.ts +3 -1
  67. package/lib/internal/task-platform-recovery.ts +53 -0
  68. package/lib/merge.ts +26 -6
  69. package/lib/platform/completion-backfill.ts +2 -1
  70. package/lib/platform/in-label-sync.ts +12 -0
  71. package/lib/platform/issue-comments.ts +59 -3
  72. package/lib/platform/issues.ts +103 -5
  73. package/lib/platform/operation-coordinator.ts +96 -0
  74. package/lib/platform/pr-review.ts +113 -1
  75. package/lib/platform/pr-summary.ts +22 -1
  76. package/lib/platform/pull-requests.ts +132 -8
  77. package/lib/sandbox/commands/create.ts +7 -8
  78. package/lib/sandbox/commands/ls.ts +4 -3
  79. package/lib/sandbox/commands/rm.ts +1 -1
  80. package/lib/sandbox/config.ts +23 -5
  81. package/lib/sandbox/control/server.ts +5 -2
  82. package/lib/sandbox/git-metadata.ts +66 -0
  83. package/lib/sandbox/index.ts +1 -1
  84. package/lib/sandbox/mounts.ts +14 -0
  85. package/lib/sandbox/recovery.ts +13 -0
  86. package/lib/sandbox/removal.ts +57 -19
  87. package/lib/sandbox/tool-types.ts +1 -1
  88. package/lib/sandbox/tools.ts +36 -7
  89. package/lib/server/config.ts +7 -29
  90. package/lib/server/daemon.ts +5 -3
  91. package/lib/server/process-control.ts +4 -2
  92. package/lib/server/runner.ts +7 -4
  93. package/lib/server/server.schema.json +0 -4
  94. package/lib/task/activity-intent.ts +4 -3
  95. package/lib/task/artifact-lifecycle.ts +35 -11
  96. package/lib/task/artifact-receipts.ts +55 -15
  97. package/lib/task/capabilities.ts +17 -3
  98. package/lib/task/completion-facts.ts +66 -0
  99. package/lib/task/create-service.ts +12 -6
  100. package/lib/task/create.ts +54 -69
  101. package/lib/task/events.ts +248 -182
  102. package/lib/task/finalization-artifacts.ts +4 -1
  103. package/lib/task/finalization-handoff.ts +2 -1
  104. package/lib/task/finalization.ts +154 -21
  105. package/lib/task/invalidation-command.ts +4 -2
  106. package/lib/task/invalidation.ts +35 -10
  107. package/lib/task/lifecycle.ts +35 -3
  108. package/lib/task/local-artifact-finalization.ts +13 -3
  109. package/lib/task/platform-operation-journal.ts +230 -0
  110. package/lib/task/platform-operation-recovery.ts +435 -0
  111. package/lib/task/qualification-audit.ts +21 -114
  112. package/lib/task/review-finalization.ts +7 -0
  113. package/lib/task/review-identity.ts +85 -0
  114. package/lib/task/sections.ts +6 -6
  115. package/lib/task/task-execution-lock.ts +7 -3
  116. package/lib/task/verification-engine.ts +13 -86
  117. package/lib/task/verification.ts +34 -38
  118. package/lib/task/workflow-warning-intents.ts +9 -1
  119. package/package.json +1 -1
  120. package/templates/.agents/README.en.md +19 -17
  121. package/templates/.agents/README.zh-CN.md +18 -16
  122. package/templates/.agents/rules/README.en.md +2 -1
  123. package/templates/.agents/rules/README.zh-CN.md +2 -1
  124. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  125. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  126. package/templates/.agents/rules/task-management.en.md +12 -0
  127. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  128. package/templates/.agents/rules/validation-output.en.md +19 -0
  129. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  130. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  131. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  132. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  133. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  134. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
  135. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
  136. package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
  137. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  138. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  139. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  140. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  142. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  143. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  144. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  145. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  146. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  148. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  149. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  151. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  155. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  157. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  158. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  159. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  160. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  161. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  162. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  163. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  164. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  165. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  166. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  167. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  168. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  169. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  170. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  171. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  172. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  173. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  174. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  175. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  176. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  177. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  178. package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
  179. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
  180. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  181. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  182. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  183. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  184. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  191. package/templates/.agents/templates/task.en.md +2 -2
  192. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -0,0 +1,96 @@
1
+ import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
2
+ import { recordPlatformOperation } from '../task/platform-operation-journal.ts';
3
+ import type { RecordOperationInput } from '../task/platform-operation-journal.ts';
4
+ import type { PlatformError, PlatformStatus } from './types.ts';
5
+ import type { PlatformClient } from './context.ts';
6
+
7
+ type OperationResult = Readonly<{
8
+ status: PlatformStatus;
9
+ error?: PlatformError | null;
10
+ warnings?: readonly { code: string }[];
11
+ }>;
12
+
13
+ type CoordinateInput<T extends OperationResult> = Readonly<{
14
+ operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
15
+ agent: string;
16
+ client?: PlatformClient;
17
+ drain?: (taskRef: string, excludeId: string, agent: string, cwd?: string) => Promise<{
18
+ status: 'applied' | 'no-op' | 'blocked' | 'failed';
19
+ error: PlatformError | null;
20
+ }>;
21
+ execute: () => Promise<T>;
22
+ block: (error: PlatformError) => T;
23
+ persistenceFailure: (error: PlatformError) => T;
24
+ }>;
25
+
26
+ async function coordinatePlatformWrite<T extends OperationResult>(input: CoordinateInput<T>): Promise<T> {
27
+ let operationId: string;
28
+ try {
29
+ const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
30
+ operationId = queued.id;
31
+ } catch (error) {
32
+ const value = error as { code?: string; message?: string };
33
+ return input.persistenceFailure({
34
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
35
+ message: value.message || 'Unable to queue platform operation',
36
+ retryable: true
37
+ });
38
+ }
39
+
40
+ const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
41
+ ? 'all' : input.operation.dependency;
42
+ const drained = input.drain
43
+ ? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
44
+ : await recoverPlatformOperations(input.operation.taskRef, selection, {
45
+ agent: input.agent,
46
+ client: input.client,
47
+ cwd: input.operation.cwd,
48
+ excludeId: operationId
49
+ });
50
+ if (drained.status !== 'applied' && drained.status !== 'no-op') {
51
+ return input.block(drained.error ?? {
52
+ code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
53
+ message: 'A previous platform operation is still unresolved; the current write remains queued',
54
+ retryable: true
55
+ });
56
+ }
57
+
58
+ try {
59
+ recordPlatformOperation({ ...input.operation, state: 'pending' });
60
+ } catch (error) {
61
+ const value = error as { code?: string; message?: string };
62
+ return input.persistenceFailure({
63
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
64
+ message: value.message || 'Unable to mark platform operation as started',
65
+ retryable: true
66
+ });
67
+ }
68
+
69
+ let output: T;
70
+ try {
71
+ output = await input.execute();
72
+ } catch (error) {
73
+ const value = error as { code?: string; message?: string };
74
+ output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
75
+ }
76
+ const succeeded = (output.status === 'applied' || output.status === 'no-op')
77
+ && !output.error && !(output.warnings?.length);
78
+ const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
79
+ try {
80
+ recordPlatformOperation({
81
+ ...input.operation,
82
+ state,
83
+ lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
84
+ });
85
+ } catch (error) {
86
+ const value = error as { code?: string; message?: string };
87
+ return input.persistenceFailure({
88
+ code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
89
+ message: value.message || 'Unable to persist platform operation outcome',
90
+ retryable: true
91
+ });
92
+ }
93
+ return output;
94
+ }
95
+
96
+ export { coordinatePlatformWrite };
@@ -1,3 +1,8 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { coordinatePlatformWrite } from './operation-coordinator.ts';
1
6
  import { resolvePlatformProviderContext } from './context.ts';
2
7
  import type { PlatformClient } from './context.ts';
3
8
  import { platformResult } from './types.ts';
@@ -9,8 +14,10 @@ import {
9
14
  providerResourceToken,
10
15
  unsupportedProviderOperation
11
16
  } from './provider-bridge.ts';
12
- import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
17
+ import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
13
18
  import type { ResourceIdentity } from './resource-identity.ts';
19
+ import type { PlatformPullRequestReviewIntent } from '../task/platform-operation-journal.ts';
20
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
14
21
 
15
22
  export type PrReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
16
23
  export type PrReviewIdentity = { scope: string; round: number; commitSha: string; resource?: ResourceIdentity };
@@ -38,6 +45,36 @@ function normalizeBody(body: string): string {
38
45
  return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
39
46
  }
40
47
 
48
+ function canonicalReviewBody(body: string): string {
49
+ return normalizeBody(body).replace(/\n$/u, '');
50
+ }
51
+
52
+ function reviewArtifactName(round: number, artifactFile: string): boolean {
53
+ return path.basename(artifactFile) === artifactFile
54
+ && (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
55
+ }
56
+
57
+ export function readReviewBodyFile(taskRef: string, round: number, artifactFile: string, bodyDigest: string, cwd?: string): string {
58
+ if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
59
+ || !reviewArtifactName(round, artifactFile)) {
60
+ throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
61
+ }
62
+ const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
63
+ if (!task.ok) throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
64
+ const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
65
+ const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
66
+ const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
67
+ .map((match) => match[1]);
68
+ if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
69
+ throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
70
+ }
71
+ const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
72
+ if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
73
+ throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
74
+ }
75
+ return body;
76
+ }
77
+
41
78
  function firstLine(body: string): string {
42
79
  return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
43
80
  }
@@ -90,6 +127,11 @@ export async function publishPrReview(options: {
90
127
  cwd?: string;
91
128
  client?: PlatformClient;
92
129
  dryRun?: boolean;
130
+ agent?: string;
131
+ skipQueue?: boolean;
132
+ expectedResource?: ResourceIdentity;
133
+ expectedProviderScopeId?: string;
134
+ recoveryArtifact?: string;
93
135
  prNumber: PrToken;
94
136
  identity: PrReviewIdentity;
95
137
  event: PrReviewEvent;
@@ -100,6 +142,13 @@ export async function publishPrReview(options: {
100
142
  if (!hasUsableContext(context)) {
101
143
  return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
102
144
  }
145
+ const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
146
+ if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
147
+ return platformResult('failed', {
148
+ platform: context.platform, capabilities: context.capabilities,
149
+ error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
150
+ });
151
+ }
103
152
  const identity = loaded.ok ? (() => { try { return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber)); } catch { return null; } })() : null;
104
153
  if (!identity) {
105
154
  return platformResult('failed', {
@@ -108,6 +157,13 @@ export async function publishPrReview(options: {
108
157
  });
109
158
  }
110
159
  const identityNumber = resourceIdentityNumber(identity);
160
+ if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
161
+ return platformResult('failed', {
162
+ platform: context.platform, capabilities: context.capabilities,
163
+ resource: { kind: 'pull-request', number: identityNumber, identity },
164
+ error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
165
+ });
166
+ }
111
167
  if (!REVIEW_EVENTS.includes(options.event)) {
112
168
  return platformResult('failed', {
113
169
  platform: context.platform, capabilities: context.capabilities,
@@ -123,6 +179,62 @@ export async function publishPrReview(options: {
123
179
  });
124
180
  }
125
181
 
182
+ if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
183
+ if (!options.agent) return platformResult('failed', {
184
+ platform: context.platform, capabilities: context.capabilities,
185
+ resource: { kind: 'pull-request', number: identityNumber, identity },
186
+ error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
187
+ });
188
+ if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact)) return platformResult('failed', {
189
+ platform: context.platform, capabilities: context.capabilities,
190
+ resource: { kind: 'pull-request', number: identityNumber, identity },
191
+ error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
192
+ });
193
+ const body = canonicalReviewBody(options.body);
194
+ const bodyDigest = createHash('sha256').update(body).digest('hex');
195
+ try {
196
+ if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body) throw new Error('body mismatch');
197
+ } catch {
198
+ return platformResult('failed', {
199
+ platform: context.platform, capabilities: context.capabilities,
200
+ resource: { kind: 'pull-request', number: identityNumber, identity },
201
+ error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
202
+ });
203
+ }
204
+ const pullRequestReview: PlatformPullRequestReviewIntent = {
205
+ prNumber: String(options.prNumber),
206
+ resource: identity,
207
+ providerScopeId: providerScopeId!,
208
+ scope: options.identity.scope,
209
+ round: options.identity.round,
210
+ commitSha: options.identity.commitSha,
211
+ event: options.event,
212
+ artifactFile: options.recoveryArtifact,
213
+ bodyDigest
214
+ };
215
+ return coordinatePlatformWrite({
216
+ operation: {
217
+ taskRef: options.identity.scope,
218
+ cwd: options.cwd || process.cwd(),
219
+ kind: 'pull-request-review',
220
+ target: JSON.stringify(identity),
221
+ expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
222
+ dependency: 'deferred',
223
+ pullRequestReview
224
+ },
225
+ agent: options.agent,
226
+ execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId!, skipQueue: true }),
227
+ block: (error) => platformResult('blocked', {
228
+ platform: context.platform, capabilities: context.capabilities,
229
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error
230
+ }),
231
+ persistenceFailure: (error) => platformResult('failed', {
232
+ platform: context.platform, capabilities: context.capabilities,
233
+ resource: { kind: 'pull-request', number: identityNumber, identity }, error
234
+ })
235
+ });
236
+ }
237
+
126
238
  const marker = reviewMarker({ ...options.identity, resource: identity });
127
239
  const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
128
240
  if (listed.status === 'failed' || listed.status === 'blocked') return listed;
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
3
4
 
4
5
  import { enumerateArtifacts } from '../task/artifacts.ts';
5
6
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
@@ -13,6 +14,7 @@ import { normalizeCommentContent } from './issue-comments.ts';
13
14
  import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
14
15
  import { platformResult } from './types.ts';
15
16
  import type { PlatformResult } from './types.ts';
17
+ import { coordinatePlatformWrite } from './operation-coordinator.ts';
16
18
  import type { OperationWarning } from '../task/operation-outcome.ts';
17
19
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
18
20
  import { resourceIdentityEquals } from './resource-identity.ts';
@@ -491,7 +493,7 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
491
493
 
492
494
  async function syncPullRequestSummary(
493
495
  taskRef: string,
494
- options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean }
496
+ options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean; skipQueue?: boolean }
495
497
  ): Promise<PullRequestSummaryResult> {
496
498
  const warningResult = warningResultForPrimary(options.primaryResult);
497
499
  let knownPrNumber: number | null = null;
@@ -657,6 +659,7 @@ async function syncPullRequestSummary(
657
659
  operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
658
660
  result: null, warnings: [], ...info
659
661
  };
662
+ const publish = async (): Promise<PullRequestSummaryResult> => {
660
663
  const existingComment = reconciliation.commentId === null
661
664
  ? undefined
662
665
  : providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
@@ -722,6 +725,24 @@ async function syncPullRequestSummary(
722
725
  operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
723
726
  result: null, warnings: [], ...info
724
727
  };
728
+ };
729
+ const operation = {
730
+ taskRef: resolved.taskId,
731
+ cwd: resolved.repoRoot,
732
+ kind: 'pull-request-summary' as const,
733
+ target: JSON.stringify(prIdentity),
734
+ expectedDigest: createHash('sha256').update(desired).digest('hex'),
735
+ dependency: 'deferred' as const,
736
+ pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile! }
737
+ };
738
+ if (options.skipQueue) return publish();
739
+ return coordinatePlatformWrite({
740
+ operation,
741
+ agent: options.agent,
742
+ execute: publish,
743
+ block: (error) => fail('blocked', context, error, prNumber),
744
+ persistenceFailure: (error) => fail('failed', context, error, prNumber)
745
+ });
725
746
  };
726
747
  return await (options.lockAlreadyHeld
727
748
  ? execute()
@@ -1,6 +1,7 @@
1
1
  import { normalizePullRequest, type RemotePullRequest } from './github-data.ts';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
4
5
  import { execFileSync } from 'node:child_process';
5
6
 
6
7
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
@@ -41,6 +42,8 @@ import {
41
42
  import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, serializeResourceIdentity } from './resource-identity.ts';
42
43
  import type { ResourceIdentity } from './resource-identity.ts';
43
44
  import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
45
+ import { coordinatePlatformWrite } from './operation-coordinator.ts';
46
+ import type { PlatformPullRequestIntent } from '../task/platform-operation-journal.ts';
44
47
  import type { ChangeRequestSnapshot as ProviderChangeRequestSnapshot, IssueSnapshot as ProviderIssueSnapshot } from './provider-contract.ts';
45
48
 
46
49
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
@@ -63,7 +66,7 @@ type PullRequestResult = PlatformResult & {
63
66
  }>;
64
67
  };
65
68
  type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
66
- type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
69
+ type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string; skipQueue?: boolean };
67
70
  type CreateOptions = SharedOptions & {
68
71
  agent: string;
69
72
  base: string;
@@ -137,6 +140,30 @@ function withCreation(output: PullRequestResult, creation: CreationOutcome): Pul
137
140
  return { ...output, creation };
138
141
  }
139
142
 
143
+ async function journalPullRequestOperation(
144
+ taskRef: string,
145
+ options: SharedOptions & { agent: string; dryRun?: boolean },
146
+ intent: PlatformPullRequestIntent,
147
+ target: string,
148
+ identityInput: unknown,
149
+ execute: () => Promise<PullRequestResult>
150
+ ): Promise<PullRequestResult> {
151
+ if (options.dryRun) return execute();
152
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
153
+ if (!resolved.ok) return execute();
154
+ const expectedDigest = createHash('sha256').update(JSON.stringify({ taskId: resolved.taskId, intent, identityInput })).digest('hex');
155
+ const operation = { kind: 'pull-request' as const, target: intent.action === 'sync' ? `bound-pr:${resolved.taskId}` : target, expectedDigest };
156
+ const dependency = intent.action === 'sync' ? 'deferred' : 'required';
157
+ if (options.skipQueue) return execute();
158
+ return coordinatePlatformWrite({
159
+ operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, pullRequest: intent, dependency },
160
+ agent: options.agent,
161
+ execute,
162
+ block: (error) => result('blocked', resolved.taskId, null, null, { error }),
163
+ persistenceFailure: (error) => result('failed', resolved.taskId, null, null, { error })
164
+ });
165
+ }
166
+
140
167
  const PRECONDITION_NOT_CREATED: CreationOutcome = {
141
168
  kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
142
169
  };
@@ -851,7 +878,7 @@ function bindIdentity(
851
878
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
852
879
  }
853
880
 
854
- async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
881
+ async function bindPlatformPullRequestImpl(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
855
882
  const base = await resolvedContext(taskRef, options);
856
883
  if (!base.ok) return base.output;
857
884
  if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -884,6 +911,12 @@ async function bindPlatformPullRequest(taskRef: string, options: BindOptions): P
884
911
  });
885
912
  }
886
913
 
914
+ async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
915
+ const intent: PlatformPullRequestIntent = { action: 'bind', prToken: String(options.pr) };
916
+ return journalPullRequestOperation(taskRef, options, intent, `pr:${options.pr}`, { pr: String(options.pr) },
917
+ () => bindPlatformPullRequestImpl(taskRef, options));
918
+ }
919
+
887
920
  function externalEvidenceNote(
888
921
  issueIdentity: ResourceIdentity | null,
889
922
  source: 'unique' | 'explicit',
@@ -1077,7 +1110,7 @@ async function resolveExternalPullRequest(taskRef: string, options: ResolveExter
1077
1110
  }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
1078
1111
  }
1079
1112
 
1080
- async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
1113
+ async function createPlatformPullRequestImpl(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
1081
1114
  const base = await resolvedContext(taskRef, options);
1082
1115
  if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
1083
1116
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
@@ -1112,6 +1145,66 @@ async function createPlatformPullRequest(taskRef: string, options: CreateOptions
1112
1145
  }
1113
1146
  }
1114
1147
 
1148
+ async function recoverCreatedPullRequest(
1149
+ taskRef: string,
1150
+ options: SharedOptions & { agent: string; base: string; head: string }
1151
+ ): Promise<PullRequestResult> {
1152
+ const base = await resolvedContext(taskRef, options);
1153
+ if (!base.ok) return base.output;
1154
+ if (!base.issueIdentity) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1155
+ error: { code: 'PLATFORM_OPERATION_ISSUE_REQUIRED', message: 'Pull-request recovery requires a bound Issue identity', retryable: false }
1156
+ });
1157
+ const head = base.provider.changeRequests?.verifyHead
1158
+ ? await base.provider.changeRequests.verifyHead({ context: providerOperationContext(base.loadedContext), head: options.head })
1159
+ : unsupportedProviderOperation(base.provider, 'changeRequests.verifyHead');
1160
+ if (!head.ok) return providerPullRequestError(base, head.error, base.prNumber);
1161
+ if (base.prIdentity) {
1162
+ const inspected = await inspectExternalPullRequest(base, base.prIdentity);
1163
+ if (!inspected.pullRequest || inspected.status === 'failed' || inspected.status === 'blocked'
1164
+ || inspected.pullRequest.base.repository !== base.context.platform.repository
1165
+ || inspected.pullRequest.base.ref !== options.base || inspected.pullRequest.head.ref !== options.head
1166
+ || inspected.pullRequest.head.sha !== head.value.sha) {
1167
+ return result('blocked', base.resolved.taskId, base.issueNumber, base.prNumber, {
1168
+ error: inspected.error ?? { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request does not match the pending create intent', retryable: true }
1169
+ });
1170
+ }
1171
+ return inspected;
1172
+ }
1173
+ const candidates = base.provider.changeRequests?.listClosing
1174
+ ? await base.provider.changeRequests.listClosing({ context: providerOperationContext(base.loadedContext), issue: base.issueIdentity })
1175
+ : unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
1176
+ if (!candidates.ok) return providerPullRequestError(base, candidates.error, null);
1177
+ const matches = candidates.value.map((candidate) => normalizeProviderPullRequest(candidate, base.context.platform.repository!, candidate.number || 0))
1178
+ .filter((candidate) => candidate.base.repository === base.context.platform.repository
1179
+ && candidate.base.ref === options.base
1180
+ && candidate.head.repository === base.context.platform.repository
1181
+ && candidate.head.ref === options.head
1182
+ && candidate.head.sha === head.value.sha);
1183
+ if (matches.length !== 1 || !matches[0]?.number) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
1184
+ error: { code: matches.length > 1 ? 'PLATFORM_OPERATION_PR_AMBIGUOUS' : 'PLATFORM_OPERATION_PR_NOT_CONFIRMED',
1185
+ message: 'Pending pull-request creation does not resolve to one exact repository/base/head identity', retryable: true }
1186
+ });
1187
+ const bound = await bindPlatformPullRequest(base.resolved.taskId, { agent: options.agent, cwd: base.resolved.repoRoot, pr: matches[0].number });
1188
+ if (!bound.pullRequest || (bound.status !== 'applied' && bound.status !== 'no-op') || bound.error) return bound;
1189
+ if (bound.pullRequest.base.repository !== base.context.platform.repository || bound.pullRequest.base.ref !== options.base
1190
+ || bound.pullRequest.head.repository !== base.context.platform.repository || bound.pullRequest.head.ref !== options.head
1191
+ || bound.pullRequest.head.sha !== head.value.sha) return result('blocked', base.resolved.taskId, base.issueNumber, bound.task.prNumber, {
1192
+ error: { code: 'PLATFORM_OPERATION_PR_NOT_CONFIRMED', message: 'Bound pull request changed while recovering the create intent', retryable: true }
1193
+ });
1194
+ return bound;
1195
+ }
1196
+
1197
+ async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
1198
+ const intent: PlatformPullRequestIntent = { action: 'create', baseRef: options.base, headRef: options.head };
1199
+ const identityInput = {
1200
+ title: createHash('sha256').update(options.title).digest('hex'),
1201
+ body: createHash('sha256').update(options.body).digest('hex'),
1202
+ draft: options.draft === true
1203
+ };
1204
+ return journalPullRequestOperation(taskRef, options, intent, `head:${options.head}:base:${options.base}`, identityInput,
1205
+ () => createPlatformPullRequestImpl(taskRef, options));
1206
+ }
1207
+
1115
1208
  async function createExternalPullRequest(
1116
1209
  taskRef: string,
1117
1210
  options: CreateOptions,
@@ -1140,9 +1233,33 @@ async function createExternalPullRequest(
1140
1233
  }
1141
1234
  if (base.prIdentity) {
1142
1235
  const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
1143
- return inspected.status === 'failed' || inspected.status === 'blocked'
1144
- ? withCreation(inspected, PRECONDITION_NOT_CREATED)
1145
- : withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1236
+ if (inspected.status === 'failed' || inspected.status === 'blocked') {
1237
+ return withCreation(inspected, PRECONDITION_NOT_CREATED);
1238
+ }
1239
+ const deliveredHeadSha = typeof base.frontmatter.delivery_remote_head === 'string'
1240
+ ? base.frontmatter.delivery_remote_head
1241
+ : undefined;
1242
+ const expectedHeadSha = verifiedHeadSha ?? deliveredHeadSha;
1243
+ if (!expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1244
+ platform: base.context.platform,
1245
+ capabilities: base.context.capabilities,
1246
+ resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
1247
+ pullRequest: inspected.pullRequest,
1248
+ error: { code: 'PR_HEAD_UNVERIFIED', message: 'Unable to verify the bound pull request head SHA', retryable: false }
1249
+ }), PRECONDITION_NOT_CREATED);
1250
+ const identity = validateWriterIdentity(base, inspected.pullRequest ?? null, {
1251
+ expectedHead: options.head,
1252
+ expectedHeadSha,
1253
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
1254
+ });
1255
+ if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1256
+ platform: base.context.platform,
1257
+ capabilities: base.context.capabilities,
1258
+ resource: { kind: 'pull-request', number: base.prNumber, identity: base.prIdentity },
1259
+ pullRequest: inspected.pullRequest,
1260
+ error: identity.error
1261
+ }), PRECONDITION_NOT_CREATED);
1262
+ return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
1146
1263
  }
1147
1264
  if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
1148
1265
  platform: base.context.platform,
@@ -1242,7 +1359,7 @@ async function createExternalPullRequest(
1242
1359
  }), { kind: 'created', createdByCurrentOperation: true });
1243
1360
  }
1244
1361
 
1245
- async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
1362
+ async function syncPlatformPullRequestImpl(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
1246
1363
  const warningResult = warningResultForPrimary(options.primaryResult);
1247
1364
  const softenFailure = (output: PullRequestResult): PullRequestResult => {
1248
1365
  const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
@@ -1406,6 +1523,13 @@ async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): P
1406
1523
  }
1407
1524
  }
1408
1525
 
1526
+ async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
1527
+ const intent: PlatformPullRequestIntent = { action: 'sync', metadata: options.metadata === true, closingIssue: options.closingIssue === true };
1528
+ return journalPullRequestOperation(taskRef, options, intent, `bound-pr:${taskRef}`, {
1529
+ metadata: options.metadata === true, closingIssue: options.closingIssue === true
1530
+ }, () => syncPlatformPullRequestImpl(taskRef, options));
1531
+ }
1532
+
1409
1533
  function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
1410
1534
  try {
1411
1535
  const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { prFlow?: unknown };
@@ -1503,6 +1627,6 @@ async function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOpt
1503
1627
  }
1504
1628
  }
1505
1629
 
1506
- export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
1630
+ export { bindPlatformPullRequest, createPlatformPullRequest, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, recoverCreatedPullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
1507
1631
  export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
1508
1632
  export { warningResultForPrimary };
@@ -83,6 +83,7 @@ import { atomicWriteJson } from '../control/state.ts';
83
83
  import { inspectSandboxControlContainer } from '../control/container-identity.ts';
84
84
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
85
85
  import { sandboxCoreBindMounts } from '../mounts.ts';
86
+ import { materializeSandboxGitMetadata } from '../git-metadata.ts';
86
87
  import {
87
88
  assertSandboxTaskSource,
88
89
  finalizeSandboxControlManifest,
@@ -950,9 +951,10 @@ function runSandboxAuthoritySafeCommand(
950
951
 
951
952
  // `docker run` args for mounting a tool's containerMount as an in-container
952
953
  // tmpfs. containerMount is an in-container path, so it is NOT engine-converted.
953
- export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string }): string[] {
954
+ export function buildTmpfsRunArgs(containerMount: string, tmpfs: { size?: string; exec?: boolean }): string[] {
954
955
  const size = tmpfs.size ?? '512m';
955
- return ['--tmpfs', `${containerMount}:rw,size=${size}`];
956
+ const options = ['rw', ...(tmpfs.exec ? ['exec'] : []), `size=${size}`];
957
+ return ['--tmpfs', `${containerMount}:${options.join(',')}`];
956
958
  }
957
959
 
958
960
  export function buildImage(
@@ -1459,6 +1461,7 @@ export async function create(
1459
1461
  engine,
1460
1462
  replacementLease,
1461
1463
  });
1464
+ const gitMetadata = materializeSandboxGitMetadata(worktree, control.channelDir);
1462
1465
  hostShellConfig = prepareHostShellConfig({
1463
1466
  home: effectiveConfig.home,
1464
1467
  project: effectiveConfig.project,
@@ -1466,6 +1469,8 @@ export async function create(
1466
1469
  repoRoot: effectiveConfig.repoRoot
1467
1470
  });
1468
1471
  const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
1472
+ gitCommonDir: gitMetadata.commonDir,
1473
+ worktreeGitFile: gitMetadata.worktreeGitFile,
1469
1474
  worktree,
1470
1475
  shellConfigHostDir: hostShellConfig.hostDir,
1471
1476
  workspaceViewRoot: workspaceView.root,
@@ -1572,12 +1577,6 @@ export async function create(
1572
1577
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1573
1578
  ...coreVolumes,
1574
1579
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1575
- '-v',
1576
- volumeArg(
1577
- engine,
1578
- path.join(effectiveConfig.repoRoot, '.git'),
1579
- `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`
1580
- ),
1581
1580
  ...dotfilesMount,
1582
1581
  ...toolVolumes,
1583
1582
  ...tmpfsArgs,
@@ -22,8 +22,9 @@ display-only row number; the 'SHORT' column shows the active task short
22
22
  id bound to each container's branch (via
23
23
  .agents/workspace/active/.short-ids.json), or '-' if no active task is
24
24
  bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
25
- A '-' means no active task is bound to that branch, so the sandbox is free
26
- to remove with "ai sandbox rm <branch>".
25
+ A SHORT value of '-' means this container has no active task short id and may
26
+ be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
27
+ remove a specific task-bound sandbox.
27
28
 
28
29
  Use "ai sandbox show <ref>" for a single sandbox's worktree and per-tool
29
30
  state paths.`;
@@ -97,7 +98,7 @@ export function ls(args: string[] = []): void {
97
98
  }
98
99
  if (tableRows.some((r) => r.shortId === '-')) {
99
100
  process.stdout.write(
100
- ` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`
101
+ ` SHORT '-' = no active task short id is displayed; --unbound may clean this sandbox if its branch is not tied to an active task.\n`
101
102
  );
102
103
  }
103
104
  }
@@ -14,7 +14,7 @@ import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
14
14
 
15
15
  const USAGE = `Usage:
16
16
  ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
17
- ai sandbox rm --unbound [--dry-run] [--yes] Remove discovered task-bound and branch-only sandboxes
17
+ ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
18
18
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
19
19
 
20
20
  function resolveMissingTaskSandboxBranch(taskId: string, config: ReturnType<typeof loadConfig>): string | null {