@fitlab-ai/agent-infra 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) 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/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/manual-validation.js +6 -2
  7. package/dist/lib/internal/platform-comment.js +60 -2
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-finalization.js +2 -2
  10. package/dist/lib/internal/task-lifecycle.js +2 -2
  11. package/dist/lib/internal/task-operation-registry.js +22 -12
  12. package/dist/lib/internal/task-orchestration.js +2 -2
  13. package/dist/lib/internal/task-review.js +10 -5
  14. package/dist/lib/platform/activity-recovery.js +159 -0
  15. package/dist/lib/platform/issue-comments.js +84 -32
  16. package/dist/lib/platform/issues.js +3 -4
  17. package/dist/lib/platform/task-comment-projection.js +70 -0
  18. package/dist/lib/platform/task-recovery.js +55 -0
  19. package/dist/lib/platform/verification-sync.js +184 -162
  20. package/dist/lib/sandbox/control/executor.js +4 -2
  21. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  22. package/dist/lib/sandbox/control/workflow-executor.js +90 -22
  23. package/dist/lib/server/process-state.js +17 -5
  24. package/dist/lib/task/artifact-command.js +6 -3
  25. package/dist/lib/task/artifact-lifecycle.js +15 -52
  26. package/dist/lib/task/artifact-operations.js +28 -2
  27. package/dist/lib/task/artifact-recovery.js +114 -68
  28. package/dist/lib/task/artifact-repair-intent.js +25 -8
  29. package/dist/lib/task/capabilities.js +0 -2
  30. package/dist/lib/task/control-authority.js +1 -1
  31. package/dist/lib/task/events.js +66 -49
  32. package/dist/lib/task/gate-policy.js +94 -0
  33. package/dist/lib/task/intermediate-cleanup.js +28 -3
  34. package/dist/lib/task/local-artifact-finalization.js +85 -18
  35. package/dist/lib/task/orchestration.js +8 -6
  36. package/dist/lib/task/review-artifacts.js +12 -6
  37. package/dist/lib/task/review-command.js +2 -2
  38. package/dist/lib/task/review-finalization.js +198 -11
  39. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  40. package/dist/lib/task/verification-engine.js +28 -17
  41. package/dist/lib/task/verification.js +25 -9
  42. package/dist/lib/task/workflow-command.js +2 -0
  43. package/dist/lib/task/workflow-dispatch.js +161 -0
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/internal/cli-route-inventory.ts +3 -4
  46. package/lib/internal/manual-validation.ts +6 -2
  47. package/lib/internal/platform-comment.ts +53 -2
  48. package/lib/internal/task-artifact.ts +1 -1
  49. package/lib/internal/task-finalization.ts +1 -1
  50. package/lib/internal/task-lifecycle.ts +1 -1
  51. package/lib/internal/task-operation-registry.ts +22 -12
  52. package/lib/internal/task-orchestration.ts +1 -1
  53. package/lib/internal/task-review.ts +9 -5
  54. package/lib/platform/activity-recovery.ts +179 -0
  55. package/lib/platform/issue-comments.ts +98 -31
  56. package/lib/platform/issues.ts +3 -4
  57. package/lib/platform/task-comment-projection.ts +79 -0
  58. package/lib/platform/task-recovery.ts +54 -0
  59. package/lib/platform/verification-sync.ts +203 -188
  60. package/lib/sandbox/control/executor.ts +4 -2
  61. package/lib/sandbox/control/protocol.ts +1 -1
  62. package/lib/sandbox/control/task-workflow.ts +1 -1
  63. package/lib/sandbox/control/workflow-executor.ts +113 -22
  64. package/lib/server/process-state.ts +26 -5
  65. package/lib/task/artifact-command.ts +6 -4
  66. package/lib/task/artifact-lifecycle.ts +12 -49
  67. package/lib/task/artifact-operations.ts +28 -2
  68. package/lib/task/artifact-recovery.ts +120 -67
  69. package/lib/task/artifact-repair-intent.ts +38 -11
  70. package/lib/task/capabilities.ts +0 -1
  71. package/lib/task/control-authority.ts +1 -1
  72. package/lib/task/events.ts +55 -38
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/intermediate-cleanup.ts +18 -2
  75. package/lib/task/local-artifact-finalization.ts +91 -15
  76. package/lib/task/orchestration.ts +12 -7
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +1 -1
  79. package/lib/task/review-finalization.ts +198 -9
  80. package/lib/{host-control → task}/secure-fs.ts +10 -32
  81. package/lib/task/verification-engine.ts +24 -14
  82. package/lib/task/verification-types.ts +7 -0
  83. package/lib/task/verification.ts +25 -9
  84. package/lib/task/workflow-command.ts +2 -0
  85. package/lib/task/workflow-dispatch.ts +171 -0
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/issue-sync.en.md +11 -1
  88. package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
  89. package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
  90. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
  91. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  94. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  95. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  96. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  97. package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
  98. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
  99. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  100. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  101. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  102. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  104. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  105. package/templates/.agents/skills/commit/config/verify.json +1 -5
  106. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  108. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  109. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  110. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  111. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  112. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  116. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  117. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  118. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
  119. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  120. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  121. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  122. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  129. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  130. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  131. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  134. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  135. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  136. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  138. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  139. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  140. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  141. package/dist/lib/host-control/audit.js +0 -19
  142. package/dist/lib/host-control/client.js +0 -142
  143. package/dist/lib/host-control/command.js +0 -49
  144. package/dist/lib/host-control/path.js +0 -160
  145. package/dist/lib/host-control/server.js +0 -159
  146. package/dist/lib/host-control/service.js +0 -112
  147. package/dist/lib/internal/host-control.js +0 -28
  148. package/lib/host-control/audit.ts +0 -22
  149. package/lib/host-control/client.ts +0 -172
  150. package/lib/host-control/command.ts +0 -60
  151. package/lib/host-control/path.ts +0 -167
  152. package/lib/host-control/server.ts +0 -180
  153. package/lib/host-control/service.ts +0 -133
  154. package/lib/internal/host-control.ts +0 -28
@@ -1,4 +1,5 @@
1
1
  import type { VerificationShared } from "../task/verification-types.ts";
2
+ import { normalizeVerificationRecord, platformAuditPolicy, type PlatformAuditId } from "../task/gate-policy.ts";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
@@ -12,7 +13,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
12
13
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
13
14
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
14
15
  import { taskIssueIdentity } from "./task-identities.ts";
15
- import { isTaskCommentTooLarge } from "./issue-comments.ts";
16
+ import { renderTaskCommentResult } from "./issue-comments.ts";
16
17
  import {
17
18
  CONTROL_MARKER_PATTERN,
18
19
  canonicalizeCommentBody,
@@ -60,8 +61,23 @@ function sanitizeCommentContent(content: string): string | null {
60
61
  return result.ok ? result.value : null;
61
62
  }
62
63
 
63
- export async function check({ taskDir, config, artifactFile }: any, shared: VerificationShared): Promise<any> {
64
- const context = await buildSyncContext({ taskDir, config, artifactFile }, shared);
64
+ function infoResult(message: string): any {
65
+ return {
66
+ type: CHECK_TYPE,
67
+ status: 'pass',
68
+ message,
69
+ classification: 'info',
70
+ reason: 'NOT_APPLICABLE',
71
+ action: 'No action required'
72
+ };
73
+ }
74
+
75
+ function hardResult(result: any): any {
76
+ return { ...result, classification: 'hard' };
77
+ }
78
+
79
+ export async function check({ taskDir, config, artifactFile, skillName }: any, shared: VerificationShared): Promise<any> {
80
+ const context = await buildSyncContext({ taskDir, config, artifactFile, skillName }, shared);
65
81
  if (context.earlyReturn) {
66
82
  return context.earlyReturn;
67
83
  }
@@ -72,35 +88,36 @@ export async function check({ taskDir, config, artifactFile }: any, shared: Veri
72
88
  }
73
89
 
74
90
  const subChecks = [
75
- checkClosedIssueStatusLabels,
76
- checkStatusLabel,
77
- checkCommentMarker,
78
- checkPrCommentMarker,
79
- checkPrCommentLastCommit,
80
- checkPrCommentRequiredPatterns,
81
- checkCommentContent,
82
- checkTaskCommentContent,
83
- checkInLabelsComputed,
84
- checkPrTypeLabel,
85
- checkInLabelsMatchPr,
86
- checkPrAssignee,
87
- checkSyncedRequirements,
88
- checkIssueType,
89
- checkIssueFields,
90
- checkMilestone
91
- ];
92
-
93
- for (const subCheck of subChecks) {
94
- const result = subCheck(context, remoteData, shared);
95
- if (result) {
96
- return result;
97
- }
98
- }
99
-
100
- return shared.passResult(CHECK_TYPE, `Platform sync checks passed for Issue ${context.issueNumber || "identity"}`);
91
+ ['closed-status-labels', checkClosedIssueStatusLabels], ['status-label', checkStatusLabel],
92
+ ['comment-marker', checkCommentMarker], ['pr-comment-marker', checkPrCommentMarker],
93
+ ['pr-comment-last-commit', checkPrCommentLastCommit], ['pr-comment-content', checkPrCommentRequiredPatterns],
94
+ ['comment-content', checkCommentContent], ['task-comment-content', checkTaskCommentContent],
95
+ ['in-labels-computed', checkInLabelsComputed], ['pr-type-label', checkPrTypeLabel],
96
+ ['in-labels-match-pr', checkInLabelsMatchPr], ['pr-assignee', checkPrAssignee],
97
+ ['requirements', checkSyncedRequirements], ['issue-type', checkIssueType],
98
+ ['issue-fields', checkIssueFields], ['milestone', checkMilestone]
99
+ ] as const;
100
+ const results = subChecks.map(([id, subCheck]) => {
101
+ const audit = platformAuditPolicy(id as PlatformAuditId, context);
102
+ const result = subCheck({ ...context, audit }, remoteData, shared) ?? infoResult(`Platform audit '${id}' was not evaluated`);
103
+ return { ...result, checkId: `platform.${id}`, classification: result.classification ?? audit.classification };
104
+ });
105
+ const blocking = results.map(normalizeVerificationRecord).find((result) => result.effectiveStatus !== 'pass');
106
+ return {
107
+ ...(blocking
108
+ ? {
109
+ type: CHECK_TYPE,
110
+ status: blocking.effectiveStatus,
111
+ message: blocking.message,
112
+ ...(blocking.fail_type ? { fail_type: blocking.fail_type } : {}),
113
+ classification: 'hard'
114
+ }
115
+ : shared.passResult(CHECK_TYPE, `Platform sync audits completed for Issue ${context.issueNumber || "identity"}`)),
116
+ subchecks: results
117
+ };
101
118
  }
102
119
 
103
- async function buildSyncContext({ taskDir, config, artifactFile }: any, shared: VerificationShared): Promise<any> {
120
+ async function buildSyncContext({ taskDir, config, artifactFile, skillName }: any, shared: VerificationShared): Promise<any> {
104
121
  const task = shared.loadTask(taskDir);
105
122
  if (!task.ok) {
106
123
  return { earlyReturn: shared.failResult(CHECK_TYPE, task.message) };
@@ -139,7 +156,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
139
156
  }
140
157
  return { earlyReturn: shared.passResult(CHECK_TYPE, `Skipped: ${platformContext.error?.message || "platform unavailable"}`) };
141
158
  }
142
- const expectedValues = resolveExpectedValues(config);
159
+ const effectiveSkillName = skillName || config.skillName || "code-task";
160
+ const expectedValues = resolveExpectedValues(effectiveSkillName, artifactFile);
143
161
  if (!expectedValues.ok) {
144
162
  return { earlyReturn: shared.failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
145
163
  }
@@ -156,6 +174,7 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
156
174
  task,
157
175
  taskDir,
158
176
  config,
177
+ skillName: effectiveSkillName,
159
178
  artifactFile,
160
179
  artifactPath,
161
180
  issueIdentity,
@@ -174,59 +193,21 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
174
193
  };
175
194
  }
176
195
 
177
- function resolveExpectedValues(config: any): any {
196
+ function resolveExpectedValues(skillName: string, artifactFile: string | undefined): any {
178
197
  const defaults = getDefaults();
179
- const statusLabel = resolveDefaultValue({
180
- collection: defaults.statusLabels,
181
- key: config.expected_status_label_key,
182
- value: config.expected_status_label,
183
- configKey: "expected_status_label_key"
184
- });
185
- if (!statusLabel.ok) {
186
- return statusLabel;
187
- }
188
-
189
- const commentMarker = resolveDefaultValue({
190
- collection: defaults.markers,
191
- key: config.expected_comment_marker_key,
192
- value: config.expected_comment_marker,
193
- configKey: "expected_comment_marker_key"
194
- });
195
- if (!commentMarker.ok) {
196
- return commentMarker;
197
- }
198
-
199
- const prCommentMarker = resolveDefaultValue({
200
- collection: defaults.markers,
201
- key: config.expected_pr_comment_marker_key,
202
- value: config.expected_pr_comment_marker,
203
- configKey: "expected_pr_comment_marker_key"
204
- });
205
- if (!prCommentMarker.ok) {
206
- return prCommentMarker;
207
- }
208
-
198
+ const statusPolicy = platformAuditPolicy('status-label', { skillName, artifactFile });
199
+ const commentPolicy = platformAuditPolicy('comment-marker', { skillName, artifactFile });
200
+ const prCommentPolicy = ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']
201
+ .map((id) => platformAuditPolicy(id as PlatformAuditId, { skillName, artifactFile }))
202
+ .find((policy) => policy.enabled && policy.expectedPrCommentMarkerKey);
209
203
  return {
210
204
  ok: true,
211
- statusLabel: statusLabel.value,
212
- commentMarker: commentMarker.value,
213
- prCommentMarker: prCommentMarker.value
205
+ statusLabel: statusPolicy.enabled ? statusPolicy.expectedStatusLabel || null : null,
206
+ commentMarker: commentPolicy.enabled && commentPolicy.expectedCommentMarkerKey ? defaults.markers[commentPolicy.expectedCommentMarkerKey] : null,
207
+ prCommentMarker: prCommentPolicy?.expectedPrCommentMarkerKey ? defaults.markers[prCommentPolicy.expectedPrCommentMarkerKey] : null
214
208
  };
215
209
  }
216
210
 
217
- function resolveDefaultValue({ collection, key, value, configKey }: any): any {
218
- if (!key) {
219
- return { ok: true, value: value || null };
220
- }
221
-
222
- const resolvedValue = collection[key];
223
- if (!resolvedValue) {
224
- return { ok: false, message: `Unknown ${configKey}: ${key}` };
225
- }
226
-
227
- return { ok: true, value: resolvedValue };
228
- }
229
-
230
211
  async function fetchRemoteData(context: any, shared: VerificationShared): Promise<any> {
231
212
  const provider = context.provider;
232
213
  const operationContext = providerOperationContext(context.loadedContext, context.taskDir);
@@ -236,8 +217,8 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
236
217
  taskId: context.task?.metadata?.id || context.task?.id || "",
237
218
  ...(context.issueIdentity ? { issue: context.issueIdentity } : {}),
238
219
  ...(context.prIdentity ? { changeRequest: context.prIdentity } : {}),
239
- includeComments: shouldFetchComments(context.config),
240
- includeFields: Boolean(context.config.verify_issue_fields)
220
+ includeComments: shouldFetchIssueComments(context),
221
+ includeFields: true
241
222
  })
242
223
  : unsupportedProviderOperation(provider, "verification.fetchRemoteFacts");
243
224
  if (!facts.ok) {
@@ -257,7 +238,7 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
257
238
  }
258
239
  : null;
259
240
  let issueFields: any;
260
- if (context.config.verify_issue_fields && issueSnapshot?.issueType) {
241
+ if (issueSnapshot?.issueType) {
261
242
  const fieldKinds = new Map(issueSnapshot.issueType.fields.map((field: { name: string; kind: string }) => [field.name, field.kind]));
262
243
  issueFields = {
263
244
  pinnedNames: new Set(issueSnapshot.issueType.fields.map((field: { name: string }) => field.name)),
@@ -265,7 +246,7 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
265
246
  };
266
247
  }
267
248
  let prComments = null;
268
- if (context.prMarker && context.prIdentity && shouldFetchComments(context.config) && provider?.comments?.list) {
249
+ if (context.prMarker && context.prIdentity && shouldFetchPrComments(context) && provider?.comments?.list) {
269
250
  const listed = await provider.comments.list({ context: operationContext, parent: context.prIdentity });
270
251
  if (!listed.ok) return {
271
252
  earlyReturn: listed.error.retryable
@@ -277,7 +258,7 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
277
258
  const changeRequest = facts.value.changeRequest;
278
259
  let inLabelMapping: Record<string, string[]> = {};
279
260
  let repositoryLabels: string[] = [];
280
- if (context.config.verify_in_labels_computed && context.hasTriage) {
261
+ if (platformAuditPolicy('in-labels-computed', context).enabled && context.hasTriage) {
281
262
  const mapping = loadInLabelMapping(shared);
282
263
  if (!mapping.ok) return { earlyReturn: shared.failResult(CHECK_TYPE, mapping.message, "check_failed") };
283
264
  inLabelMapping = mapping.value;
@@ -319,30 +300,31 @@ function mapTaskTypeToLabel(taskType: any): any {
319
300
  return taskTypeLabel(taskType);
320
301
  }
321
302
 
322
- function shouldFetchComments(config: any): any {
323
- return Boolean(
324
- config.expected_comment_marker
325
- || config.expected_comment_marker_key
326
- || config.expected_pr_comment_marker
327
- || config.expected_pr_comment_marker_key
328
- || config.verify_pr_comment_last_commit_matches_head
329
- || config.verify_comment_content
330
- || config.verify_task_comment_content
331
- );
303
+ function hasEnabledAudit(context: any, auditIds: PlatformAuditId[]): boolean {
304
+ return auditIds
305
+ .some((id) => platformAuditPolicy(id as PlatformAuditId, context).enabled);
306
+ }
307
+
308
+ function shouldFetchIssueComments(context: any): boolean {
309
+ return hasEnabledAudit(context, ['comment-marker', 'comment-content', 'task-comment-content']);
310
+ }
311
+
312
+ function shouldFetchPrComments(context: any): boolean {
313
+ return hasEnabledAudit(context, ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']);
332
314
  }
333
315
 
334
316
  function checkStatusLabel(context: any, remoteData: any, shared: VerificationShared): any {
335
317
  if (!context.expectedStatusLabel || !context.hasTriage) {
336
- return null;
318
+ return infoResult('Status label audit is not applicable because no expected label or triage capability is available');
337
319
  }
338
320
 
339
321
  if (String(remoteData.issue.state || "").toUpperCase() !== "OPEN") {
340
- return null;
322
+ return infoResult('Status label audit is not applicable because the Issue is closed');
341
323
  }
342
324
 
343
325
  const labels = extractLabelNames(remoteData.issue.labels);
344
326
  if (labels.includes(context.expectedStatusLabel)) {
345
- return null;
327
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected status label '${context.expectedStatusLabel}'`);
346
328
  }
347
329
 
348
330
  return shared.failResult(CHECK_TYPE,
@@ -352,18 +334,18 @@ function checkStatusLabel(context: any, remoteData: any, shared: VerificationSha
352
334
  }
353
335
 
354
336
  function checkClosedIssueStatusLabels(context: any, remoteData: any, shared: VerificationShared): any {
355
- if (!context.config.verify_closed_issue_has_no_status_labels) {
356
- return null;
337
+ if (!context.audit.enabled) {
338
+ return infoResult('Closed Issue status-label audit is not enabled for this workflow');
357
339
  }
358
340
 
359
341
  if (String(remoteData.issue.state || "").toUpperCase() !== "CLOSED") {
360
- return null;
342
+ return infoResult('Closed Issue status-label audit is not applicable because the Issue is open');
361
343
  }
362
344
 
363
345
  const statusLabels = extractLabelNames(remoteData.issue.labels)
364
346
  .filter((label: any) => label.startsWith("status:"));
365
347
  if (statusLabels.length === 0) {
366
- return null;
348
+ return shared.passResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} has no status labels`);
367
349
  }
368
350
 
369
351
  return shared.failResult(CHECK_TYPE,
@@ -373,13 +355,13 @@ function checkClosedIssueStatusLabels(context: any, remoteData: any, shared: Ver
373
355
  }
374
356
 
375
357
  function checkCommentMarker(context: any, remoteData: any, shared: VerificationShared): any {
376
- if (!context.marker) {
377
- return null;
358
+ if (!context.audit.enabled || !context.marker) {
359
+ return infoResult('Issue comment-marker audit is not applicable because no marker is configured');
378
360
  }
379
361
 
380
362
  const comment = findCommentByMarker(remoteData.comments, context.marker);
381
363
  if (comment) {
382
- return null;
364
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
383
365
  }
384
366
 
385
367
  return shared.failResult(CHECK_TYPE,
@@ -389,13 +371,13 @@ function checkCommentMarker(context: any, remoteData: any, shared: VerificationS
389
371
  }
390
372
 
391
373
  function checkPrCommentMarker(context: any, remoteData: any, shared: VerificationShared): any {
392
- if (!context.prMarker) {
393
- return null;
374
+ if (!context.audit.enabled || !context.prMarker) {
375
+ return infoResult('PR comment-marker audit is not applicable because no marker is configured');
394
376
  }
395
377
 
396
378
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
397
379
  if (comment) {
398
- return null;
380
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected comment marker`);
399
381
  }
400
382
 
401
383
  return shared.failResult(CHECK_TYPE,
@@ -405,95 +387,95 @@ function checkPrCommentMarker(context: any, remoteData: any, shared: Verificatio
405
387
  }
406
388
 
407
389
  function checkPrCommentLastCommit(context: any, remoteData: any, shared: VerificationShared): any {
408
- if (!context.config.verify_pr_comment_last_commit_matches_head) {
409
- return null;
390
+ if (!context.audit.enabled) {
391
+ return infoResult('PR last-commit audit is not enabled for this workflow');
410
392
  }
411
393
 
412
394
  if (!context.prMarker) {
413
- return shared.failResult(CHECK_TYPE,
395
+ return hardResult(shared.failResult(CHECK_TYPE,
414
396
  "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
415
397
  "check_failed"
416
- );
398
+ ));
417
399
  }
418
400
 
419
401
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
420
402
  if (!comment) {
421
- return shared.failResult(CHECK_TYPE,
403
+ return hardResult(shared.failResult(CHECK_TYPE,
422
404
  `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
423
405
  "check_failed"
424
- );
406
+ ));
425
407
  }
426
408
 
427
409
  const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
428
410
  if (!match) {
429
- return shared.failResult(CHECK_TYPE,
411
+ return hardResult(shared.failResult(CHECK_TYPE,
430
412
  `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
431
413
  "check_failed"
432
- );
414
+ ));
433
415
  }
434
416
 
435
417
  const expectedHead = String(remoteData.prHeadSha || "").trim();
436
- if (!expectedHead) return shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
418
+ if (!expectedHead) return hardResult(shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error"));
437
419
  const actualHead = match[1]!.trim();
438
420
  if (expectedHead === actualHead) {
439
- return null;
421
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit matches HEAD`);
440
422
  }
441
423
 
442
- return shared.failResult(CHECK_TYPE,
424
+ return hardResult(shared.failResult(CHECK_TYPE,
443
425
  `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
444
426
  "check_failed"
445
- );
427
+ ));
446
428
  }
447
429
 
448
430
  function checkPrCommentRequiredPatterns(context: any, remoteData: any, shared: VerificationShared): any {
449
- const patterns = context.config.expected_pr_comment_required_patterns || [];
431
+ const patterns = context.audit.enabled ? context.config.expected_pr_comment_required_patterns || [] : [];
450
432
  if (!Array.isArray(patterns) || patterns.length === 0) {
451
- return null;
433
+ return infoResult('PR comment-content audit is not enabled for this workflow');
452
434
  }
453
435
 
454
436
  if (!context.prMarker) {
455
- return shared.failResult(CHECK_TYPE,
437
+ return hardResult(shared.failResult(CHECK_TYPE,
456
438
  "expected_pr_comment_required_patterns requires expected_pr_comment_marker",
457
439
  "check_failed"
458
- );
440
+ ));
459
441
  }
460
442
 
461
443
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
462
444
  if (!comment) {
463
- return shared.failResult(CHECK_TYPE,
445
+ return hardResult(shared.failResult(CHECK_TYPE,
464
446
  `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
465
447
  "check_failed"
466
- );
448
+ ));
467
449
  }
468
450
 
469
451
  const body = String(comment.body || "");
470
452
  for (const pattern of patterns) {
471
453
  const regex = new RegExp(pattern, "m");
472
454
  if (!regex.test(body)) {
473
- return shared.failResult(CHECK_TYPE,
455
+ return hardResult(shared.failResult(CHECK_TYPE,
474
456
  `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`,
475
457
  "check_failed"
476
- );
458
+ ));
477
459
  }
478
460
  }
479
461
 
480
- return null;
462
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment has all required patterns`);
481
463
  }
482
464
 
483
465
  function checkCommentContent(context: any, remoteData: any, shared: VerificationShared): any {
484
- if (!context.config.verify_comment_content) {
485
- return null;
466
+ if (!context.audit.enabled) {
467
+ return infoResult('Artifact comment-content audit is not enabled for this workflow');
486
468
  }
487
469
 
488
470
  if (!context.marker) {
489
- return shared.failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
471
+ return hardResult(shared.failResult(CHECK_TYPE, "Artifact comment-content audit requires an expected comment marker", "check_failed"));
490
472
  }
491
473
 
492
474
  if (!context.artifactPath || !shared.safeStat(context.artifactPath)) {
493
- return shared.failResult(CHECK_TYPE,
475
+ return hardResult(shared.failResult(CHECK_TYPE,
494
476
  `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
495
477
  "check_failed"
496
- );
478
+ ));
497
479
  }
498
480
 
499
481
  const comment = findCommentByMarker(remoteData.comments, context.marker);
@@ -503,28 +485,28 @@ function checkCommentContent(context: any, remoteData: any, shared: Verification
503
485
  const localCanonical = canonicalizeCommentBody(fs.readFileSync(context.artifactPath, "utf8"));
504
486
  const commentCanonical = canonicalizeCommentBody(extractCommentBody(comment?.body || ""));
505
487
  if (!localCanonical.ok) {
506
- return shared.failResult(CHECK_TYPE,
488
+ return hardResult(shared.failResult(CHECK_TYPE,
507
489
  `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`,
508
490
  "check_failed"
509
- );
491
+ ));
510
492
  }
511
493
  if (!commentCanonical.ok) {
512
- return shared.failResult(CHECK_TYPE,
494
+ return hardResult(shared.failResult(CHECK_TYPE,
513
495
  `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`,
514
496
  "check_failed"
515
- );
497
+ ));
516
498
  }
517
499
  localContent = shared.normalizeContent(localCanonical.value);
518
500
  commentContent = shared.normalizeContent(commentCanonical.value);
519
501
  } catch (error) {
520
- return shared.failResult(CHECK_TYPE,
502
+ return hardResult(shared.failResult(CHECK_TYPE,
521
503
  `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`,
522
504
  "check_failed"
523
- );
505
+ ));
524
506
  }
525
507
 
526
508
  if (localContent === commentContent) {
527
- return null;
509
+ return shared.passResult(CHECK_TYPE, `Artifact comment content matches Issue #${context.issueNumber}`);
528
510
  }
529
511
 
530
512
  return shared.failResult(CHECK_TYPE,
@@ -539,13 +521,9 @@ function checkCommentContent(context: any, remoteData: any, shared: Verification
539
521
  }
540
522
 
541
523
  function checkTaskCommentContent(context: any, remoteData: any, shared: VerificationShared): any {
542
- if (!context.config.verify_task_comment_content) {
543
- return null;
524
+ if (!context.audit.enabled) {
525
+ return infoResult('Task comment-content audit is not enabled for this workflow');
544
526
  }
545
- if (isTaskCommentTooLarge(context.task.content)) {
546
- return null;
547
- }
548
-
549
527
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
550
528
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
551
529
  if (!comment) {
@@ -555,18 +533,24 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
555
533
  );
556
534
  }
557
535
 
558
- const expectedTaskBody = buildExpectedTaskBody(context.task.content, shared);
559
- if (expectedTaskBody === null) {
560
- return shared.failResult(CHECK_TYPE,
561
- "Task content cannot be rendered safely for comment verification",
562
- "check_failed"
536
+ let renderedTask: { body: string; byteLength: number };
537
+ try {
538
+ renderedTask = renderTaskCommentResult(
539
+ context.task.content,
540
+ context.task.metadata.id,
541
+ loadProjectLanguage(shared)
563
542
  );
543
+ } catch {
544
+ return hardResult(shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed"));
545
+ }
546
+ if (renderedTask.byteLength > 60_000) {
547
+ return hardResult(shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed"));
564
548
  }
565
- const expectedBody = shared.normalizeContent(expectedTaskBody);
549
+ const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
566
550
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
567
551
 
568
552
  if (expectedBody === commentBody) {
569
- return null;
553
+ return shared.passResult(CHECK_TYPE, `Task comment content matches Issue #${context.issueNumber}`);
570
554
  }
571
555
 
572
556
  return shared.failResult(CHECK_TYPE,
@@ -576,17 +560,17 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
576
560
  }
577
561
 
578
562
  function checkPrTypeLabel(context: any, remoteData: any, shared: VerificationShared): any {
579
- if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
580
- return null;
563
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
564
+ return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
581
565
  }
582
566
 
583
567
  const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
584
568
  if (!expectedLabel) {
585
- return null;
569
+ return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
586
570
  }
587
571
 
588
572
  if (remoteData.prLabels.includes(expectedLabel)) {
589
- return null;
573
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
590
574
  }
591
575
 
592
576
  return shared.failResult(CHECK_TYPE,
@@ -596,8 +580,8 @@ function checkPrTypeLabel(context: any, remoteData: any, shared: VerificationSha
596
580
  }
597
581
 
598
582
  function checkInLabelsMatchPr(context: any, remoteData: any, shared: VerificationShared): any {
599
- if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
600
- return null;
583
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
584
+ return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
601
585
  }
602
586
 
603
587
  const issueInLabels = extractLabelNames(remoteData.issue.labels)
@@ -608,7 +592,7 @@ function checkInLabelsMatchPr(context: any, remoteData: any, shared: Verificatio
608
592
  .sort();
609
593
 
610
594
  if (arraysEqual(issueInLabels, prInLabels)) {
611
- return null;
595
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
612
596
  }
613
597
 
614
598
  return shared.failResult(CHECK_TYPE,
@@ -618,8 +602,8 @@ function checkInLabelsMatchPr(context: any, remoteData: any, shared: Verificatio
618
602
  }
619
603
 
620
604
  function checkInLabelsComputed(context: any, remoteData: any, shared: VerificationShared): any {
621
- if (!context.config.verify_in_labels_computed || !context.hasTriage) {
622
- return null;
605
+ if (!context.audit.enabled || !context.hasTriage) {
606
+ return infoResult('Computed in: label audit is not applicable because it is disabled or triage capability is unavailable');
623
607
  }
624
608
 
625
609
  const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
@@ -630,7 +614,7 @@ function checkInLabelsComputed(context: any, remoteData: any, shared: Verificati
630
614
  }
631
615
 
632
616
  if (expectedInLabels.mode === "skipped") {
633
- return null;
617
+ return infoResult('Computed in: label audit is not applicable because no mapped changed path exists');
634
618
  }
635
619
 
636
620
  const actualInLabels = extractLabelNames(remoteData.issue.labels)
@@ -638,7 +622,7 @@ function checkInLabelsComputed(context: any, remoteData: any, shared: Verificati
638
622
  .sort();
639
623
 
640
624
  if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
641
- return null;
625
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels match committed changes`);
642
626
  }
643
627
 
644
628
  return shared.failResult(
@@ -649,13 +633,13 @@ function checkInLabelsComputed(context: any, remoteData: any, shared: Verificati
649
633
  }
650
634
 
651
635
  function checkSyncedRequirements(context: any, remoteData: any, shared: VerificationShared): any {
652
- if (!context.config.sync_checked_requirements || !context.hasTriage) {
653
- return null;
636
+ if (!context.audit.enabled || !context.hasTriage) {
637
+ return infoResult('Requirements audit is not applicable because it is disabled or triage capability is unavailable');
654
638
  }
655
639
 
656
640
  const checkedRequirements = shared.getCheckedRequirements(context.task.content);
657
641
  if (checkedRequirements.length === 0) {
658
- return null;
642
+ return infoResult('Requirements audit is not applicable because the task has no checked requirements');
659
643
  }
660
644
 
661
645
  const issueBody = remoteData.issue.body || "";
@@ -664,20 +648,20 @@ function checkSyncedRequirements(context: any, remoteData: any, shared: Verifica
664
648
  requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task")
665
649
  );
666
650
  if (resolution.status === "missing") {
667
- return null;
651
+ return infoResult('Requirements audit is not applicable because the Issue has no requirements section');
668
652
  }
669
653
  if (resolution.status === "ambiguous") {
670
- return shared.failResult(CHECK_TYPE,
654
+ return hardResult(shared.failResult(CHECK_TYPE,
671
655
  `Issue #${context.issueNumber} requirements section is ambiguous`,
672
656
  "check_failed"
673
- );
657
+ ));
674
658
  }
675
659
  const requirementBody = issueBody.slice(resolution.bodyStart, resolution.bodyEnd);
676
660
  const missingRequirements = checkedRequirements.filter(
677
661
  (item: any) => !hasCheckedRequirement(requirementBody, item)
678
662
  );
679
663
  if (missingRequirements.length === 0) {
680
- return null;
664
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} contains all checked requirements`);
681
665
  }
682
666
 
683
667
  return shared.failResult(CHECK_TYPE,
@@ -687,17 +671,17 @@ function checkSyncedRequirements(context: any, remoteData: any, shared: Verifica
687
671
  }
688
672
 
689
673
  function checkIssueType(context: any, remoteData: any, shared: VerificationShared): any {
690
- if (!context.config.verify_issue_type || !context.hasPush) {
691
- return null;
674
+ if (!context.hasPush) {
675
+ return infoResult('Issue Type audit is not applicable because push capability is unavailable');
692
676
  }
693
677
 
694
678
  if (remoteData.issueType === undefined) {
695
- return null;
679
+ return infoResult('Issue Type audit is not applicable because the provider does not expose Issue Type');
696
680
  }
697
681
 
698
682
  if (!remoteData.issueType) {
699
683
  if (context.repoOwnerType === "User") {
700
- return null;
684
+ return infoResult('Issue Type audit is not applicable for a user-owned repository');
701
685
  }
702
686
 
703
687
  return shared.failResult(CHECK_TYPE,
@@ -714,16 +698,16 @@ function checkIssueType(context: any, remoteData: any, shared: VerificationShare
714
698
  );
715
699
  }
716
700
 
717
- return null;
701
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has the expected Issue Type`);
718
702
  }
719
703
 
720
704
  function checkIssueFields(context: any, remoteData: any, shared: VerificationShared): any {
721
- if (!context.config.verify_issue_fields || !context.hasPush) {
722
- return null;
705
+ if (!context.hasPush) {
706
+ return infoResult('Issue field audit is not applicable because push capability is unavailable');
723
707
  }
724
708
 
725
709
  if (remoteData.issueFields === undefined) {
726
- return null;
710
+ return infoResult('Issue field audit is not applicable because the provider does not expose Issue fields');
727
711
  }
728
712
 
729
713
  for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
@@ -753,12 +737,12 @@ function checkIssueFields(context: any, remoteData: any, shared: VerificationSha
753
737
  }
754
738
  }
755
739
 
756
- return null;
740
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
757
741
  }
758
742
 
759
743
  function checkPrAssignee(context: any, remoteData: any, shared: VerificationShared): any {
760
- if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
761
- return null;
744
+ if (!context.audit.enabled || !context.hasPush || !context.prNumber) {
745
+ return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
762
746
  }
763
747
 
764
748
  if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
@@ -768,12 +752,12 @@ function checkPrAssignee(context: any, remoteData: any, shared: VerificationShar
768
752
  );
769
753
  }
770
754
 
771
- return null;
755
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
772
756
  }
773
757
 
774
758
  function checkMilestone(context: any, remoteData: any, shared: VerificationShared): any {
775
- if (!context.config.verify_milestone || !context.hasTriage) {
776
- return null;
759
+ if (!context.hasTriage) {
760
+ return infoResult('Milestone audit is not applicable because triage capability is unavailable');
777
761
  }
778
762
 
779
763
  if (!remoteData.issue?.milestone?.title) {
@@ -790,7 +774,7 @@ function checkMilestone(context: any, remoteData: any, shared: VerificationShare
790
774
  );
791
775
  }
792
776
 
793
- if (context.config.verify_milestone_specific) {
777
+ if (context.audit.requireSpecificMilestone) {
794
778
  const issueTitle = remoteData.issue.milestone.title;
795
779
  if (VERSION_LINE_REGEX.test(issueTitle)) {
796
780
  return shared.failResult(CHECK_TYPE,
@@ -806,7 +790,7 @@ function checkMilestone(context: any, remoteData: any, shared: VerificationShare
806
790
  }
807
791
  }
808
792
 
809
- return null;
793
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
810
794
  }
811
795
 
812
796
  function findCommentByMarker(comments: any, marker: any): any {
@@ -833,7 +817,7 @@ function extractCommentBody(commentBody: any): any {
833
817
  start += 1;
834
818
  }
835
819
 
836
- if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start]!.trim())) {
820
+ if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start]!.trim()) || /^> 任务同步 · .+$/.test(lines[start]!.trim()))) {
837
821
  start += 1;
838
822
  }
839
823
 
@@ -841,6 +825,16 @@ function extractCommentBody(commentBody: any): any {
841
825
  start += 1;
842
826
  }
843
827
 
828
+ if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
829
+ const metadataEnd = lines.indexOf("</details>", start + 1);
830
+ if (metadataEnd >= 0) {
831
+ start = metadataEnd + 1;
832
+ while (start < lines.length && lines[start]!.trim() === "") {
833
+ start += 1;
834
+ }
835
+ }
836
+ }
837
+
844
838
  let end = lines.length;
845
839
  for (let index = lines.length - 1; index >= start; index -= 1) {
846
840
  const trimmed = lines[index]!.trim();
@@ -1042,7 +1036,9 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1042
1036
  if (!baseRef) {
1043
1037
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
1044
1038
  }
1045
- const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], taskDir);
1039
+ const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
1040
+ const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
1041
+ const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
1046
1042
  if (!changedFilesResult.ok) {
1047
1043
  return { ...changedFilesResult, type: "network_error" };
1048
1044
  }
@@ -1069,6 +1065,25 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1069
1065
  return { ok: true, labels: planned.target, mode: "mapped" };
1070
1066
  }
1071
1067
 
1068
+ function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
1069
+ try {
1070
+ const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
1071
+ cwd: repositoryRoot,
1072
+ encoding: "utf8",
1073
+ stdio: ["pipe", "pipe", "pipe"]
1074
+ }).trim().split(/\r?\n\r?\n/);
1075
+ const expectedRef = `refs/heads/${branch}`;
1076
+ for (const record of records) {
1077
+ const worktree = /^worktree (.+)$/m.exec(record)?.[1];
1078
+ const branchRef = /^branch (.+)$/m.exec(record)?.[1];
1079
+ if (worktree && branchRef === expectedRef) return worktree;
1080
+ }
1081
+ } catch {
1082
+ // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
1083
+ }
1084
+ return null;
1085
+ }
1086
+
1072
1087
  function loadInLabelMapping(shared: VerificationShared): any {
1073
1088
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
1074
1089
  if (!fs.existsSync(configPath)) {