@fitlab-ai/agent-infra 0.9.15 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  7. package/dist/lib/fs/durable-write.js +2 -0
  8. package/dist/lib/internal/cli-route-inventory.js +3 -4
  9. package/dist/lib/internal/manual-validation.js +17 -8
  10. package/dist/lib/internal/platform-comment.js +60 -2
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-finalization.js +2 -2
  13. package/dist/lib/internal/task-lifecycle.js +4 -4
  14. package/dist/lib/internal/task-operation-registry.js +22 -13
  15. package/dist/lib/internal/task-orchestration.js +2 -2
  16. package/dist/lib/internal/task-review.js +12 -7
  17. package/dist/lib/platform/activity-recovery.js +159 -0
  18. package/dist/lib/platform/issue-comments.js +89 -31
  19. package/dist/lib/platform/issues.js +1 -1
  20. package/dist/lib/platform/pr-change-report.js +5 -4
  21. package/dist/lib/platform/pr-summary.js +64 -32
  22. package/dist/lib/platform/pull-requests.js +3 -3
  23. package/dist/lib/platform/task-comment-projection.js +70 -0
  24. package/dist/lib/platform/task-identities.js +2 -9
  25. package/dist/lib/platform/task-recovery.js +55 -0
  26. package/dist/lib/platform/verification-sync.js +45 -7
  27. package/dist/lib/pr-review/evidence-grading.js +4 -8
  28. package/dist/lib/sandbox/commands/rm.js +21 -2
  29. package/dist/lib/sandbox/control/executor.js +4 -2
  30. package/dist/lib/sandbox/control/server.js +29 -9
  31. package/dist/lib/sandbox/control/state.js +7 -1
  32. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  33. package/dist/lib/sandbox/control/workflow-executor.js +87 -35
  34. package/dist/lib/sandbox/index.js +1 -2
  35. package/dist/lib/sandbox/removal.js +116 -1011
  36. package/dist/lib/sandbox/workspace-identity.js +17 -1
  37. package/dist/lib/server/process-state.js +17 -5
  38. package/dist/lib/task/artifact-command.js +11 -30
  39. package/dist/lib/task/artifact-operations.js +17 -231
  40. package/dist/lib/task/artifact-recovery.js +476 -0
  41. package/dist/lib/task/artifact-repair-intent.js +90 -39
  42. package/dist/lib/task/commit-operation.js +31 -4
  43. package/dist/lib/task/control-authority.js +39 -8
  44. package/dist/lib/task/control-recovery.js +14 -2
  45. package/dist/lib/task/create-service.js +1 -8
  46. package/dist/lib/task/delegation-receipts.js +64 -3
  47. package/dist/lib/task/events.js +165 -23
  48. package/dist/lib/task/human-override.js +5 -8
  49. package/dist/lib/task/intermediate-cleanup.js +91 -8
  50. package/dist/lib/task/lifecycle-recovery.js +549 -0
  51. package/dist/lib/task/lifecycle.js +12 -3
  52. package/dist/lib/task/local-artifact-finalization.js +212 -123
  53. package/dist/lib/task/manual-validation-receipt.js +5 -1
  54. package/dist/lib/task/orchestration.js +52 -2
  55. package/dist/lib/task/pr-delivery-fact.js +12 -56
  56. package/dist/lib/task/recovery-warning.js +19 -0
  57. package/dist/lib/task/review-command.js +3 -2
  58. package/dist/lib/task/review-finalization.js +294 -74
  59. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  60. package/dist/lib/task/short-id.js +15 -7
  61. package/dist/lib/task/task-execution-lock.js +1 -1
  62. package/dist/lib/task/verification-engine.js +3 -14
  63. package/dist/lib/task/workflow-command.js +2 -1
  64. package/dist/lib/task/workflow-dispatch.js +161 -0
  65. package/dist/lib/version.js +1 -11
  66. package/dist/lifecycle-build-manifest.json +3 -3
  67. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  68. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  69. package/lib/fs/durable-write.ts +1 -0
  70. package/lib/internal/cli-route-inventory.ts +3 -4
  71. package/lib/internal/manual-validation.ts +17 -8
  72. package/lib/internal/platform-comment.ts +53 -2
  73. package/lib/internal/task-artifact.ts +1 -1
  74. package/lib/internal/task-finalization.ts +1 -1
  75. package/lib/internal/task-lifecycle.ts +5 -4
  76. package/lib/internal/task-operation-registry.ts +22 -13
  77. package/lib/internal/task-orchestration.ts +1 -1
  78. package/lib/internal/task-review.ts +11 -7
  79. package/lib/platform/activity-recovery.ts +179 -0
  80. package/lib/platform/issue-comments.ts +105 -30
  81. package/lib/platform/issues.ts +1 -1
  82. package/lib/platform/pr-change-report.ts +7 -5
  83. package/lib/platform/pr-summary.ts +64 -32
  84. package/lib/platform/pull-requests.ts +3 -3
  85. package/lib/platform/task-comment-projection.ts +79 -0
  86. package/lib/platform/task-identities.ts +3 -14
  87. package/lib/platform/task-recovery.ts +54 -0
  88. package/lib/platform/verification-sync.ts +48 -11
  89. package/lib/pr-review/evidence-grading.ts +4 -9
  90. package/lib/sandbox/commands/rm.ts +34 -2
  91. package/lib/sandbox/control/executor.ts +4 -2
  92. package/lib/sandbox/control/protocol.ts +6 -1
  93. package/lib/sandbox/control/server.ts +30 -10
  94. package/lib/sandbox/control/state.ts +9 -1
  95. package/lib/sandbox/control/task-workflow.ts +1 -1
  96. package/lib/sandbox/control/workflow-executor.ts +106 -35
  97. package/lib/sandbox/index.ts +1 -2
  98. package/lib/sandbox/removal.ts +126 -1128
  99. package/lib/sandbox/workspace-identity.ts +19 -3
  100. package/lib/server/process-state.ts +26 -5
  101. package/lib/task/artifact-command.ts +12 -29
  102. package/lib/task/artifact-operations.ts +22 -252
  103. package/lib/task/artifact-recovery.ts +572 -0
  104. package/lib/task/artifact-repair-intent.ts +126 -51
  105. package/lib/task/commit-operation.ts +35 -4
  106. package/lib/task/control-authority.ts +48 -15
  107. package/lib/task/control-recovery.ts +13 -1
  108. package/lib/task/create-service.ts +1 -10
  109. package/lib/task/delegation-receipts.ts +73 -2
  110. package/lib/task/events.ts +188 -24
  111. package/lib/task/human-override.ts +5 -8
  112. package/lib/task/intermediate-cleanup.ts +66 -12
  113. package/lib/task/lifecycle-recovery.ts +691 -0
  114. package/lib/task/lifecycle.ts +13 -4
  115. package/lib/task/local-artifact-finalization.ts +249 -131
  116. package/lib/task/manual-validation-receipt.ts +5 -1
  117. package/lib/task/orchestration.ts +65 -0
  118. package/lib/task/pr-delivery-fact.ts +15 -62
  119. package/lib/task/recovery-warning.ts +21 -0
  120. package/lib/task/review-command.ts +2 -1
  121. package/lib/task/review-finalization.ts +298 -84
  122. package/lib/{host-control → task}/secure-fs.ts +10 -32
  123. package/lib/task/short-id.ts +27 -5
  124. package/lib/task/task-execution-lock.ts +1 -1
  125. package/lib/task/verification-engine.ts +3 -17
  126. package/lib/task/verification-types.ts +0 -2
  127. package/lib/task/workflow-command.ts +2 -1
  128. package/lib/task/workflow-dispatch.ts +171 -0
  129. package/lib/version.ts +1 -15
  130. package/package.json +1 -1
  131. package/templates/.agents/rules/create-issue.en.md +1 -1
  132. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  133. package/templates/.agents/rules/issue-sync.en.md +12 -2
  134. package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
  135. package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
  136. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
  137. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  138. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  139. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  140. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  141. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  142. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  143. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  144. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  145. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  146. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  147. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  148. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
  149. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  150. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  151. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  152. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  153. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  154. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  155. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  157. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  158. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  159. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  160. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  161. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  162. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  163. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  164. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  165. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  166. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  167. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  168. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  169. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  170. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  171. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  172. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  173. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  174. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  175. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  176. package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
  177. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
  178. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  179. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  180. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  181. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  182. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  183. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  184. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  185. package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
  186. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
  187. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  188. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  189. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  190. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  191. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  192. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  193. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  194. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  195. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  196. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  197. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  198. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  199. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  200. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  201. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  202. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  203. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/templates/task.en.md +1 -1
  206. package/templates/.agents/templates/task.zh-CN.md +1 -1
  207. package/templates/.claude/commands/import-issue.en.md +1 -1
  208. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  209. package/templates/.claude/commands/restore-task.en.md +1 -1
  210. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  211. package/dist/lib/host-control/audit.js +0 -19
  212. package/dist/lib/host-control/client.js +0 -142
  213. package/dist/lib/host-control/command.js +0 -49
  214. package/dist/lib/host-control/path.js +0 -160
  215. package/dist/lib/host-control/server.js +0 -159
  216. package/dist/lib/host-control/service.js +0 -112
  217. package/dist/lib/internal/host-control.js +0 -28
  218. package/lib/host-control/audit.ts +0 -22
  219. package/lib/host-control/client.ts +0 -172
  220. package/lib/host-control/command.ts +0 -60
  221. package/lib/host-control/path.ts +0 -167
  222. package/lib/host-control/server.ts +0 -180
  223. package/lib/host-control/service.ts +0 -133
  224. package/lib/internal/host-control.ts +0 -28
@@ -11,6 +11,7 @@ import { normalizeCommentContent } from './issue-comments.js';
11
11
  import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
12
12
  import { platformResult } from './types.js';
13
13
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
14
+ import { resourceIdentityEquals } from './resource-identity.js';
14
15
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
15
16
  import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
16
17
  import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
@@ -48,7 +49,8 @@ function readBoundTaskSnapshot(taskMdPath) {
48
49
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
49
50
  const fact = readPrDeliveryFact(frontmatter);
50
51
  const boundFact = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact : null;
51
- return { taskContent, boundFact };
52
+ const factError = fact.status === 'invalid' ? { code: fact.error.code, message: fact.error.message } : null;
53
+ return { taskContent, boundFact, factError };
52
54
  }
53
55
  function boundPrNumberForWarning(taskMdPath) {
54
56
  try {
@@ -117,7 +119,7 @@ function isHardSummaryFailure(error) {
117
119
  function inspectionError(error) {
118
120
  return error.retryable ? 'blocked' : 'failed';
119
121
  }
120
- async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContext) {
122
+ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedContext) {
121
123
  if (!context.platform.repository) {
122
124
  return { ok: false, status: context.status, error: context.error };
123
125
  }
@@ -126,16 +128,15 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
126
128
  : await resolvePlatformProviderContext({ cwd: repoRoot });
127
129
  if (!loaded.ok)
128
130
  return { ok: false, status: loaded.context.status, error: loaded.context.error };
129
- const target = providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber));
130
131
  const inspected = loaded.value.provider.changeRequests?.inspect
131
- ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target })
132
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: prIdentity })
132
133
  : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
133
134
  if (!inspected.ok) {
134
135
  const error = providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED');
135
136
  return { ok: false, status: inspectionError(error), error };
136
137
  }
137
138
  const remote = inspected.value;
138
- const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? prNumber;
139
+ const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
139
140
  const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
140
141
  const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
141
142
  return {
@@ -143,6 +144,7 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
143
144
  value: {
144
145
  repository: context.platform.repository,
145
146
  number,
147
+ identity: remote.identity ?? prIdentity,
146
148
  nodeId: remote.id,
147
149
  url: remote.displayUrl || '',
148
150
  state: remote.state === 'closed' ? 'closed' : 'open',
@@ -163,20 +165,22 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
163
165
  function sameIdentity(left, right) {
164
166
  return JSON.stringify({
165
167
  repository: left.repository,
166
- number: left.number,
168
+ identity: left.identity ?? null,
167
169
  base: left.base,
168
170
  head: left.head
169
171
  }) === JSON.stringify({
170
172
  repository: right.repository,
171
- number: right.number,
173
+ identity: right.identity ?? null,
172
174
  base: right.base,
173
175
  head: right.head
174
176
  });
175
177
  }
176
178
  function identityFromSnapshot(snapshot) {
179
+ if (!snapshot.identity)
180
+ throw new Error('pull-request snapshot has no canonical identity');
177
181
  return {
178
182
  repository: snapshot.repository,
179
- number: snapshot.number,
183
+ resource: snapshot.identity,
180
184
  base: { ...snapshot.base },
181
185
  head: { ...snapshot.head }
182
186
  };
@@ -205,7 +209,7 @@ function currentReportCheck(report, snapshot, taskContent, repoRoot) {
205
209
  if (!digest.ok)
206
210
  return { ok: false, error: platformError(digest.error) };
207
211
  const reportIdentity = report.identity;
208
- if (reportIdentity.repository !== snapshot.repository || reportIdentity.number !== snapshot.number ||
212
+ if (reportIdentity.repository !== snapshot.repository || !resourceIdentityEquals(reportIdentity.resource, snapshot.identity) ||
209
213
  JSON.stringify(reportIdentity.base) !== JSON.stringify(snapshot.base) || JSON.stringify(reportIdentity.head) !== JSON.stringify(snapshot.head))
210
214
  return {
211
215
  ok: false,
@@ -263,14 +267,23 @@ async function summaryContext(taskRef, options = {}) {
263
267
  };
264
268
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
265
269
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
266
- const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
267
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? resourceIdentityNumber(fact.fact.identity.resource) : null;
268
270
  const reportPath = taskReportPath(resolved.taskDir);
271
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
272
+ if (fact.status === 'invalid')
273
+ return {
274
+ ...platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }),
275
+ task: { id: resolved.taskId, prNumber: null },
276
+ pullRequest: null,
277
+ changeReport: { status: 'invalid', path: reportPath, taskIntentSha256: null, reason: fact.error.message },
278
+ artifacts: []
279
+ };
280
+ const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
281
+ const prNumber = resourceIdentityNumber(prIdentity);
269
282
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
270
283
  const context = loaded.ok ? loaded.value.context : loaded.context;
271
284
  let pullRequest = null;
272
- if (loaded.ok && prNumber && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
273
- const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
285
+ if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
286
+ const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
274
287
  if (inspected.ok)
275
288
  pullRequest = inspected.value;
276
289
  }
@@ -290,14 +303,15 @@ async function summaryContext(taskRef, options = {}) {
290
303
  }
291
304
  async function summaryCommentState(taskRef, options = {}) {
292
305
  const context = await summaryContext(taskRef, options);
293
- if (!context.task.prNumber || !context.pullRequest)
306
+ const prIdentity = context.pullRequest?.identity;
307
+ if (!prIdentity)
294
308
  return { ...context, comment: null };
295
309
  const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
296
310
  if (!loaded.ok || !loaded.value.provider.comments?.list)
297
311
  return { ...context, comment: null };
298
312
  const listed = await loaded.value.provider.comments.list({
299
313
  context: providerOperationContext(loaded.value),
300
- parent: providerResourceToken(loaded.value.provider, 'pull-request', String(context.task.prNumber))
314
+ parent: prIdentity
301
315
  });
302
316
  if (!listed.ok)
303
317
  return { ...context, comment: null };
@@ -311,6 +325,21 @@ async function reportWrite(taskRef, options) {
311
325
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
312
326
  if (!resolved.ok)
313
327
  return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), report: null };
328
+ let preflight;
329
+ try {
330
+ preflight = readBoundTaskSnapshot(resolved.taskMdPath);
331
+ }
332
+ catch (error) {
333
+ return {
334
+ ...platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } }),
335
+ report: null
336
+ };
337
+ }
338
+ if (preflight.factError)
339
+ return {
340
+ ...platformResult('failed', { error: { code: preflight.factError.code, message: preflight.factError.message, retryable: false } }),
341
+ report: null
342
+ };
314
343
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
315
344
  const context = loaded.ok ? loaded.value.context : loaded.context;
316
345
  if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status))
@@ -318,20 +347,21 @@ async function reportWrite(taskRef, options) {
318
347
  let knownPrNumber = null;
319
348
  try {
320
349
  return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
321
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
322
- if (!boundFact)
350
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
351
+ if (factError)
323
352
  return {
324
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
353
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: factError.code, message: factError.message, retryable: false }),
325
354
  report: null
326
355
  };
327
- const boundPrNumber = resourceIdentityNumber(boundFact.identity.resource);
328
- if (!boundPrNumber)
356
+ if (!boundFact)
329
357
  return {
330
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false }),
358
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
331
359
  report: null
332
360
  };
361
+ const boundPrIdentity = boundFact.identity.resource;
362
+ const boundPrNumber = resourceIdentityNumber(boundPrIdentity);
333
363
  knownPrNumber = boundPrNumber;
334
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
364
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
335
365
  if (!initial.ok)
336
366
  return { ...basePlatformResult(initial.status, context, resolved.taskId, boundPrNumber, initial.error), report: null };
337
367
  const digest = taskIntentDigest(taskContent);
@@ -364,7 +394,7 @@ async function reportWrite(taskRef, options) {
364
394
  const built = buildPrChangeReport(identityFromSnapshot(initial.value), digest.value.sha256, mechanical.value, candidate.value);
365
395
  if (!built.ok)
366
396
  return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(built.error)), report: null };
367
- const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
397
+ const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
368
398
  if (!final.ok)
369
399
  return { ...basePlatformResult(final.status, context, resolved.taskId, boundPrNumber, final.error), report: null };
370
400
  if (!sameIdentity(initial.value, final.value))
@@ -466,7 +496,12 @@ async function syncPullRequestSummary(taskRef, options) {
466
496
  }
467
497
  try {
468
498
  const execute = async () => {
469
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
499
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
500
+ if (factError)
501
+ return {
502
+ ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
503
+ result: null, warnings: []
504
+ };
470
505
  if (!boundFact)
471
506
  return {
472
507
  ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
@@ -474,14 +509,9 @@ async function syncPullRequestSummary(taskRef, options) {
474
509
  };
475
510
  const prIdentity = boundFact.identity.resource;
476
511
  const boundPrNumber = resourceIdentityNumber(prIdentity);
477
- if (!boundPrNumber)
478
- return {
479
- ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false } }),
480
- result: null, warnings: []
481
- };
482
512
  knownPrNumber = boundPrNumber;
483
513
  const prNumber = boundPrNumber;
484
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
514
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
485
515
  if (!initial.ok)
486
516
  return fail(initial.status, context, initial.error);
487
517
  const manual = options.manualValidation;
@@ -491,6 +521,8 @@ async function syncPullRequestSummary(taskRef, options) {
491
521
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
492
522
  return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
493
523
  if (manual) {
524
+ if (prNumber === null)
525
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
494
526
  if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
495
527
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
496
528
  if (manual.phase === 'final') {
@@ -570,7 +602,7 @@ async function syncPullRequestSummary(taskRef, options) {
570
602
  ? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
571
603
  : null;
572
604
  const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route };
573
- const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
605
+ const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
574
606
  if (!beforeWrite.ok)
575
607
  return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
576
608
  if (!sameIdentity(initial.value, beforeWrite.value))
@@ -605,7 +637,7 @@ async function syncPullRequestSummary(taskRef, options) {
605
637
  : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
606
638
  if (!written.ok)
607
639
  return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
608
- const after = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
640
+ const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
609
641
  const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
610
642
  const compensateWrittenSummary = async () => reconciliation.action === 'create'
611
643
  ? written.value.remoteId && loaded.value.provider.comments?.delete
@@ -252,7 +252,7 @@ async function resolvedContext(taskRef, options) {
252
252
  const fact = factRead.status === 'valid' ? factRead.fact : null;
253
253
  let issueIdentity;
254
254
  try {
255
- issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
255
+ issueIdentity = taskIssueIdentity(frontmatter);
256
256
  }
257
257
  catch (error) {
258
258
  return { ok: false, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
@@ -776,7 +776,7 @@ async function resolveExternalPullRequest(taskRef, options) {
776
776
  return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
777
777
  platform: base.context.platform,
778
778
  capabilities: base.context.capabilities,
779
- error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
779
+ error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid platform issue identity', retryable: false }
780
780
  }));
781
781
  const inspected = base.provider.changeRequests?.listClosing
782
782
  ? await base.provider.changeRequests.listClosing({
@@ -1285,7 +1285,7 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
1285
1285
  }
1286
1286
  let issueIdentity;
1287
1287
  try {
1288
- issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
1288
+ issueIdentity = taskIssueIdentity(frontmatter);
1289
1289
  }
1290
1290
  catch (error) {
1291
1291
  return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } });
@@ -0,0 +1,70 @@
1
+ import { createHash } from 'node:crypto';
2
+ const SNAPSHOT_FRONTMATTER = new Set([
3
+ 'id', 'type', 'branch', 'workflow', 'status', 'created_at', 'updated_at',
4
+ 'priority', 'effort', 'start_date', 'target_date', 'current_step', 'assigned_to',
5
+ 'platform_issue_identity', 'delivery_remote', 'delivery_base_ref', 'pr_delivery_fact'
6
+ ]);
7
+ const PROCESS_SECTION_ALIASES = new Set([
8
+ '活动日志', 'activity log',
9
+ '产物生命周期收据', 'artifact lifecycle receipts',
10
+ '审查分歧账本', 'review dispute ledger', 'review disagreement ledger',
11
+ '人工裁决', '人工裁决待办', 'human decisions', 'human decision pending', 'human decisions pending',
12
+ '实现输入', 'implementation input', 'implementation inputs',
13
+ '工作流告警', 'workflow warning', 'workflow warnings', 'warnings',
14
+ '返工意图', 'rework intent', 'rework intents',
15
+ '产物失效记录', 'artifact invalidation', 'artifact invalidation records',
16
+ '实现备注', 'implementation notes',
17
+ '审查反馈', 'review feedback'
18
+ ]);
19
+ function normalize(content) {
20
+ return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
21
+ }
22
+ function splitFrontmatter(content) {
23
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
24
+ if (!match)
25
+ throw new Error('task comment projection requires task frontmatter');
26
+ return { frontmatter: match[1], body: content.slice(match[0].length) };
27
+ }
28
+ function projectFrontmatter(frontmatter) {
29
+ const lines = frontmatter.replace(/\r\n/g, '\n').split('\n');
30
+ const selected = lines.filter((line) => {
31
+ if (!line.trim() || line.trimStart().startsWith('#'))
32
+ return false;
33
+ const match = /^([^:\s][^:]*):/.exec(line);
34
+ return Boolean(match && SNAPSHOT_FRONTMATTER.has(match[1].trim()));
35
+ });
36
+ if (!selected.some((line) => /^id:/.test(line)))
37
+ throw new Error('task comment projection requires frontmatter id');
38
+ return selected.join('\n');
39
+ }
40
+ function projectBody(body) {
41
+ const lines = body.replace(/\r\n/g, '\n').split('\n');
42
+ const kept = [];
43
+ let excludedLevel = null;
44
+ for (const line of lines) {
45
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
46
+ if (heading) {
47
+ const level = heading[1].length;
48
+ if (excludedLevel !== null && level <= excludedLevel)
49
+ excludedLevel = null;
50
+ if (PROCESS_SECTION_ALIASES.has(heading[2].trim().toLowerCase())) {
51
+ excludedLevel = level;
52
+ continue;
53
+ }
54
+ }
55
+ if (excludedLevel === null)
56
+ kept.push(line);
57
+ }
58
+ return kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
59
+ }
60
+ function projectTaskComment(content) {
61
+ const split = splitFrontmatter(content);
62
+ const projected = normalize(`---\n${projectFrontmatter(split.frontmatter)}\n---\n\n${projectBody(split.body)}`);
63
+ return {
64
+ content: projected,
65
+ byteLength: Buffer.byteLength(projected, 'utf8'),
66
+ sha256: createHash('sha256').update(projected, 'utf8').digest('hex')
67
+ };
68
+ }
69
+ export { projectTaskComment };
70
+ //# sourceMappingURL=task-comment-projection.js.map
@@ -1,7 +1,6 @@
1
1
  import { parseResourceIdentity } from './resource-identity.js';
2
- import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.js';
3
2
  import { parse as parseYaml } from 'yaml';
4
- function taskIssueIdentity(frontmatter, _declaration, runtimeVersion = VERSION) {
3
+ function taskIssueIdentity(frontmatter) {
5
4
  const serialized = frontmatter.platform_issue_identity;
6
5
  if (typeof serialized === 'string' && serialized.trim()) {
7
6
  try {
@@ -12,13 +11,7 @@ function taskIssueIdentity(frontmatter, _declaration, runtimeVersion = VERSION)
12
11
  return null;
13
12
  }
14
13
  }
15
- const legacy = frontmatter.issue_number;
16
- const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
17
- // TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
18
- if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
19
- throw legacyCompatibilityError('issue_number', runtimeVersion);
20
- }
21
- return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
14
+ return null;
22
15
  }
23
16
  function taskIssueIdentityError(error) {
24
17
  const value = error && typeof error === 'object' ? error : {};
@@ -0,0 +1,55 @@
1
+ import { parseActivityRecoveryMetadata } from './activity-recovery.js';
2
+ function normalized(body) {
3
+ return body.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
4
+ }
5
+ function decodeRenderedText(value) {
6
+ return value
7
+ .replace(/&lt;/g, '<')
8
+ .replace(/&gt;/g, '>')
9
+ .replace(/&quot;/g, '"')
10
+ .replace(/&#39;/g, "'")
11
+ .replace(/&amp;/g, '&');
12
+ }
13
+ function taskSnapshot(taskId, body) {
14
+ const marker = `<!-- sync-issue:${taskId}:task -->`;
15
+ if (normalized(body).split('\n', 1)[0] !== marker)
16
+ throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot marker is invalid');
17
+ const frontmatter = body.match(/<details><summary>[^<]*<\/summary>\n\n```yaml\n---\n([\s\S]*?)\n---\n```\n\n<\/details>/);
18
+ if (!frontmatter)
19
+ throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot frontmatter is invalid');
20
+ const afterDetails = (frontmatter.index ?? 0) + frontmatter[0].length;
21
+ const footer = body.lastIndexOf('\n\n---\n*由 ');
22
+ if (footer <= afterDetails)
23
+ throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot envelope is invalid');
24
+ const projectedBody = body.slice(afterDetails, footer).replace(/^\n+|\n+$/g, '');
25
+ return `---\n${decodeRenderedText(frontmatter[1])}\n---\n\n${decodeRenderedText(projectedBody)}\n`;
26
+ }
27
+ function activityLine(entry) {
28
+ return `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
29
+ }
30
+ function recoverTaskFromComments(input) {
31
+ const marker = `<!-- sync-issue:${input.taskId}:task -->`;
32
+ const snapshots = input.comments.filter((comment) => normalized(comment.body).split('\n', 1)[0] === marker);
33
+ if (snapshots.length !== 1)
34
+ throw new Error('RECOVERY_EVIDENCE_MISSING: exactly one task snapshot comment is required');
35
+ const author = snapshots[0].user?.login;
36
+ if (!author)
37
+ throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot author is unavailable');
38
+ const content = taskSnapshot(input.taskId, normalized(snapshots[0].body));
39
+ const entries = new Map();
40
+ for (const comment of input.comments) {
41
+ if (comment.user?.login !== author)
42
+ continue;
43
+ const record = parseActivityRecoveryMetadata(comment.body);
44
+ if (!record)
45
+ continue;
46
+ if (record.taskId !== input.taskId)
47
+ throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata task identity is invalid');
48
+ for (const entry of record.entries)
49
+ entries.set(JSON.stringify(entry), entry);
50
+ }
51
+ const restored = [...entries.values()].sort((left, right) => left.time.localeCompare(right.time));
52
+ return `${content.replace(/\n+$/, '')}\n\n## 活动日志\n\n${restored.map(activityLine).join('\n')}\n`;
53
+ }
54
+ export { recoverTaskFromComments };
55
+ //# sourceMappingURL=task-recovery.js.map
@@ -10,6 +10,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.js";
10
10
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.js";
11
11
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.js";
12
12
  import { taskIssueIdentity } from "./task-identities.js";
13
+ import { renderTaskCommentResult } from "./issue-comments.js";
13
14
  import { CONTROL_MARKER_PATTERN, canonicalizeCommentBody, renderSafeCodeFence, sanitizeMarkdownDocument } from "./comment-safety.js";
14
15
  const CHECK_TYPE = "platform-sync";
15
16
  const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
@@ -96,8 +97,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
96
97
  }
97
98
  const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
98
99
  const prNumber = resourceIdentityNumber(prIdentity);
99
- if (config.when === "issue_number_exists" && !issueIdentity) {
100
- return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
100
+ if (config.when === "platform_issue_identity_exists" && !issueIdentity) {
101
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no platform_issue_identity") };
101
102
  }
102
103
  if (config.when === "pr_fact_bound" && !prIdentity) {
103
104
  return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
@@ -432,11 +433,17 @@ function checkTaskCommentContent(context, remoteData, shared) {
432
433
  if (!comment) {
433
434
  return shared.failResult(CHECK_TYPE, `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`, "check_failed");
434
435
  }
435
- const expectedTaskBody = buildExpectedTaskBody(context.task.content, shared);
436
- if (expectedTaskBody === null) {
436
+ let renderedTask;
437
+ try {
438
+ renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
439
+ }
440
+ catch {
437
441
  return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
438
442
  }
439
- const expectedBody = shared.normalizeContent(expectedTaskBody);
443
+ if (renderedTask.byteLength > 60_000) {
444
+ return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
445
+ }
446
+ const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
440
447
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
441
448
  if (expectedBody === commentBody) {
442
449
  return null;
@@ -608,12 +615,21 @@ function extractCommentBody(commentBody) {
608
615
  while (start < lines.length && lines[start].trim() === "") {
609
616
  start += 1;
610
617
  }
611
- if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
618
+ if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start].trim()) || /^> 任务同步 · .+$/.test(lines[start].trim()))) {
612
619
  start += 1;
613
620
  }
614
621
  while (start < lines.length && lines[start].trim() === "") {
615
622
  start += 1;
616
623
  }
624
+ if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
625
+ const metadataEnd = lines.indexOf("</details>", start + 1);
626
+ if (metadataEnd >= 0) {
627
+ start = metadataEnd + 1;
628
+ while (start < lines.length && lines[start].trim() === "") {
629
+ start += 1;
630
+ }
631
+ }
632
+ }
617
633
  let end = lines.length;
618
634
  for (let index = lines.length - 1; index >= start; index -= 1) {
619
635
  const trimmed = lines[index].trim();
@@ -785,7 +801,9 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
785
801
  if (!baseRef) {
786
802
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
787
803
  }
788
- const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], taskDir);
804
+ const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
805
+ const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
806
+ const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
789
807
  if (!changedFilesResult.ok) {
790
808
  return { ...changedFilesResult, type: "network_error" };
791
809
  }
@@ -808,6 +826,26 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
808
826
  }
809
827
  return { ok: true, labels: planned.target, mode: "mapped" };
810
828
  }
829
+ function worktreeForBranch(repositoryRoot, branch) {
830
+ try {
831
+ const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
832
+ cwd: repositoryRoot,
833
+ encoding: "utf8",
834
+ stdio: ["pipe", "pipe", "pipe"]
835
+ }).trim().split(/\r?\n\r?\n/);
836
+ const expectedRef = `refs/heads/${branch}`;
837
+ for (const record of records) {
838
+ const worktree = /^worktree (.+)$/m.exec(record)?.[1];
839
+ const branchRef = /^branch (.+)$/m.exec(record)?.[1];
840
+ if (worktree && branchRef === expectedRef)
841
+ return worktree;
842
+ }
843
+ }
844
+ catch {
845
+ // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
846
+ }
847
+ return null;
848
+ }
811
849
  function loadInLabelMapping(shared) {
812
850
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
813
851
  if (!fs.existsSync(configPath)) {
@@ -12,10 +12,6 @@ export class HostResolutionError extends Error {
12
12
  this.code = code;
13
13
  }
14
14
  }
15
- function toPositiveNumber(value) {
16
- const n = Number(value);
17
- return Number.isInteger(n) && n > 0 ? n : null;
18
- }
19
15
  /**
20
16
  * Parse the PR body for closing-issue references. Handles the conventional
21
17
  * `Closes #1` / `Fixes: #2, #3` shapes (case-insensitive, comma/space
@@ -41,7 +37,7 @@ export function extractClosingIssueNumbers(body) {
41
37
  /**
42
38
  * Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
43
39
  * PR number or to one of the PR's closing issues. A task is emitted at most once;
44
- * a verified fact identity hit takes priority over the issue-number reverse lookup.
40
+ * a verified fact identity hit takes priority over the issue-identity reverse lookup.
45
41
  */
46
42
  export function collectHostCandidates(input) {
47
43
  const activeRoot = path.join(input.workspaceRoot, '.agents', 'workspace', 'active');
@@ -73,7 +69,7 @@ export function collectHostCandidates(input) {
73
69
  continue;
74
70
  }
75
71
  const issueIdentity = taskIssueIdentity(frontmatter);
76
- const issueNumber = resourceIdentityNumber(issueIdentity) ?? toPositiveNumber(frontmatter.issue_number);
72
+ const issueNumber = resourceIdentityNumber(issueIdentity);
77
73
  const fact = readPrDeliveryFact(frontmatter);
78
74
  const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
79
75
  const prNumber = resourceIdentityNumber(prIdentity);
@@ -133,8 +129,8 @@ function isTrusted(head) {
133
129
  /**
134
130
  * Derive the S1(a) trust factor `taskIssueMatches` (PL-3) mechanically from
135
131
  * resolved data: a unique host is trusted on the issue dimension only when the
136
- * task's bound issue_number is among the PR's closing issues. Ambiguous/none
137
- * hosts and tasks without a positive issue_number cannot be confirmed -> false
132
+ * task's bound issue identity is among the PR's closing issues. Ambiguous/none
133
+ * hosts and tasks without a numeric issue identity cannot be confirmed -> false
138
134
  * (fail closed to S2/audit). `resolve-host` emits this value so the prompt layer
139
135
  * does not derive the trust factor ad hoc (CD-2).
140
136
  */
@@ -2,11 +2,27 @@ import { parseArgs } from 'node:util';
2
2
  import { loadConfig } from '../config.js';
3
3
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
4
4
  import { resolveSandboxCleanupTarget } from '../workspace-identity.js';
5
+ import { detectEngine } from '../engine.js';
6
+ import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel } from '../constants.js';
7
+ import { fetchSandboxRows } from './list-running.js';
5
8
  import { rmOne, rmPurge, rmUnbound } from '../removal.js';
6
9
  const USAGE = `Usage:
7
10
  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
8
- ai sandbox rm --unbound [--dry-run] [--yes] Remove completed task-bound and branch-only sandboxes and verified auxiliary state; active, blocked, and archive tasks are protected
11
+ ai sandbox rm --unbound [--dry-run] [--yes] Remove discovered task-bound and branch-only sandboxes
9
12
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
13
+ function resolveMissingTaskSandboxBranch(taskId, config) {
14
+ const engine = detectEngine(config);
15
+ const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
16
+ const branches = new Set([...listed.running, ...listed.nonRunning]
17
+ .filter((row) => row.workspaceMode === 'task-bound' && row.taskId === taskId && row.branch)
18
+ .map((row) => row.branch));
19
+ if (branches.size === 0)
20
+ return null;
21
+ if (branches.size > 1) {
22
+ throw new Error(`SANDBOX_CLEANUP_TASK_ID_AMBIGUOUS: task ${taskId} matches multiple sandbox branches`);
23
+ }
24
+ return branches.values().next().value ?? null;
25
+ }
10
26
  export async function rm(args) {
11
27
  const { values, positionals } = parseArgs({
12
28
  args,
@@ -53,7 +69,10 @@ export async function rm(args) {
53
69
  });
54
70
  return;
55
71
  }
56
- const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot);
72
+ const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot, {
73
+ allowProtected: true,
74
+ resolveMissingTask: (taskId) => resolveMissingTaskSandboxBranch(taskId, config)
75
+ });
57
76
  await rmOne(config, tools, cleanupTarget.branch, { cleanupTarget });
58
77
  }
59
78
  //# sourceMappingURL=rm.js.map
@@ -125,7 +125,9 @@ export async function prepareSandboxControlExecution(params) {
125
125
  owner: gateOwner,
126
126
  requestId: params.request.id,
127
127
  operationId: authority?.operationId ?? params.request.id,
128
- phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
128
+ phase: authority?.phase ?? (params.request.family === 'task-workflow'
129
+ && params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
130
+ : params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
129
131
  authority
130
132
  });
131
133
  },
@@ -163,7 +165,7 @@ function waitForGate(nonce, timeoutMs = 2_000) {
163
165
  return;
164
166
  }
165
167
  if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
166
- || !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase)
168
+ || !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase)
167
169
  || (value.authority !== null && value.authority !== undefined && (() => {
168
170
  try {
169
171
  validateLifecycleRecoveryAttestation(value.authority);