@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
@@ -0,0 +1,79 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const SNAPSHOT_FRONTMATTER = new Set([
4
+ 'id', 'type', 'branch', 'workflow', 'status', 'created_at', 'updated_at',
5
+ 'priority', 'effort', 'start_date', 'target_date', 'current_step', 'assigned_to',
6
+ 'platform_issue_identity', 'delivery_remote', 'delivery_base_ref', 'pr_delivery_fact'
7
+ ]);
8
+
9
+ type TaskCommentProjection = {
10
+ content: string;
11
+ byteLength: number;
12
+ sha256: string;
13
+ };
14
+
15
+ const PROCESS_SECTION_ALIASES = new Set([
16
+ '活动日志', 'activity log',
17
+ '产物生命周期收据', 'artifact lifecycle receipts',
18
+ '审查分歧账本', 'review dispute ledger', 'review disagreement ledger',
19
+ '人工裁决', '人工裁决待办', 'human decisions', 'human decision pending', 'human decisions pending',
20
+ '实现输入', 'implementation input', 'implementation inputs',
21
+ '工作流告警', 'workflow warning', 'workflow warnings', 'warnings',
22
+ '返工意图', 'rework intent', 'rework intents',
23
+ '产物失效记录', 'artifact invalidation', 'artifact invalidation records',
24
+ '实现备注', 'implementation notes',
25
+ '审查反馈', 'review feedback'
26
+ ]);
27
+
28
+ function normalize(content: string): string {
29
+ return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
30
+ }
31
+
32
+ function splitFrontmatter(content: string): { frontmatter: string; body: string } {
33
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
34
+ if (!match) throw new Error('task comment projection requires task frontmatter');
35
+ return { frontmatter: match[1]!, body: content.slice(match[0].length) };
36
+ }
37
+
38
+ function projectFrontmatter(frontmatter: string): string {
39
+ const lines = frontmatter.replace(/\r\n/g, '\n').split('\n');
40
+ const selected = lines.filter((line) => {
41
+ if (!line.trim() || line.trimStart().startsWith('#')) return false;
42
+ const match = /^([^:\s][^:]*):/.exec(line);
43
+ return Boolean(match && SNAPSHOT_FRONTMATTER.has(match[1]!.trim()));
44
+ });
45
+ if (!selected.some((line) => /^id:/.test(line))) throw new Error('task comment projection requires frontmatter id');
46
+ return selected.join('\n');
47
+ }
48
+
49
+ function projectBody(body: string): string {
50
+ const lines = body.replace(/\r\n/g, '\n').split('\n');
51
+ const kept: string[] = [];
52
+ let excludedLevel: number | null = null;
53
+ for (const line of lines) {
54
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
55
+ if (heading) {
56
+ const level = heading[1]!.length;
57
+ if (excludedLevel !== null && level <= excludedLevel) excludedLevel = null;
58
+ if (PROCESS_SECTION_ALIASES.has(heading[2]!.trim().toLowerCase())) {
59
+ excludedLevel = level;
60
+ continue;
61
+ }
62
+ }
63
+ if (excludedLevel === null) kept.push(line);
64
+ }
65
+ return kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
66
+ }
67
+
68
+ function projectTaskComment(content: string): TaskCommentProjection {
69
+ const split = splitFrontmatter(content);
70
+ const projected = normalize(`---\n${projectFrontmatter(split.frontmatter)}\n---\n\n${projectBody(split.body)}`);
71
+ return {
72
+ content: projected,
73
+ byteLength: Buffer.byteLength(projected, 'utf8'),
74
+ sha256: createHash('sha256').update(projected, 'utf8').digest('hex')
75
+ };
76
+ }
77
+
78
+ export { projectTaskComment };
79
+ export type { TaskCommentProjection };
@@ -1,13 +1,8 @@
1
- import type { ProviderIdentityDeclaration, ResourceIdentity } from './resource-identity.ts';
1
+ import type { ResourceIdentity } from './resource-identity.ts';
2
2
  import { parseResourceIdentity } from './resource-identity.ts';
3
- import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
4
3
  import { parse as parseYaml } from 'yaml';
5
4
 
6
- function taskIssueIdentity(
7
- frontmatter: Record<string, string | number | boolean | null>,
8
- _declaration?: ProviderIdentityDeclaration,
9
- runtimeVersion = VERSION
10
- ): ResourceIdentity | null {
5
+ function taskIssueIdentity(frontmatter: Record<string, string | number | boolean | null>): ResourceIdentity | null {
11
6
  const serialized = frontmatter.platform_issue_identity;
12
7
  if (typeof serialized === 'string' && serialized.trim()) {
13
8
  try {
@@ -20,13 +15,7 @@ function taskIssueIdentity(
20
15
  return null;
21
16
  }
22
17
  }
23
- const legacy = frontmatter.issue_number;
24
- const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
25
- // TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
26
- if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
27
- throw legacyCompatibilityError('issue_number', runtimeVersion);
28
- }
29
- return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
18
+ return null;
30
19
  }
31
20
 
32
21
  function taskIssueIdentityError(error: unknown): { code: string; message: string } {
@@ -0,0 +1,54 @@
1
+ import { parseActivityRecoveryMetadata } from './activity-recovery.ts';
2
+
3
+ type RecoveryRemoteComment = { body: string; user?: { login?: string } };
4
+
5
+ function normalized(body: string): string {
6
+ return body.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
7
+ }
8
+
9
+ function decodeRenderedText(value: string): string {
10
+ return value
11
+ .replace(/&lt;/g, '<')
12
+ .replace(/&gt;/g, '>')
13
+ .replace(/&quot;/g, '"')
14
+ .replace(/&#39;/g, "'")
15
+ .replace(/&amp;/g, '&');
16
+ }
17
+
18
+ function taskSnapshot(taskId: string, body: string): string {
19
+ const marker = `<!-- sync-issue:${taskId}:task -->`;
20
+ if (normalized(body).split('\n', 1)[0] !== marker) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot marker is invalid');
21
+ const frontmatter = body.match(/<details><summary>[^<]*<\/summary>\n\n```yaml\n---\n([\s\S]*?)\n---\n```\n\n<\/details>/);
22
+ if (!frontmatter) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot frontmatter is invalid');
23
+ const afterDetails = (frontmatter.index ?? 0) + frontmatter[0].length;
24
+ const footer = body.lastIndexOf('\n\n---\n*由 ');
25
+ if (footer <= afterDetails) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot envelope is invalid');
26
+ const projectedBody = body.slice(afterDetails, footer).replace(/^\n+|\n+$/g, '');
27
+ return `---\n${decodeRenderedText(frontmatter[1]!)}\n---\n\n${decodeRenderedText(projectedBody)}\n`;
28
+ }
29
+
30
+ function activityLine(entry: { time: string; step: string; agent: string; note: string }): string {
31
+ return `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
32
+ }
33
+
34
+ function recoverTaskFromComments(input: { taskId: string; comments: readonly RecoveryRemoteComment[] }): string {
35
+ const marker = `<!-- sync-issue:${input.taskId}:task -->`;
36
+ const snapshots = input.comments.filter((comment) => normalized(comment.body).split('\n', 1)[0] === marker);
37
+ if (snapshots.length !== 1) throw new Error('RECOVERY_EVIDENCE_MISSING: exactly one task snapshot comment is required');
38
+ const author = snapshots[0]!.user?.login;
39
+ if (!author) throw new Error('RECOVERY_EVIDENCE_MISSING: task snapshot author is unavailable');
40
+ const content = taskSnapshot(input.taskId, normalized(snapshots[0]!.body));
41
+ const entries = new Map<string, { time: string; step: string; agent: string; note: string }>();
42
+ for (const comment of input.comments) {
43
+ if (comment.user?.login !== author) continue;
44
+ const record = parseActivityRecoveryMetadata(comment.body);
45
+ if (!record) continue;
46
+ if (record.taskId !== input.taskId) throw new Error('RECOVERY_EVIDENCE_MISSING: activity metadata task identity is invalid');
47
+ for (const entry of record.entries) entries.set(JSON.stringify(entry), entry);
48
+ }
49
+ const restored = [...entries.values()].sort((left, right) => left.time.localeCompare(right.time));
50
+ return `${content.replace(/\n+$/, '')}\n\n## 活动日志\n\n${restored.map(activityLine).join('\n')}\n`;
51
+ }
52
+
53
+ export { recoverTaskFromComments };
54
+ export type { RecoveryRemoteComment };
@@ -12,6 +12,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
12
12
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
13
13
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
14
14
  import { taskIssueIdentity } from "./task-identities.ts";
15
+ import { renderTaskCommentResult } from "./issue-comments.ts";
15
16
  import {
16
17
  CONTROL_MARKER_PATTERN,
17
18
  canonicalizeCommentBody,
@@ -113,8 +114,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
113
114
  }
114
115
  const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
115
116
  const prNumber = resourceIdentityNumber(prIdentity);
116
- if (config.when === "issue_number_exists" && !issueIdentity) {
117
- return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
117
+ if (config.when === "platform_issue_identity_exists" && !issueIdentity) {
118
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no platform_issue_identity") };
118
119
  }
119
120
  if (config.when === "pr_fact_bound" && !prIdentity) {
120
121
  return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
@@ -541,7 +542,6 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
541
542
  if (!context.config.verify_task_comment_content) {
542
543
  return null;
543
544
  }
544
-
545
545
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
546
546
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
547
547
  if (!comment) {
@@ -551,14 +551,20 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
551
551
  );
552
552
  }
553
553
 
554
- const expectedTaskBody = buildExpectedTaskBody(context.task.content, shared);
555
- if (expectedTaskBody === null) {
556
- return shared.failResult(CHECK_TYPE,
557
- "Task content cannot be rendered safely for comment verification",
558
- "check_failed"
554
+ let renderedTask: { body: string; byteLength: number };
555
+ try {
556
+ renderedTask = renderTaskCommentResult(
557
+ context.task.content,
558
+ context.task.metadata.id,
559
+ loadProjectLanguage(shared)
559
560
  );
561
+ } catch {
562
+ return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
560
563
  }
561
- const expectedBody = shared.normalizeContent(expectedTaskBody);
564
+ if (renderedTask.byteLength > 60_000) {
565
+ return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
566
+ }
567
+ const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
562
568
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
563
569
 
564
570
  if (expectedBody === commentBody) {
@@ -829,7 +835,7 @@ function extractCommentBody(commentBody: any): any {
829
835
  start += 1;
830
836
  }
831
837
 
832
- if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start]!.trim())) {
838
+ if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start]!.trim()) || /^> 任务同步 · .+$/.test(lines[start]!.trim()))) {
833
839
  start += 1;
834
840
  }
835
841
 
@@ -837,6 +843,16 @@ function extractCommentBody(commentBody: any): any {
837
843
  start += 1;
838
844
  }
839
845
 
846
+ if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
847
+ const metadataEnd = lines.indexOf("</details>", start + 1);
848
+ if (metadataEnd >= 0) {
849
+ start = metadataEnd + 1;
850
+ while (start < lines.length && lines[start]!.trim() === "") {
851
+ start += 1;
852
+ }
853
+ }
854
+ }
855
+
840
856
  let end = lines.length;
841
857
  for (let index = lines.length - 1; index >= start; index -= 1) {
842
858
  const trimmed = lines[index]!.trim();
@@ -1038,7 +1054,9 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1038
1054
  if (!baseRef) {
1039
1055
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
1040
1056
  }
1041
- const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], taskDir);
1057
+ const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
1058
+ const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
1059
+ const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
1042
1060
  if (!changedFilesResult.ok) {
1043
1061
  return { ...changedFilesResult, type: "network_error" };
1044
1062
  }
@@ -1065,6 +1083,25 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1065
1083
  return { ok: true, labels: planned.target, mode: "mapped" };
1066
1084
  }
1067
1085
 
1086
+ function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
1087
+ try {
1088
+ const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
1089
+ cwd: repositoryRoot,
1090
+ encoding: "utf8",
1091
+ stdio: ["pipe", "pipe", "pipe"]
1092
+ }).trim().split(/\r?\n\r?\n/);
1093
+ const expectedRef = `refs/heads/${branch}`;
1094
+ for (const record of records) {
1095
+ const worktree = /^worktree (.+)$/m.exec(record)?.[1];
1096
+ const branchRef = /^branch (.+)$/m.exec(record)?.[1];
1097
+ if (worktree && branchRef === expectedRef) return worktree;
1098
+ }
1099
+ } catch {
1100
+ // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
1101
+ }
1102
+ return null;
1103
+ }
1104
+
1068
1105
  function loadInLabelMapping(shared: VerificationShared): any {
1069
1106
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
1070
1107
  if (!fs.existsSync(configPath)) {
@@ -37,11 +37,6 @@ export class HostResolutionError extends Error {
37
37
  }
38
38
  }
39
39
 
40
- function toPositiveNumber(value: unknown): number | null {
41
- const n = Number(value);
42
- return Number.isInteger(n) && n > 0 ? n : null;
43
- }
44
-
45
40
  /**
46
41
  * Parse the PR body for closing-issue references. Handles the conventional
47
42
  * `Closes #1` / `Fixes: #2, #3` shapes (case-insensitive, comma/space
@@ -68,7 +63,7 @@ export function extractClosingIssueNumbers(body: string): number[] {
68
63
  /**
69
64
  * Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
70
65
  * PR number or to one of the PR's closing issues. A task is emitted at most once;
71
- * a verified fact identity hit takes priority over the issue-number reverse lookup.
66
+ * a verified fact identity hit takes priority over the issue-identity reverse lookup.
72
67
  */
73
68
  export function collectHostCandidates(input: {
74
69
  prNumber?: number;
@@ -97,7 +92,7 @@ export function collectHostCandidates(input: {
97
92
  let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
98
93
  try { frontmatter = parseTypedTaskFrontmatter(content); } catch { continue; }
99
94
  const issueIdentity = taskIssueIdentity(frontmatter);
100
- const issueNumber = resourceIdentityNumber(issueIdentity) ?? toPositiveNumber(frontmatter.issue_number);
95
+ const issueNumber = resourceIdentityNumber(issueIdentity);
101
96
  const fact = readPrDeliveryFact(frontmatter);
102
97
  const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
103
98
  const prNumber = resourceIdentityNumber(prIdentity);
@@ -209,8 +204,8 @@ function isTrusted(head: HeadState): boolean {
209
204
  /**
210
205
  * Derive the S1(a) trust factor `taskIssueMatches` (PL-3) mechanically from
211
206
  * resolved data: a unique host is trusted on the issue dimension only when the
212
- * task's bound issue_number is among the PR's closing issues. Ambiguous/none
213
- * hosts and tasks without a positive issue_number cannot be confirmed -> false
207
+ * task's bound issue identity is among the PR's closing issues. Ambiguous/none
208
+ * hosts and tasks without a numeric issue identity cannot be confirmed -> false
214
209
  * (fail closed to S2/audit). `resolve-host` emits this value so the prompt layer
215
210
  * does not derive the trust factor ad hoc (CD-2).
216
211
  */
@@ -2,12 +2,41 @@ import { parseArgs } from 'node:util';
2
2
  import { loadConfig } from '../config.ts';
3
3
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
4
4
  import { resolveSandboxCleanupTarget } from '../workspace-identity.ts';
5
+ import { detectEngine } from '../engine.ts';
6
+ import {
7
+ sandboxBranchLabel,
8
+ sandboxLabel,
9
+ sandboxTaskIdLabel,
10
+ sandboxWorkspaceModeLabel
11
+ } from '../constants.ts';
12
+ import { fetchSandboxRows } from './list-running.ts';
5
13
  import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
6
14
 
7
15
  const USAGE = `Usage:
8
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
9
- 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
17
+ ai sandbox rm --unbound [--dry-run] [--yes] Remove discovered task-bound and branch-only sandboxes
10
18
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
19
+
20
+ function resolveMissingTaskSandboxBranch(taskId: string, config: ReturnType<typeof loadConfig>): string | null {
21
+ const engine = detectEngine(config);
22
+ const listed = fetchSandboxRows(
23
+ engine,
24
+ sandboxLabel(config),
25
+ sandboxBranchLabel(config),
26
+ { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
27
+ );
28
+ const branches = new Set(
29
+ [...listed.running, ...listed.nonRunning]
30
+ .filter((row) => row.workspaceMode === 'task-bound' && row.taskId === taskId && row.branch)
31
+ .map((row) => row.branch)
32
+ );
33
+ if (branches.size === 0) return null;
34
+ if (branches.size > 1) {
35
+ throw new Error(`SANDBOX_CLEANUP_TASK_ID_AMBIGUOUS: task ${taskId} matches multiple sandbox branches`);
36
+ }
37
+ return branches.values().next().value ?? null;
38
+ }
39
+
11
40
  export async function rm(args: string[]): Promise<void> {
12
41
  const { values, positionals } = parseArgs({
13
42
  args,
@@ -64,6 +93,9 @@ export async function rm(args: string[]): Promise<void> {
64
93
  return;
65
94
  }
66
95
 
67
- const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot);
96
+ const cleanupTarget = resolveSandboxCleanupTarget(positionals[0] ?? '', config.repoRoot, {
97
+ allowProtected: true,
98
+ resolveMissingTask: (taskId) => resolveMissingTaskSandboxBranch(taskId, config)
99
+ });
68
100
  await rmOne(config, tools, cleanupTarget.branch, { cleanupTarget });
69
101
  }
@@ -188,7 +188,9 @@ export async function prepareSandboxControlExecution(params: {
188
188
  owner: gateOwner,
189
189
  requestId: params.request.id,
190
190
  operationId: authority?.operationId ?? params.request.id,
191
- phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
191
+ phase: authority?.phase ?? (params.request.family === 'task-workflow'
192
+ && params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
193
+ : params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
192
194
  authority
193
195
  } satisfies SandboxControlExecutorGateV2);
194
196
  },
@@ -235,7 +237,7 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
235
237
  return;
236
238
  }
237
239
  if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
238
- || !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
240
+ || !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
239
241
  || (value.authority !== null && value.authority !== undefined && (() => {
240
242
  try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
241
243
  })())) {
@@ -62,7 +62,7 @@ export type SandboxControlExecutorGateV2 = Readonly<{
62
62
  owner: SandboxControlBrokerOwner;
63
63
  requestId: string;
64
64
  operationId: string;
65
- phase: 'orchestration.prepare' | 'artifact.finalize-local' | 'task-event.completed';
65
+ phase: 'orchestration.prepare' | 'artifact.preflight' | 'artifact.finalize-local' | 'task-event.completed';
66
66
  authority: LifecycleRecoveryAttestationV1 | null;
67
67
  }>;
68
68
  type RequestBase = Readonly<{
@@ -131,6 +131,11 @@ export type SandboxControlResponse = Readonly<{
131
131
  outputState?: 'available' | 'unavailable';
132
132
  payload?: SandboxControlPayloadReference | null;
133
133
  }>;
134
+ export type SandboxControlRecoveryWarning = Readonly<{
135
+ code: string;
136
+ message: string;
137
+ action: string;
138
+ }>;
134
139
  export type SandboxControlStatus = Readonly<{
135
140
  version: 3; generation: string; broker: ProcessIdentity & { brokerId: string };
136
141
  state: 'starting' | 'healthy' | 'busy' | 'parked'; reasonCode: string | null;
@@ -12,6 +12,7 @@ import {
12
12
  SANDBOX_CONTROL_MAX_RESPONSE_BYTES,
13
13
  SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES,
14
14
  type SandboxControlManifest,
15
+ type SandboxControlRecoveryWarning,
15
16
  type SandboxControlRequest,
16
17
  type SandboxControlResponse,
17
18
  type SandboxControlTimingPolicy
@@ -62,6 +63,7 @@ import {
62
63
  SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES
63
64
  } from '../../task/control-recovery.ts';
64
65
  import { readRun } from '../../task/orchestration.ts';
66
+ import { readLifecycleRecoveryDomainEvidence } from '../../task/lifecycle-recovery.ts';
65
67
  import { captureRepositorySnapshot } from '../../task/workspace-snapshot.ts';
66
68
  import { parseTypedTaskFrontmatter } from '../../task/frontmatter.ts';
67
69
  import { readLifecycleJournalEvidence } from '../../task/lifecycle.ts';
@@ -368,11 +370,12 @@ function outputMatchesEvidence(output: string, bytes: number, sha256: string): b
368
370
  && createHash('sha256').update(output, 'utf8').digest('hex') === sha256;
369
371
  }
370
372
 
371
- function genericRecoveryResponse(
373
+ export function genericRecoveryResponse(
372
374
  request: SandboxControlRequest,
373
375
  exitCode: number,
374
376
  payload: ReturnType<typeof readSandboxControlPayload> | null,
375
- cause: 'publish' | 'recovery' = 'recovery'
377
+ cause: 'publish' | 'recovery' = 'recovery',
378
+ warning: SandboxControlRecoveryWarning | null = null
376
379
  ): SandboxControlResponse {
377
380
  const outputUnavailable = request.family === 'task-create' && !payload;
378
381
  return {
@@ -381,9 +384,11 @@ function genericRecoveryResponse(
381
384
  phase: 'completed',
382
385
  exitCode,
383
386
  stdout: outputUnavailable ? `${JSON.stringify(taskCreateOutputUnavailableResult(request.id))}\n` : '',
384
- stderr: payload ? '' : cause === 'publish'
385
- ? 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n'
386
- : 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n',
387
+ stderr: payload ? '' : warning
388
+ ? `${warning.code}: ${warning.message}\nAction: ${warning.action}\n`
389
+ : cause === 'publish'
390
+ ? 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n'
391
+ : 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n',
387
392
  error: null,
388
393
  outputState: payload ? 'available' : 'unavailable',
389
394
  payload: payload ? payloadReference(payload) : null
@@ -504,6 +509,21 @@ function readRecoveryDomain(
504
509
  }
505
510
 
506
511
  if (operation.family === 'task-lifecycle') {
512
+ if (operation.intent === 'recover-started') {
513
+ try {
514
+ if (!('args' in request)) return { domain: null };
515
+ const parsed = parseTaskControlOperation('task-lifecycle', request.args);
516
+ if (parsed.family !== 'task-lifecycle' || parsed.request.intent !== 'recover-started') {
517
+ return { domain: null };
518
+ }
519
+ return {
520
+ domain: readLifecycleRecoveryDomainEvidence(manifest.repoRoot, parsed.request, terminalResult),
521
+ journal: readLifecycleJournalEvidence(manifest.repoRoot, taskRef!)
522
+ };
523
+ } catch {
524
+ return { domain: null };
525
+ }
526
+ }
507
527
  if (!taskRef || !terminalResult.targetState) return { domain: null };
508
528
  const journal = readLifecycleJournalEvidence(manifest.repoRoot, taskRef);
509
529
  try {
@@ -577,7 +597,7 @@ function readCommittedCriticalPhases(manifest: SandboxControlManifest, requestId
577
597
  });
578
598
  }
579
599
 
580
- function recoveryResponse(
600
+ export function recoveryResponse(
581
601
  manifest: SandboxControlManifest,
582
602
  manifestPath: string,
583
603
  request: SandboxControlRequest,
@@ -637,7 +657,7 @@ function recoveryResponse(
637
657
  if (finalization?.status !== 'matched') return null;
638
658
  return finalization.response ?? null;
639
659
  }
640
- return genericRecoveryResponse(request, evidence.exitCode, payload);
660
+ return genericRecoveryResponse(request, evidence.exitCode, payload, 'recovery', terminalResult.warning ?? null);
641
661
  }
642
662
 
643
663
  function terminalMatchesEvidence(
@@ -681,7 +701,7 @@ function terminalMatchesEvidence(
681
701
  if (response.outputState === 'unavailable') {
682
702
  const causes = ['recovery', 'publish'] as const;
683
703
  const valid = causes.some((cause) => JSON.stringify(response)
684
- === JSON.stringify(genericRecoveryResponse(request, evidence.exitCode, null, cause)));
704
+ === JSON.stringify(genericRecoveryResponse(request, evidence.exitCode, null, cause, terminalResult?.warning ?? null)));
685
705
  return { valid, payloadReferenced: false };
686
706
  }
687
707
  return {
@@ -700,7 +720,7 @@ function publishExecutionResult(
700
720
  brokerOwns: () => boolean
701
721
  ): boolean {
702
722
  const normalized = sanitizeSandboxControlResult(manifest, result);
703
- writeSandboxControlTerminalResult(manifest, {
723
+ const terminalResult = writeSandboxControlTerminalResult(manifest, {
704
724
  id: request.id,
705
725
  family: request.family,
706
726
  operation: operationKey(request, normalized.stdout)
@@ -737,7 +757,7 @@ function publishExecutionResult(
737
757
  } catch {
738
758
  payload = null;
739
759
  }
740
- terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish');
760
+ terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish', terminalResult.warning ?? null);
741
761
  }
742
762
  }
743
763
  if (!brokerOwns()) return false;
@@ -1,5 +1,6 @@
1
1
  import { isCompletionEvidence, type CleanCompletionEvidence } from '../../task/orchestration.ts';
2
2
  import { parseControlOutput } from '../../task/control-recovery.ts';
3
+ import { isRecoveryWarning } from '../../task/recovery-warning.ts';
3
4
  import { execFileSync } from 'node:child_process';
4
5
  import fs from 'node:fs';
5
6
  import path from 'node:path';
@@ -14,6 +15,7 @@ import type {
14
15
  SandboxControlPayload,
15
16
  SandboxControlReservation,
16
17
  SandboxControlResultEvidence,
18
+ SandboxControlRecoveryWarning,
17
19
  SandboxControlStatus
18
20
  } from './protocol.ts';
19
21
  import {
@@ -113,6 +115,7 @@ export type SandboxControlTerminalResult = Readonly<{
113
115
  status: string;
114
116
  changed: boolean | null;
115
117
  completionEvidence: CleanCompletionEvidence | null;
118
+ warning?: SandboxControlRecoveryWarning | null;
116
119
  }>;
117
120
 
118
121
  export function terminalResultPath(manifest: SandboxControlManifest, requestId: string): string {
@@ -130,6 +133,7 @@ export function createSandboxControlTerminalResult(
130
133
  ? nested.run as Record<string, unknown> : null;
131
134
  const rawCompletion = nested.completionEvidence ?? run?.completionEvidence;
132
135
  const operation = request.operation ?? (typeof nested.intent === 'string' ? nested.intent : null) ?? '';
136
+ const warning = isRecoveryWarning(nested.warning) ? nested.warning : null;
133
137
  return {
134
138
  version: 1,
135
139
  requestId: request.id,
@@ -139,7 +143,8 @@ export function createSandboxControlTerminalResult(
139
143
  targetState: typeof nested.targetState === 'string' ? nested.targetState : null,
140
144
  status: typeof nested.status === 'string' ? nested.status : 'completed',
141
145
  changed: typeof nested.changed === 'boolean' ? nested.changed : null,
142
- completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
146
+ completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null,
147
+ ...(warning ? { warning } : {})
143
148
  };
144
149
  }
145
150
 
@@ -173,6 +178,9 @@ export function readSandboxControlTerminalResult(filePath: string): SandboxContr
173
178
  if (value.completionEvidence !== null && !isCompletionEvidence(value.completionEvidence)) {
174
179
  throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
175
180
  }
181
+ if (value.warning !== undefined && value.warning !== null && !isRecoveryWarning(value.warning)) {
182
+ throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
183
+ }
176
184
  if (value.changed === undefined) {
177
185
  throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
178
186
  }
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
- import { readStableFile, SecureFileError, writeAtomicFile } from '../../host-control/secure-fs.ts';
5
+ import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.ts';
6
6
 
7
7
  import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
8
8