@fitlab-ai/agent-infra 0.11.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 (80) hide show
  1. package/dist/lib/internal/manual-validation.js +6 -2
  2. package/dist/lib/platform/issues.js +3 -4
  3. package/dist/lib/platform/verification-sync.js +142 -154
  4. package/dist/lib/sandbox/control/workflow-executor.js +7 -1
  5. package/dist/lib/task/artifact-lifecycle.js +15 -52
  6. package/dist/lib/task/artifact-operations.js +27 -1
  7. package/dist/lib/task/capabilities.js +0 -2
  8. package/dist/lib/task/events.js +60 -43
  9. package/dist/lib/task/gate-policy.js +94 -0
  10. package/dist/lib/task/local-artifact-finalization.js +7 -4
  11. package/dist/lib/task/orchestration.js +8 -6
  12. package/dist/lib/task/review-artifacts.js +12 -6
  13. package/dist/lib/task/review-finalization.js +3 -9
  14. package/dist/lib/task/verification-engine.js +28 -17
  15. package/dist/lib/task/verification.js +25 -9
  16. package/dist/lifecycle-build-manifest.json +3 -3
  17. package/lib/internal/manual-validation.ts +6 -2
  18. package/lib/platform/issues.ts +3 -4
  19. package/lib/platform/verification-sync.ts +159 -177
  20. package/lib/sandbox/control/workflow-executor.ts +11 -1
  21. package/lib/task/artifact-lifecycle.ts +12 -49
  22. package/lib/task/artifact-operations.ts +27 -1
  23. package/lib/task/capabilities.ts +0 -1
  24. package/lib/task/events.ts +49 -32
  25. package/lib/task/gate-policy.ts +133 -0
  26. package/lib/task/local-artifact-finalization.ts +6 -4
  27. package/lib/task/orchestration.ts +12 -7
  28. package/lib/task/review-artifacts.ts +8 -2
  29. package/lib/task/review-finalization.ts +3 -9
  30. package/lib/task/verification-engine.ts +24 -14
  31. package/lib/task/verification-types.ts +7 -0
  32. package/lib/task/verification.ts +25 -9
  33. package/package.json +1 -1
  34. package/templates/.agents/rules/issue-sync.en.md +2 -0
  35. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  36. package/templates/.agents/rules/local-artifact-repair.en.md +11 -3
  37. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +11 -3
  38. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  39. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  40. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  41. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  42. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  43. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  44. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  45. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  46. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  47. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  48. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  49. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  50. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  51. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  52. package/templates/.agents/skills/commit/config/verify.json +1 -5
  53. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  54. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  55. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  56. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  57. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  58. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  59. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  60. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  61. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  62. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  63. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  64. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  65. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  66. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  67. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  68. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  69. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  70. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  71. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  72. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  73. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  74. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  75. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  76. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  77. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  78. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  79. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  80. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
@@ -23,6 +23,7 @@ function printFailure(format, error) {
23
23
  process.exitCode = 1;
24
24
  }
25
25
  const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
26
+ const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
26
27
  function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
27
28
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
28
29
  const section = phase === 'pending'
@@ -33,11 +34,14 @@ function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evide
33
34
  ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
34
35
  : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
35
36
  let inserted = false;
36
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
37
+ const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
37
38
  if (inserted)
38
39
  return '';
39
40
  inserted = true;
40
- return `${section}\n\n`;
41
+ const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
42
+ return trailingPlaceholder
43
+ ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
44
+ : `${section}\n\n`;
41
45
  });
42
46
  return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
43
47
  }
@@ -538,11 +538,10 @@ async function syncPlatformIssue(taskRef, options) {
538
538
  if (failure)
539
539
  return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
540
540
  const planned = plan.operations.filter((operation) => operation.status === 'planned');
541
- const skipped = plan.operations.some((operation) => operation.status === 'skipped');
542
541
  if (options.dryRun)
543
- return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
542
+ return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
543
  if (planned.length === 0)
545
- return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
+ return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
546
545
  const payload = applyRestOperations(snapshot, planned);
547
546
  const updated = base.provider.issues?.update
548
547
  ? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
@@ -577,7 +576,7 @@ async function syncPlatformIssue(taskRef, options) {
577
576
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
578
577
  });
579
578
  }
580
- return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
579
+ return result('applied', base.resolved.taskId, base.issueNumber, {
581
580
  changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
582
581
  resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
583
582
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
@@ -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";
@@ -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,25 +396,25 @@ 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;
416
+ if (!context.audit.enabled) {
417
+ return infoResult('Task comment-content audit is not enabled for this workflow');
430
418
  }
431
419
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
432
420
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
@@ -438,34 +426,34 @@ function checkTaskCommentContent(context, remoteData, shared) {
438
426
  renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
439
427
  }
440
428
  catch {
441
- return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
429
+ return hardResult(shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed"));
442
430
  }
443
431
  if (renderedTask.byteLength > 60_000) {
444
- return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
432
+ return hardResult(shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed"));
445
433
  }
446
434
  const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
447
435
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
448
436
  if (expectedBody === commentBody) {
449
- return null;
437
+ return shared.passResult(CHECK_TYPE, `Task comment content matches Issue #${context.issueNumber}`);
450
438
  }
451
439
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
452
440
  }
453
441
  function checkPrTypeLabel(context, remoteData, shared) {
454
- if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
455
- 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');
456
444
  }
457
445
  const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
458
446
  if (!expectedLabel) {
459
- return null;
447
+ return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
460
448
  }
461
449
  if (remoteData.prLabels.includes(expectedLabel)) {
462
- return null;
450
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
463
451
  }
464
452
  return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`, "check_failed");
465
453
  }
466
454
  function checkInLabelsMatchPr(context, remoteData, shared) {
467
- if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
468
- 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');
469
457
  }
470
458
  const issueInLabels = extractLabelNames(remoteData.issue.labels)
471
459
  .filter((label) => label.startsWith("in:"))
@@ -474,13 +462,13 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
474
462
  .filter((label) => label.startsWith("in:"))
475
463
  .sort();
476
464
  if (arraysEqual(issueInLabels, prInLabels)) {
477
- return null;
465
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
478
466
  }
479
467
  return shared.failResult(CHECK_TYPE, `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
480
468
  }
481
469
  function checkInLabelsComputed(context, remoteData, shared) {
482
- if (!context.config.verify_in_labels_computed || !context.hasTriage) {
483
- 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');
484
472
  }
485
473
  const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
486
474
  if (!expectedInLabels.ok) {
@@ -489,49 +477,49 @@ function checkInLabelsComputed(context, remoteData, shared) {
489
477
  : shared.blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
490
478
  }
491
479
  if (expectedInLabels.mode === "skipped") {
492
- return null;
480
+ return infoResult('Computed in: label audit is not applicable because no mapped changed path exists');
493
481
  }
494
482
  const actualInLabels = extractLabelNames(remoteData.issue.labels)
495
483
  .filter((label) => label.startsWith("in:"))
496
484
  .sort();
497
485
  if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
498
- return null;
486
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels match committed changes`);
499
487
  }
500
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");
501
489
  }
502
490
  function checkSyncedRequirements(context, remoteData, shared) {
503
- if (!context.config.sync_checked_requirements || !context.hasTriage) {
504
- 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');
505
493
  }
506
494
  const checkedRequirements = shared.getCheckedRequirements(context.task.content);
507
495
  if (checkedRequirements.length === 0) {
508
- return null;
496
+ return infoResult('Requirements audit is not applicable because the task has no checked requirements');
509
497
  }
510
498
  const issueBody = remoteData.issue.body || "";
511
499
  const resolution = resolveRequirementSection(issueBody, requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task"));
512
500
  if (resolution.status === "missing") {
513
- return null;
501
+ return infoResult('Requirements audit is not applicable because the Issue has no requirements section');
514
502
  }
515
503
  if (resolution.status === "ambiguous") {
516
- 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"));
517
505
  }
518
506
  const requirementBody = issueBody.slice(resolution.bodyStart, resolution.bodyEnd);
519
507
  const missingRequirements = checkedRequirements.filter((item) => !hasCheckedRequirement(requirementBody, item));
520
508
  if (missingRequirements.length === 0) {
521
- return null;
509
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} contains all checked requirements`);
522
510
  }
523
511
  return shared.failResult(CHECK_TYPE, `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`, "check_failed");
524
512
  }
525
513
  function checkIssueType(context, remoteData, shared) {
526
- if (!context.config.verify_issue_type || !context.hasPush) {
527
- return null;
514
+ if (!context.hasPush) {
515
+ return infoResult('Issue Type audit is not applicable because push capability is unavailable');
528
516
  }
529
517
  if (remoteData.issueType === undefined) {
530
- return null;
518
+ return infoResult('Issue Type audit is not applicable because the provider does not expose Issue Type');
531
519
  }
532
520
  if (!remoteData.issueType) {
533
521
  if (context.repoOwnerType === "User") {
534
- return null;
522
+ return infoResult('Issue Type audit is not applicable for a user-owned repository');
535
523
  }
536
524
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no Issue Type set`, "check_failed");
537
525
  }
@@ -539,14 +527,14 @@ function checkIssueType(context, remoteData, shared) {
539
527
  if (expectedType && remoteData.issueType !== expectedType) {
540
528
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`, "check_failed");
541
529
  }
542
- return null;
530
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has the expected Issue Type`);
543
531
  }
544
532
  function checkIssueFields(context, remoteData, shared) {
545
- if (!context.config.verify_issue_fields || !context.hasPush) {
546
- return null;
533
+ if (!context.hasPush) {
534
+ return infoResult('Issue field audit is not applicable because push capability is unavailable');
547
535
  }
548
536
  if (remoteData.issueFields === undefined) {
549
- return null;
537
+ return infoResult('Issue field audit is not applicable because the provider does not expose Issue fields');
550
538
  }
551
539
  for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
552
540
  const expectedRaw = context.task.metadata[metadataKey];
@@ -565,20 +553,20 @@ function checkIssueFields(context, remoteData, shared) {
565
553
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`, "check_failed");
566
554
  }
567
555
  }
568
- return null;
556
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
569
557
  }
570
558
  function checkPrAssignee(context, remoteData, shared) {
571
- if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
572
- 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');
573
561
  }
574
562
  if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
575
563
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no assignee`, "check_failed");
576
564
  }
577
- return null;
565
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
578
566
  }
579
567
  function checkMilestone(context, remoteData, shared) {
580
- if (!context.config.verify_milestone || !context.hasTriage) {
581
- return null;
568
+ if (!context.hasTriage) {
569
+ return infoResult('Milestone audit is not applicable because triage capability is unavailable');
582
570
  }
583
571
  if (!remoteData.issue?.milestone?.title) {
584
572
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
@@ -586,7 +574,7 @@ function checkMilestone(context, remoteData, shared) {
586
574
  if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
587
575
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no milestone set`, "check_failed");
588
576
  }
589
- if (context.config.verify_milestone_specific) {
577
+ if (context.audit.requireSpecificMilestone) {
590
578
  const issueTitle = remoteData.issue.milestone.title;
591
579
  if (VERSION_LINE_REGEX.test(issueTitle)) {
592
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");
@@ -595,7 +583,7 @@ function checkMilestone(context, remoteData, shared) {
595
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");
596
584
  }
597
585
  }
598
- return null;
586
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
599
587
  }
600
588
  function findCommentByMarker(comments, marker) {
601
589
  return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;