@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,3 +1,4 @@
1
+ import { normalizeVerificationRecord, platformAuditPolicy } from "../task/gate-policy.js";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
@@ -10,7 +11,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.js";
10
11
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.js";
11
12
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.js";
12
13
  import { taskIssueIdentity } from "./task-identities.js";
13
- import { isTaskCommentTooLarge } from "./issue-comments.js";
14
+ import { renderTaskCommentResult } from "./issue-comments.js";
14
15
  import { CONTROL_MARKER_PATTERN, canonicalizeCommentBody, renderSafeCodeFence, sanitizeMarkdownDocument } from "./comment-safety.js";
15
16
  const CHECK_TYPE = "platform-sync";
16
17
  const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
@@ -49,8 +50,21 @@ function sanitizeCommentContent(content) {
49
50
  const result = sanitizeMarkdownDocument(content, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
50
51
  return result.ok ? result.value : null;
51
52
  }
52
- export async function check({ taskDir, config, artifactFile }, shared) {
53
- const context = await buildSyncContext({ taskDir, config, artifactFile }, shared);
53
+ function infoResult(message) {
54
+ return {
55
+ type: CHECK_TYPE,
56
+ status: 'pass',
57
+ message,
58
+ classification: 'info',
59
+ reason: 'NOT_APPLICABLE',
60
+ action: 'No action required'
61
+ };
62
+ }
63
+ function hardResult(result) {
64
+ return { ...result, classification: 'hard' };
65
+ }
66
+ export async function check({ taskDir, config, artifactFile, skillName }, shared) {
67
+ const context = await buildSyncContext({ taskDir, config, artifactFile, skillName }, shared);
54
68
  if (context.earlyReturn) {
55
69
  return context.earlyReturn;
56
70
  }
@@ -59,32 +73,35 @@ export async function check({ taskDir, config, artifactFile }, shared) {
59
73
  return remoteData.earlyReturn;
60
74
  }
61
75
  const subChecks = [
62
- checkClosedIssueStatusLabels,
63
- checkStatusLabel,
64
- checkCommentMarker,
65
- checkPrCommentMarker,
66
- checkPrCommentLastCommit,
67
- checkPrCommentRequiredPatterns,
68
- checkCommentContent,
69
- checkTaskCommentContent,
70
- checkInLabelsComputed,
71
- checkPrTypeLabel,
72
- checkInLabelsMatchPr,
73
- checkPrAssignee,
74
- checkSyncedRequirements,
75
- checkIssueType,
76
- checkIssueFields,
77
- checkMilestone
76
+ ['closed-status-labels', checkClosedIssueStatusLabels], ['status-label', checkStatusLabel],
77
+ ['comment-marker', checkCommentMarker], ['pr-comment-marker', checkPrCommentMarker],
78
+ ['pr-comment-last-commit', checkPrCommentLastCommit], ['pr-comment-content', checkPrCommentRequiredPatterns],
79
+ ['comment-content', checkCommentContent], ['task-comment-content', checkTaskCommentContent],
80
+ ['in-labels-computed', checkInLabelsComputed], ['pr-type-label', checkPrTypeLabel],
81
+ ['in-labels-match-pr', checkInLabelsMatchPr], ['pr-assignee', checkPrAssignee],
82
+ ['requirements', checkSyncedRequirements], ['issue-type', checkIssueType],
83
+ ['issue-fields', checkIssueFields], ['milestone', checkMilestone]
78
84
  ];
79
- for (const subCheck of subChecks) {
80
- const result = subCheck(context, remoteData, shared);
81
- if (result) {
82
- return result;
83
- }
84
- }
85
- return shared.passResult(CHECK_TYPE, `Platform sync checks passed for Issue ${context.issueNumber || "identity"}`);
85
+ const results = subChecks.map(([id, subCheck]) => {
86
+ const audit = platformAuditPolicy(id, context);
87
+ const result = subCheck({ ...context, audit }, remoteData, shared) ?? infoResult(`Platform audit '${id}' was not evaluated`);
88
+ return { ...result, checkId: `platform.${id}`, classification: result.classification ?? audit.classification };
89
+ });
90
+ const blocking = results.map(normalizeVerificationRecord).find((result) => result.effectiveStatus !== 'pass');
91
+ return {
92
+ ...(blocking
93
+ ? {
94
+ type: CHECK_TYPE,
95
+ status: blocking.effectiveStatus,
96
+ message: blocking.message,
97
+ ...(blocking.fail_type ? { fail_type: blocking.fail_type } : {}),
98
+ classification: 'hard'
99
+ }
100
+ : shared.passResult(CHECK_TYPE, `Platform sync audits completed for Issue ${context.issueNumber || "identity"}`)),
101
+ subchecks: results
102
+ };
86
103
  }
87
- async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
104
+ async function buildSyncContext({ taskDir, config, artifactFile, skillName }, shared) {
88
105
  const task = shared.loadTask(taskDir);
89
106
  if (!task.ok) {
90
107
  return { earlyReturn: shared.failResult(CHECK_TYPE, task.message) };
@@ -120,7 +137,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
120
137
  }
121
138
  return { earlyReturn: shared.passResult(CHECK_TYPE, `Skipped: ${platformContext.error?.message || "platform unavailable"}`) };
122
139
  }
123
- const expectedValues = resolveExpectedValues(config);
140
+ const effectiveSkillName = skillName || config.skillName || "code-task";
141
+ const expectedValues = resolveExpectedValues(effectiveSkillName, artifactFile);
124
142
  if (!expectedValues.ok) {
125
143
  return { earlyReturn: shared.failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
126
144
  }
@@ -135,6 +153,7 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
135
153
  task,
136
154
  taskDir,
137
155
  config,
156
+ skillName: effectiveSkillName,
138
157
  artifactFile,
139
158
  artifactPath,
140
159
  issueIdentity,
@@ -152,52 +171,20 @@ async function buildSyncContext({ taskDir, config, artifactFile }, shared) {
152
171
  loadedContext: loaded.ok ? loaded.value : null
153
172
  };
154
173
  }
155
- function resolveExpectedValues(config) {
174
+ function resolveExpectedValues(skillName, artifactFile) {
156
175
  const defaults = getDefaults();
157
- const statusLabel = resolveDefaultValue({
158
- collection: defaults.statusLabels,
159
- key: config.expected_status_label_key,
160
- value: config.expected_status_label,
161
- configKey: "expected_status_label_key"
162
- });
163
- if (!statusLabel.ok) {
164
- return statusLabel;
165
- }
166
- const commentMarker = resolveDefaultValue({
167
- collection: defaults.markers,
168
- key: config.expected_comment_marker_key,
169
- value: config.expected_comment_marker,
170
- configKey: "expected_comment_marker_key"
171
- });
172
- if (!commentMarker.ok) {
173
- return commentMarker;
174
- }
175
- const prCommentMarker = resolveDefaultValue({
176
- collection: defaults.markers,
177
- key: config.expected_pr_comment_marker_key,
178
- value: config.expected_pr_comment_marker,
179
- configKey: "expected_pr_comment_marker_key"
180
- });
181
- if (!prCommentMarker.ok) {
182
- return prCommentMarker;
183
- }
176
+ const statusPolicy = platformAuditPolicy('status-label', { skillName, artifactFile });
177
+ const commentPolicy = platformAuditPolicy('comment-marker', { skillName, artifactFile });
178
+ const prCommentPolicy = ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']
179
+ .map((id) => platformAuditPolicy(id, { skillName, artifactFile }))
180
+ .find((policy) => policy.enabled && policy.expectedPrCommentMarkerKey);
184
181
  return {
185
182
  ok: true,
186
- statusLabel: statusLabel.value,
187
- commentMarker: commentMarker.value,
188
- prCommentMarker: prCommentMarker.value
183
+ statusLabel: statusPolicy.enabled ? statusPolicy.expectedStatusLabel || null : null,
184
+ commentMarker: commentPolicy.enabled && commentPolicy.expectedCommentMarkerKey ? defaults.markers[commentPolicy.expectedCommentMarkerKey] : null,
185
+ prCommentMarker: prCommentPolicy?.expectedPrCommentMarkerKey ? defaults.markers[prCommentPolicy.expectedPrCommentMarkerKey] : null
189
186
  };
190
187
  }
191
- function resolveDefaultValue({ collection, key, value, configKey }) {
192
- if (!key) {
193
- return { ok: true, value: value || null };
194
- }
195
- const resolvedValue = collection[key];
196
- if (!resolvedValue) {
197
- return { ok: false, message: `Unknown ${configKey}: ${key}` };
198
- }
199
- return { ok: true, value: resolvedValue };
200
- }
201
188
  async function fetchRemoteData(context, shared) {
202
189
  const provider = context.provider;
203
190
  const operationContext = providerOperationContext(context.loadedContext, context.taskDir);
@@ -207,8 +194,8 @@ async function fetchRemoteData(context, shared) {
207
194
  taskId: context.task?.metadata?.id || context.task?.id || "",
208
195
  ...(context.issueIdentity ? { issue: context.issueIdentity } : {}),
209
196
  ...(context.prIdentity ? { changeRequest: context.prIdentity } : {}),
210
- includeComments: shouldFetchComments(context.config),
211
- includeFields: Boolean(context.config.verify_issue_fields)
197
+ includeComments: shouldFetchIssueComments(context),
198
+ includeFields: true
212
199
  })
213
200
  : unsupportedProviderOperation(provider, "verification.fetchRemoteFacts");
214
201
  if (!facts.ok) {
@@ -228,7 +215,7 @@ async function fetchRemoteData(context, shared) {
228
215
  }
229
216
  : null;
230
217
  let issueFields;
231
- if (context.config.verify_issue_fields && issueSnapshot?.issueType) {
218
+ if (issueSnapshot?.issueType) {
232
219
  const fieldKinds = new Map(issueSnapshot.issueType.fields.map((field) => [field.name, field.kind]));
233
220
  issueFields = {
234
221
  pinnedNames: new Set(issueSnapshot.issueType.fields.map((field) => field.name)),
@@ -236,7 +223,7 @@ async function fetchRemoteData(context, shared) {
236
223
  };
237
224
  }
238
225
  let prComments = null;
239
- if (context.prMarker && context.prIdentity && shouldFetchComments(context.config) && provider?.comments?.list) {
226
+ if (context.prMarker && context.prIdentity && shouldFetchPrComments(context) && provider?.comments?.list) {
240
227
  const listed = await provider.comments.list({ context: operationContext, parent: context.prIdentity });
241
228
  if (!listed.ok)
242
229
  return {
@@ -249,7 +236,7 @@ async function fetchRemoteData(context, shared) {
249
236
  const changeRequest = facts.value.changeRequest;
250
237
  let inLabelMapping = {};
251
238
  let repositoryLabels = [];
252
- if (context.config.verify_in_labels_computed && context.hasTriage) {
239
+ if (platformAuditPolicy('in-labels-computed', context).enabled && context.hasTriage) {
253
240
  const mapping = loadInLabelMapping(shared);
254
241
  if (!mapping.ok)
255
242
  return { earlyReturn: shared.failResult(CHECK_TYPE, mapping.message, "check_failed") };
@@ -290,116 +277,117 @@ async function fetchRemoteData(context, shared) {
290
277
  function mapTaskTypeToLabel(taskType) {
291
278
  return taskTypeLabel(taskType);
292
279
  }
293
- function shouldFetchComments(config) {
294
- return Boolean(config.expected_comment_marker
295
- || config.expected_comment_marker_key
296
- || config.expected_pr_comment_marker
297
- || config.expected_pr_comment_marker_key
298
- || config.verify_pr_comment_last_commit_matches_head
299
- || config.verify_comment_content
300
- || config.verify_task_comment_content);
280
+ function hasEnabledAudit(context, auditIds) {
281
+ return auditIds
282
+ .some((id) => platformAuditPolicy(id, context).enabled);
283
+ }
284
+ function shouldFetchIssueComments(context) {
285
+ return hasEnabledAudit(context, ['comment-marker', 'comment-content', 'task-comment-content']);
286
+ }
287
+ function shouldFetchPrComments(context) {
288
+ return hasEnabledAudit(context, ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']);
301
289
  }
302
290
  function checkStatusLabel(context, remoteData, shared) {
303
291
  if (!context.expectedStatusLabel || !context.hasTriage) {
304
- return null;
292
+ return infoResult('Status label audit is not applicable because no expected label or triage capability is available');
305
293
  }
306
294
  if (String(remoteData.issue.state || "").toUpperCase() !== "OPEN") {
307
- return null;
295
+ return infoResult('Status label audit is not applicable because the Issue is closed');
308
296
  }
309
297
  const labels = extractLabelNames(remoteData.issue.labels);
310
298
  if (labels.includes(context.expectedStatusLabel)) {
311
- return null;
299
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected status label '${context.expectedStatusLabel}'`);
312
300
  }
313
301
  return shared.failResult(CHECK_TYPE, `Expected label '${context.expectedStatusLabel}' not found on Issue #${context.issueNumber}`, "check_failed");
314
302
  }
315
303
  function checkClosedIssueStatusLabels(context, remoteData, shared) {
316
- if (!context.config.verify_closed_issue_has_no_status_labels) {
317
- return null;
304
+ if (!context.audit.enabled) {
305
+ return infoResult('Closed Issue status-label audit is not enabled for this workflow');
318
306
  }
319
307
  if (String(remoteData.issue.state || "").toUpperCase() !== "CLOSED") {
320
- return null;
308
+ return infoResult('Closed Issue status-label audit is not applicable because the Issue is open');
321
309
  }
322
310
  const statusLabels = extractLabelNames(remoteData.issue.labels)
323
311
  .filter((label) => label.startsWith("status:"));
324
312
  if (statusLabels.length === 0) {
325
- return null;
313
+ return shared.passResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} has no status labels`);
326
314
  }
327
315
  return shared.failResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} retains status labels: ${statusLabels.join(", ")}`, "check_failed");
328
316
  }
329
317
  function checkCommentMarker(context, remoteData, shared) {
330
- if (!context.marker) {
331
- return null;
318
+ if (!context.audit.enabled || !context.marker) {
319
+ return infoResult('Issue comment-marker audit is not applicable because no marker is configured');
332
320
  }
333
321
  const comment = findCommentByMarker(remoteData.comments, context.marker);
334
322
  if (comment) {
335
- return null;
323
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
336
324
  }
337
325
  return shared.failResult(CHECK_TYPE, `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`, "check_failed");
338
326
  }
339
327
  function checkPrCommentMarker(context, remoteData, shared) {
340
- if (!context.prMarker) {
341
- return null;
328
+ if (!context.audit.enabled || !context.prMarker) {
329
+ return infoResult('PR comment-marker audit is not applicable because no marker is configured');
342
330
  }
343
331
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
344
332
  if (comment) {
345
- return null;
333
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected comment marker`);
346
334
  }
347
335
  return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
348
336
  }
349
337
  function checkPrCommentLastCommit(context, remoteData, shared) {
350
- if (!context.config.verify_pr_comment_last_commit_matches_head) {
351
- return null;
338
+ if (!context.audit.enabled) {
339
+ return infoResult('PR last-commit audit is not enabled for this workflow');
352
340
  }
353
341
  if (!context.prMarker) {
354
- return shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed");
342
+ return hardResult(shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed"));
355
343
  }
356
344
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
357
345
  if (!comment) {
358
- return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
346
+ return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
359
347
  }
360
348
  const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
361
349
  if (!match) {
362
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed");
350
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed"));
363
351
  }
364
352
  const expectedHead = String(remoteData.prHeadSha || "").trim();
365
353
  if (!expectedHead)
366
- return shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
354
+ return hardResult(shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error"));
367
355
  const actualHead = match[1].trim();
368
356
  if (expectedHead === actualHead) {
369
- return null;
357
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit matches HEAD`);
370
358
  }
371
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed");
359
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed"));
372
360
  }
373
361
  function checkPrCommentRequiredPatterns(context, remoteData, shared) {
374
- const patterns = context.config.expected_pr_comment_required_patterns || [];
362
+ const patterns = context.audit.enabled ? context.config.expected_pr_comment_required_patterns || [] : [];
375
363
  if (!Array.isArray(patterns) || patterns.length === 0) {
376
- return null;
364
+ return infoResult('PR comment-content audit is not enabled for this workflow');
377
365
  }
378
366
  if (!context.prMarker) {
379
- return shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed");
367
+ return hardResult(shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed"));
380
368
  }
381
369
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
382
370
  if (!comment) {
383
- return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
371
+ return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
384
372
  }
385
373
  const body = String(comment.body || "");
386
374
  for (const pattern of patterns) {
387
375
  const regex = new RegExp(pattern, "m");
388
376
  if (!regex.test(body)) {
389
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed");
377
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed"));
390
378
  }
391
379
  }
392
- return null;
380
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment has all required patterns`);
393
381
  }
394
382
  function checkCommentContent(context, remoteData, shared) {
395
- if (!context.config.verify_comment_content) {
396
- return null;
383
+ if (!context.audit.enabled) {
384
+ return infoResult('Artifact comment-content audit is not enabled for this workflow');
397
385
  }
398
386
  if (!context.marker) {
399
- return shared.failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
387
+ return hardResult(shared.failResult(CHECK_TYPE, "Artifact comment-content audit requires an expected comment marker", "check_failed"));
400
388
  }
401
389
  if (!context.artifactPath || !shared.safeStat(context.artifactPath)) {
402
- return shared.failResult(CHECK_TYPE, `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`, "check_failed");
390
+ return hardResult(shared.failResult(CHECK_TYPE, `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`, "check_failed"));
403
391
  }
404
392
  const comment = findCommentByMarker(remoteData.comments, context.marker);
405
393
  let localContent;
@@ -408,61 +396,64 @@ function checkCommentContent(context, remoteData, shared) {
408
396
  const localCanonical = canonicalizeCommentBody(fs.readFileSync(context.artifactPath, "utf8"));
409
397
  const commentCanonical = canonicalizeCommentBody(extractCommentBody(comment?.body || ""));
410
398
  if (!localCanonical.ok) {
411
- return shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`, "check_failed");
399
+ return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`, "check_failed"));
412
400
  }
413
401
  if (!commentCanonical.ok) {
414
- return shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`, "check_failed");
402
+ return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`, "check_failed"));
415
403
  }
416
404
  localContent = shared.normalizeContent(localCanonical.value);
417
405
  commentContent = shared.normalizeContent(commentCanonical.value);
418
406
  }
419
407
  catch (error) {
420
- return shared.failResult(CHECK_TYPE, `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`, "check_failed");
408
+ return hardResult(shared.failResult(CHECK_TYPE, `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`, "check_failed"));
421
409
  }
422
410
  if (localContent === commentContent) {
423
- return null;
411
+ return shared.passResult(CHECK_TYPE, `Artifact comment content matches Issue #${context.issueNumber}`);
424
412
  }
425
413
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage(path.basename(context.artifactPath, path.extname(context.artifactPath)), context.issueNumber, localContent, commentContent), "check_failed");
426
414
  }
427
415
  function checkTaskCommentContent(context, remoteData, shared) {
428
- if (!context.config.verify_task_comment_content) {
429
- return null;
430
- }
431
- if (isTaskCommentTooLarge(context.task.content)) {
432
- return null;
416
+ if (!context.audit.enabled) {
417
+ return infoResult('Task comment-content audit is not enabled for this workflow');
433
418
  }
434
419
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
435
420
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
436
421
  if (!comment) {
437
422
  return shared.failResult(CHECK_TYPE, `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`, "check_failed");
438
423
  }
439
- const expectedTaskBody = buildExpectedTaskBody(context.task.content, shared);
440
- if (expectedTaskBody === null) {
441
- return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
424
+ let renderedTask;
425
+ try {
426
+ renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
427
+ }
428
+ catch {
429
+ return hardResult(shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed"));
430
+ }
431
+ if (renderedTask.byteLength > 60_000) {
432
+ return hardResult(shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed"));
442
433
  }
443
- const expectedBody = shared.normalizeContent(expectedTaskBody);
434
+ const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
444
435
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
445
436
  if (expectedBody === commentBody) {
446
- return null;
437
+ return shared.passResult(CHECK_TYPE, `Task comment content matches Issue #${context.issueNumber}`);
447
438
  }
448
439
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
449
440
  }
450
441
  function checkPrTypeLabel(context, remoteData, shared) {
451
- if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
452
- return null;
442
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
443
+ return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
453
444
  }
454
445
  const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
455
446
  if (!expectedLabel) {
456
- return null;
447
+ return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
457
448
  }
458
449
  if (remoteData.prLabels.includes(expectedLabel)) {
459
- return null;
450
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
460
451
  }
461
452
  return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`, "check_failed");
462
453
  }
463
454
  function checkInLabelsMatchPr(context, remoteData, shared) {
464
- if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
465
- return null;
455
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
456
+ return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
466
457
  }
467
458
  const issueInLabels = extractLabelNames(remoteData.issue.labels)
468
459
  .filter((label) => label.startsWith("in:"))
@@ -471,13 +462,13 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
471
462
  .filter((label) => label.startsWith("in:"))
472
463
  .sort();
473
464
  if (arraysEqual(issueInLabels, prInLabels)) {
474
- return null;
465
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
475
466
  }
476
467
  return shared.failResult(CHECK_TYPE, `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
477
468
  }
478
469
  function checkInLabelsComputed(context, remoteData, shared) {
479
- if (!context.config.verify_in_labels_computed || !context.hasTriage) {
480
- return null;
470
+ if (!context.audit.enabled || !context.hasTriage) {
471
+ return infoResult('Computed in: label audit is not applicable because it is disabled or triage capability is unavailable');
481
472
  }
482
473
  const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
483
474
  if (!expectedInLabels.ok) {
@@ -486,49 +477,49 @@ function checkInLabelsComputed(context, remoteData, shared) {
486
477
  : shared.blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
487
478
  }
488
479
  if (expectedInLabels.mode === "skipped") {
489
- return null;
480
+ return infoResult('Computed in: label audit is not applicable because no mapped changed path exists');
490
481
  }
491
482
  const actualInLabels = extractLabelNames(remoteData.issue.labels)
492
483
  .filter((label) => label.startsWith("in:"))
493
484
  .sort();
494
485
  if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
495
- return null;
486
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels match committed changes`);
496
487
  }
497
488
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels do not match committed changes: expected [${formatLabelList(expectedInLabels.labels)}], got [${formatLabelList(actualInLabels)}]`, "check_failed");
498
489
  }
499
490
  function checkSyncedRequirements(context, remoteData, shared) {
500
- if (!context.config.sync_checked_requirements || !context.hasTriage) {
501
- return null;
491
+ if (!context.audit.enabled || !context.hasTriage) {
492
+ return infoResult('Requirements audit is not applicable because it is disabled or triage capability is unavailable');
502
493
  }
503
494
  const checkedRequirements = shared.getCheckedRequirements(context.task.content);
504
495
  if (checkedRequirements.length === 0) {
505
- return null;
496
+ return infoResult('Requirements audit is not applicable because the task has no checked requirements');
506
497
  }
507
498
  const issueBody = remoteData.issue.body || "";
508
499
  const resolution = resolveRequirementSection(issueBody, requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task"));
509
500
  if (resolution.status === "missing") {
510
- return null;
501
+ return infoResult('Requirements audit is not applicable because the Issue has no requirements section');
511
502
  }
512
503
  if (resolution.status === "ambiguous") {
513
- return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed");
504
+ return hardResult(shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed"));
514
505
  }
515
506
  const requirementBody = issueBody.slice(resolution.bodyStart, resolution.bodyEnd);
516
507
  const missingRequirements = checkedRequirements.filter((item) => !hasCheckedRequirement(requirementBody, item));
517
508
  if (missingRequirements.length === 0) {
518
- return null;
509
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} contains all checked requirements`);
519
510
  }
520
511
  return shared.failResult(CHECK_TYPE, `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`, "check_failed");
521
512
  }
522
513
  function checkIssueType(context, remoteData, shared) {
523
- if (!context.config.verify_issue_type || !context.hasPush) {
524
- return null;
514
+ if (!context.hasPush) {
515
+ return infoResult('Issue Type audit is not applicable because push capability is unavailable');
525
516
  }
526
517
  if (remoteData.issueType === undefined) {
527
- return null;
518
+ return infoResult('Issue Type audit is not applicable because the provider does not expose Issue Type');
528
519
  }
529
520
  if (!remoteData.issueType) {
530
521
  if (context.repoOwnerType === "User") {
531
- return null;
522
+ return infoResult('Issue Type audit is not applicable for a user-owned repository');
532
523
  }
533
524
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no Issue Type set`, "check_failed");
534
525
  }
@@ -536,14 +527,14 @@ function checkIssueType(context, remoteData, shared) {
536
527
  if (expectedType && remoteData.issueType !== expectedType) {
537
528
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`, "check_failed");
538
529
  }
539
- return null;
530
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has the expected Issue Type`);
540
531
  }
541
532
  function checkIssueFields(context, remoteData, shared) {
542
- if (!context.config.verify_issue_fields || !context.hasPush) {
543
- return null;
533
+ if (!context.hasPush) {
534
+ return infoResult('Issue field audit is not applicable because push capability is unavailable');
544
535
  }
545
536
  if (remoteData.issueFields === undefined) {
546
- return null;
537
+ return infoResult('Issue field audit is not applicable because the provider does not expose Issue fields');
547
538
  }
548
539
  for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
549
540
  const expectedRaw = context.task.metadata[metadataKey];
@@ -562,20 +553,20 @@ function checkIssueFields(context, remoteData, shared) {
562
553
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`, "check_failed");
563
554
  }
564
555
  }
565
- return null;
556
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
566
557
  }
567
558
  function checkPrAssignee(context, remoteData, shared) {
568
- if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
569
- return null;
559
+ if (!context.audit.enabled || !context.hasPush || !context.prNumber) {
560
+ return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
570
561
  }
571
562
  if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
572
563
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no assignee`, "check_failed");
573
564
  }
574
- return null;
565
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
575
566
  }
576
567
  function checkMilestone(context, remoteData, shared) {
577
- if (!context.config.verify_milestone || !context.hasTriage) {
578
- return null;
568
+ if (!context.hasTriage) {
569
+ return infoResult('Milestone audit is not applicable because triage capability is unavailable');
579
570
  }
580
571
  if (!remoteData.issue?.milestone?.title) {
581
572
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
@@ -583,7 +574,7 @@ function checkMilestone(context, remoteData, shared) {
583
574
  if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
584
575
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no milestone set`, "check_failed");
585
576
  }
586
- if (context.config.verify_milestone_specific) {
577
+ if (context.audit.requireSpecificMilestone) {
587
578
  const issueTitle = remoteData.issue.milestone.title;
588
579
  if (VERSION_LINE_REGEX.test(issueTitle)) {
589
580
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`, "check_failed");
@@ -592,7 +583,7 @@ function checkMilestone(context, remoteData, shared) {
592
583
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`, "check_failed");
593
584
  }
594
585
  }
595
- return null;
586
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
596
587
  }
597
588
  function findCommentByMarker(comments, marker) {
598
589
  return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
@@ -612,12 +603,21 @@ function extractCommentBody(commentBody) {
612
603
  while (start < lines.length && lines[start].trim() === "") {
613
604
  start += 1;
614
605
  }
615
- if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
606
+ if (start < lines.length && (/^> \*\*.+\*\* · .+$/.test(lines[start].trim()) || /^> 任务同步 · .+$/.test(lines[start].trim()))) {
616
607
  start += 1;
617
608
  }
618
609
  while (start < lines.length && lines[start].trim() === "") {
619
610
  start += 1;
620
611
  }
612
+ if (lines[start]?.trim() === "<details><summary>恢复元数据</summary>") {
613
+ const metadataEnd = lines.indexOf("</details>", start + 1);
614
+ if (metadataEnd >= 0) {
615
+ start = metadataEnd + 1;
616
+ while (start < lines.length && lines[start].trim() === "") {
617
+ start += 1;
618
+ }
619
+ }
620
+ }
621
621
  let end = lines.length;
622
622
  for (let index = lines.length - 1; index >= start; index -= 1) {
623
623
  const trimmed = lines[index].trim();
@@ -789,7 +789,9 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
789
789
  if (!baseRef) {
790
790
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
791
791
  }
792
- const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], taskDir);
792
+ const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
793
+ const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
794
+ const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
793
795
  if (!changedFilesResult.ok) {
794
796
  return { ...changedFilesResult, type: "network_error" };
795
797
  }
@@ -812,6 +814,26 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
812
814
  }
813
815
  return { ok: true, labels: planned.target, mode: "mapped" };
814
816
  }
817
+ function worktreeForBranch(repositoryRoot, branch) {
818
+ try {
819
+ const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
820
+ cwd: repositoryRoot,
821
+ encoding: "utf8",
822
+ stdio: ["pipe", "pipe", "pipe"]
823
+ }).trim().split(/\r?\n\r?\n/);
824
+ const expectedRef = `refs/heads/${branch}`;
825
+ for (const record of records) {
826
+ const worktree = /^worktree (.+)$/m.exec(record)?.[1];
827
+ const branchRef = /^branch (.+)$/m.exec(record)?.[1];
828
+ if (worktree && branchRef === expectedRef)
829
+ return worktree;
830
+ }
831
+ }
832
+ catch {
833
+ // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
834
+ }
835
+ return null;
836
+ }
815
837
  function loadInLabelMapping(shared) {
816
838
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
817
839
  if (!fs.existsSync(configPath)) {