@fitlab-ai/agent-infra 0.11.0 → 0.11.2

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 (146) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/manual-validation.js +6 -2
  3. package/dist/lib/internal/platform-comment.js +59 -0
  4. package/dist/lib/internal/sandbox-control.js +4 -131
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-operation-registry.js +1 -0
  7. package/dist/lib/internal/task-review.js +5 -5
  8. package/dist/lib/internal/task-validate.js +0 -6
  9. package/dist/lib/platform/comment-safety.js +7 -1
  10. package/dist/lib/platform/github-provider.js +13 -3
  11. package/dist/lib/platform/issue-comments.js +120 -13
  12. package/dist/lib/platform/issues.js +3 -4
  13. package/dist/lib/platform/provider-contract.d.ts +2 -0
  14. package/dist/lib/platform/provider-validation.js +7 -2
  15. package/dist/lib/platform/verification-sync.js +161 -156
  16. package/dist/lib/sandbox/control/executor.js +0 -38
  17. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  18. package/dist/lib/sandbox/control/task-view.js +3 -1
  19. package/dist/lib/sandbox/control/workflow-executor.js +32 -41
  20. package/dist/lib/task/artifact-command.js +6 -6
  21. package/dist/lib/task/artifact-lifecycle.js +15 -52
  22. package/dist/lib/task/artifact-operations.js +27 -1
  23. package/dist/lib/task/capabilities.js +0 -2
  24. package/dist/lib/task/codex-orchestration.js +10 -208
  25. package/dist/lib/task/commit-operation.js +3 -18
  26. package/dist/lib/task/control-authority.js +1 -8
  27. package/dist/lib/task/delegation-receipts.js +14 -76
  28. package/dist/lib/task/events.js +147 -278
  29. package/dist/lib/task/finalization-state.js +2 -1
  30. package/dist/lib/task/finalization.js +273 -49
  31. package/dist/lib/task/gate-policy.js +94 -0
  32. package/dist/lib/task/lifecycle-execution.js +2 -11
  33. package/dist/lib/task/local-artifact-finalization.js +31 -245
  34. package/dist/lib/task/orchestration.js +8 -24
  35. package/dist/lib/task/review-artifacts.js +12 -6
  36. package/dist/lib/task/review-command.js +0 -1
  37. package/dist/lib/task/review-finalization.js +47 -218
  38. package/dist/lib/task/verification-engine.js +28 -17
  39. package/dist/lib/task/verification.js +25 -9
  40. package/dist/lib/task/workflow-command.js +4 -1
  41. package/dist/lib/task/workflow-dispatch.js +55 -3
  42. package/dist/lifecycle-build-manifest.json +3 -3
  43. package/lib/internal/cli-route-inventory.ts +1 -1
  44. package/lib/internal/manual-validation.ts +6 -2
  45. package/lib/internal/platform-comment.ts +38 -0
  46. package/lib/internal/sandbox-control.ts +3 -145
  47. package/lib/internal/task-artifact.ts +1 -1
  48. package/lib/internal/task-operation-registry.ts +1 -0
  49. package/lib/internal/task-review.ts +5 -5
  50. package/lib/internal/task-validate.ts +0 -5
  51. package/lib/platform/comment-safety.ts +8 -0
  52. package/lib/platform/github-provider.ts +11 -2
  53. package/lib/platform/issue-comments.ts +132 -13
  54. package/lib/platform/issues.ts +3 -4
  55. package/lib/platform/provider-contract.ts +2 -0
  56. package/lib/platform/provider-validation.ts +6 -2
  57. package/lib/platform/verification-sync.ts +178 -179
  58. package/lib/sandbox/control/executor.ts +0 -44
  59. package/lib/sandbox/control/finalization-response.ts +10 -2
  60. package/lib/sandbox/control/task-view.ts +3 -1
  61. package/lib/sandbox/control/workflow-executor.ts +31 -48
  62. package/lib/task/artifact-command.ts +6 -7
  63. package/lib/task/artifact-lifecycle.ts +12 -49
  64. package/lib/task/artifact-operations.ts +27 -1
  65. package/lib/task/capabilities.ts +0 -1
  66. package/lib/task/codex-orchestration.ts +8 -241
  67. package/lib/task/commit-operation.ts +3 -23
  68. package/lib/task/control-authority.ts +1 -8
  69. package/lib/task/delegation-receipts.ts +12 -79
  70. package/lib/task/events.ts +155 -287
  71. package/lib/task/finalization-state.ts +2 -1
  72. package/lib/task/finalization.ts +277 -57
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/lifecycle-execution.ts +10 -21
  75. package/lib/task/local-artifact-finalization.ts +29 -285
  76. package/lib/task/orchestration.ts +12 -41
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +0 -1
  79. package/lib/task/review-finalization.ts +46 -200
  80. package/lib/task/verification-engine.ts +24 -14
  81. package/lib/task/verification-types.ts +7 -0
  82. package/lib/task/verification.ts +25 -9
  83. package/lib/task/workflow-command.ts +4 -1
  84. package/lib/task/workflow-dispatch.ts +46 -3
  85. package/package.json +2 -2
  86. package/templates/.agents/rules/issue-sync.en.md +2 -0
  87. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  88. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  89. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  90. package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
  91. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
  92. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  93. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  94. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  95. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  96. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  97. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  98. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  101. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  102. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  105. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  106. package/templates/.agents/skills/commit/config/verify.json +1 -5
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  108. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  109. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  110. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  111. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  112. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  113. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  114. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  115. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  116. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  117. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  118. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  119. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  120. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  121. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  122. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  123. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  124. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  125. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  129. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  133. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  134. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  135. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  136. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  137. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  139. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  140. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  141. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  142. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  143. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  144. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  145. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  146. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -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 { renderTaskCommentResult } from "./issue-comments.js";
14
+ import { MARKERS, 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") };
@@ -271,7 +258,9 @@ async function fetchRemoteData(context, shared) {
271
258
  }
272
259
  return {
273
260
  issue,
274
- comments: facts.value.comments.map((comment) => ({ id: comment.id, body: comment.body })),
261
+ comments: facts.value.comments.map((comment) => ({
262
+ id: comment.id, body: comment.body, createdSequence: comment.createdSequence
263
+ })),
275
264
  prComments,
276
265
  prLabels: changeRequest?.labels || null,
277
266
  issueType: issueSnapshot
@@ -290,116 +279,132 @@ async function fetchRemoteData(context, shared) {
290
279
  function mapTaskTypeToLabel(taskType) {
291
280
  return taskTypeLabel(taskType);
292
281
  }
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);
282
+ function hasEnabledAudit(context, auditIds) {
283
+ return auditIds
284
+ .some((id) => platformAuditPolicy(id, context).enabled);
285
+ }
286
+ function shouldFetchIssueComments(context) {
287
+ return hasEnabledAudit(context, ['comment-marker', 'comment-content', 'task-comment-content']);
288
+ }
289
+ function shouldFetchPrComments(context) {
290
+ return hasEnabledAudit(context, ['pr-comment-marker', 'pr-comment-last-commit', 'pr-comment-content']);
301
291
  }
302
292
  function checkStatusLabel(context, remoteData, shared) {
303
293
  if (!context.expectedStatusLabel || !context.hasTriage) {
304
- return null;
294
+ return infoResult('Status label audit is not applicable because no expected label or triage capability is available');
305
295
  }
306
296
  if (String(remoteData.issue.state || "").toUpperCase() !== "OPEN") {
307
- return null;
297
+ return infoResult('Status label audit is not applicable because the Issue is closed');
308
298
  }
309
299
  const labels = extractLabelNames(remoteData.issue.labels);
310
300
  if (labels.includes(context.expectedStatusLabel)) {
311
- return null;
301
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected status label '${context.expectedStatusLabel}'`);
312
302
  }
313
303
  return shared.failResult(CHECK_TYPE, `Expected label '${context.expectedStatusLabel}' not found on Issue #${context.issueNumber}`, "check_failed");
314
304
  }
315
305
  function checkClosedIssueStatusLabels(context, remoteData, shared) {
316
- if (!context.config.verify_closed_issue_has_no_status_labels) {
317
- return null;
306
+ if (!context.audit.enabled) {
307
+ return infoResult('Closed Issue status-label audit is not enabled for this workflow');
318
308
  }
319
309
  if (String(remoteData.issue.state || "").toUpperCase() !== "CLOSED") {
320
- return null;
310
+ return infoResult('Closed Issue status-label audit is not applicable because the Issue is open');
321
311
  }
322
312
  const statusLabels = extractLabelNames(remoteData.issue.labels)
323
313
  .filter((label) => label.startsWith("status:"));
324
314
  if (statusLabels.length === 0) {
325
- return null;
315
+ return shared.passResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} has no status labels`);
326
316
  }
327
317
  return shared.failResult(CHECK_TYPE, `Closed Issue #${context.issueNumber} retains status labels: ${statusLabels.join(", ")}`, "check_failed");
328
318
  }
329
319
  function checkCommentMarker(context, remoteData, shared) {
330
- if (!context.marker) {
331
- return null;
320
+ if (!context.audit.enabled || !context.marker) {
321
+ return infoResult('Issue comment-marker audit is not applicable because no marker is configured');
332
322
  }
333
323
  const comment = findCommentByMarker(remoteData.comments, context.marker);
334
324
  if (comment) {
335
- return null;
325
+ if (context.marker === MARKERS.summary(context.task.metadata.id)) {
326
+ const managed = remoteData.comments.filter((candidate) => {
327
+ const first = String(candidate.body || '').replace(/\r\n/g, '\n').split('\n', 1)[0] || '';
328
+ return first === MARKERS.task(context.task.metadata.id)
329
+ || first === MARKERS.summary(context.task.metadata.id)
330
+ || first.startsWith(`<!-- sync-issue:${context.task.metadata.id}:`);
331
+ });
332
+ if (managed.some((candidate) => !Number.isSafeInteger(candidate.createdSequence) || candidate.createdSequence < 1)) {
333
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment ordering is not provable`, "check_failed");
334
+ }
335
+ const summarySequence = comment.createdSequence;
336
+ if (!managed.every((candidate) => summarySequence >= candidate.createdSequence)) {
337
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment is not last among task-managed comments`, "check_failed");
338
+ }
339
+ }
340
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
336
341
  }
337
342
  return shared.failResult(CHECK_TYPE, `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`, "check_failed");
338
343
  }
339
344
  function checkPrCommentMarker(context, remoteData, shared) {
340
- if (!context.prMarker) {
341
- return null;
345
+ if (!context.audit.enabled || !context.prMarker) {
346
+ return infoResult('PR comment-marker audit is not applicable because no marker is configured');
342
347
  }
343
348
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
344
349
  if (comment) {
345
- return null;
350
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected comment marker`);
346
351
  }
347
352
  return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
348
353
  }
349
354
  function checkPrCommentLastCommit(context, remoteData, shared) {
350
- if (!context.config.verify_pr_comment_last_commit_matches_head) {
351
- return null;
355
+ if (!context.audit.enabled) {
356
+ return infoResult('PR last-commit audit is not enabled for this workflow');
352
357
  }
353
358
  if (!context.prMarker) {
354
- return shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed");
359
+ return hardResult(shared.failResult(CHECK_TYPE, "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker", "check_failed"));
355
360
  }
356
361
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
357
362
  if (!comment) {
358
- return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
363
+ return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
359
364
  }
360
365
  const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
361
366
  if (!match) {
362
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed");
367
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`, "check_failed"));
363
368
  }
364
369
  const expectedHead = String(remoteData.prHeadSha || "").trim();
365
370
  if (!expectedHead)
366
- return shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
371
+ return hardResult(shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error"));
367
372
  const actualHead = match[1].trim();
368
373
  if (expectedHead === actualHead) {
369
- return null;
374
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit matches HEAD`);
370
375
  }
371
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed");
376
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`, "check_failed"));
372
377
  }
373
378
  function checkPrCommentRequiredPatterns(context, remoteData, shared) {
374
- const patterns = context.config.expected_pr_comment_required_patterns || [];
379
+ const patterns = context.audit.enabled ? context.config.expected_pr_comment_required_patterns || [] : [];
375
380
  if (!Array.isArray(patterns) || patterns.length === 0) {
376
- return null;
381
+ return infoResult('PR comment-content audit is not enabled for this workflow');
377
382
  }
378
383
  if (!context.prMarker) {
379
- return shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed");
384
+ return hardResult(shared.failResult(CHECK_TYPE, "expected_pr_comment_required_patterns requires expected_pr_comment_marker", "check_failed"));
380
385
  }
381
386
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
382
387
  if (!comment) {
383
- return shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed");
388
+ return hardResult(shared.failResult(CHECK_TYPE, `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`, "check_failed"));
384
389
  }
385
390
  const body = String(comment.body || "");
386
391
  for (const pattern of patterns) {
387
392
  const regex = new RegExp(pattern, "m");
388
393
  if (!regex.test(body)) {
389
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed");
394
+ return hardResult(shared.failResult(CHECK_TYPE, `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`, "check_failed"));
390
395
  }
391
396
  }
392
- return null;
397
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} summary comment has all required patterns`);
393
398
  }
394
399
  function checkCommentContent(context, remoteData, shared) {
395
- if (!context.config.verify_comment_content) {
396
- return null;
400
+ if (!context.audit.enabled) {
401
+ return infoResult('Artifact comment-content audit is not enabled for this workflow');
397
402
  }
398
403
  if (!context.marker) {
399
- return shared.failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
404
+ return hardResult(shared.failResult(CHECK_TYPE, "Artifact comment-content audit requires an expected comment marker", "check_failed"));
400
405
  }
401
406
  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");
407
+ return hardResult(shared.failResult(CHECK_TYPE, `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`, "check_failed"));
403
408
  }
404
409
  const comment = findCommentByMarker(remoteData.comments, context.marker);
405
410
  let localContent;
@@ -408,25 +413,25 @@ function checkCommentContent(context, remoteData, shared) {
408
413
  const localCanonical = canonicalizeCommentBody(fs.readFileSync(context.artifactPath, "utf8"));
409
414
  const commentCanonical = canonicalizeCommentBody(extractCommentBody(comment?.body || ""));
410
415
  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");
416
+ 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
417
  }
413
418
  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");
419
+ 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
420
  }
416
421
  localContent = shared.normalizeContent(localCanonical.value);
417
422
  commentContent = shared.normalizeContent(commentCanonical.value);
418
423
  }
419
424
  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");
425
+ 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
426
  }
422
427
  if (localContent === commentContent) {
423
- return null;
428
+ return shared.passResult(CHECK_TYPE, `Artifact comment content matches Issue #${context.issueNumber}`);
424
429
  }
425
430
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage(path.basename(context.artifactPath, path.extname(context.artifactPath)), context.issueNumber, localContent, commentContent), "check_failed");
426
431
  }
427
432
  function checkTaskCommentContent(context, remoteData, shared) {
428
- if (!context.config.verify_task_comment_content) {
429
- return null;
433
+ if (!context.audit.enabled) {
434
+ return infoResult('Task comment-content audit is not enabled for this workflow');
430
435
  }
431
436
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
432
437
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
@@ -438,34 +443,34 @@ function checkTaskCommentContent(context, remoteData, shared) {
438
443
  renderedTask = renderTaskCommentResult(context.task.content, context.task.metadata.id, loadProjectLanguage(shared));
439
444
  }
440
445
  catch {
441
- return shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed");
446
+ return hardResult(shared.failResult(CHECK_TYPE, "Task content cannot be rendered safely for comment verification", "check_failed"));
442
447
  }
443
448
  if (renderedTask.byteLength > 60_000) {
444
- return shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed");
449
+ return hardResult(shared.failResult(CHECK_TYPE, "Task comment exceeds the platform byte limit", "check_failed"));
445
450
  }
446
451
  const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
447
452
  const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
448
453
  if (expectedBody === commentBody) {
449
- return null;
454
+ return shared.passResult(CHECK_TYPE, `Task comment content matches Issue #${context.issueNumber}`);
450
455
  }
451
456
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
452
457
  }
453
458
  function checkPrTypeLabel(context, remoteData, shared) {
454
- if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
455
- return null;
459
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
460
+ return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
456
461
  }
457
462
  const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
458
463
  if (!expectedLabel) {
459
- return null;
464
+ return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
460
465
  }
461
466
  if (remoteData.prLabels.includes(expectedLabel)) {
462
- return null;
467
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
463
468
  }
464
469
  return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`, "check_failed");
465
470
  }
466
471
  function checkInLabelsMatchPr(context, remoteData, shared) {
467
- if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
468
- return null;
472
+ if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
473
+ return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
469
474
  }
470
475
  const issueInLabels = extractLabelNames(remoteData.issue.labels)
471
476
  .filter((label) => label.startsWith("in:"))
@@ -474,13 +479,13 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
474
479
  .filter((label) => label.startsWith("in:"))
475
480
  .sort();
476
481
  if (arraysEqual(issueInLabels, prInLabels)) {
477
- return null;
482
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
478
483
  }
479
484
  return shared.failResult(CHECK_TYPE, `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
480
485
  }
481
486
  function checkInLabelsComputed(context, remoteData, shared) {
482
- if (!context.config.verify_in_labels_computed || !context.hasTriage) {
483
- return null;
487
+ if (!context.audit.enabled || !context.hasTriage) {
488
+ return infoResult('Computed in: label audit is not applicable because it is disabled or triage capability is unavailable');
484
489
  }
485
490
  const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
486
491
  if (!expectedInLabels.ok) {
@@ -489,49 +494,49 @@ function checkInLabelsComputed(context, remoteData, shared) {
489
494
  : shared.blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
490
495
  }
491
496
  if (expectedInLabels.mode === "skipped") {
492
- return null;
497
+ return infoResult('Computed in: label audit is not applicable because no mapped changed path exists');
493
498
  }
494
499
  const actualInLabels = extractLabelNames(remoteData.issue.labels)
495
500
  .filter((label) => label.startsWith("in:"))
496
501
  .sort();
497
502
  if (arraysEqual(expectedInLabels.labels, actualInLabels)) {
498
- return null;
503
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} in: labels match committed changes`);
499
504
  }
500
505
  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
506
  }
502
507
  function checkSyncedRequirements(context, remoteData, shared) {
503
- if (!context.config.sync_checked_requirements || !context.hasTriage) {
504
- return null;
508
+ if (!context.audit.enabled || !context.hasTriage) {
509
+ return infoResult('Requirements audit is not applicable because it is disabled or triage capability is unavailable');
505
510
  }
506
511
  const checkedRequirements = shared.getCheckedRequirements(context.task.content);
507
512
  if (checkedRequirements.length === 0) {
508
- return null;
513
+ return infoResult('Requirements audit is not applicable because the task has no checked requirements');
509
514
  }
510
515
  const issueBody = remoteData.issue.body || "";
511
516
  const resolution = resolveRequirementSection(issueBody, requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task"));
512
517
  if (resolution.status === "missing") {
513
- return null;
518
+ return infoResult('Requirements audit is not applicable because the Issue has no requirements section');
514
519
  }
515
520
  if (resolution.status === "ambiguous") {
516
- return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed");
521
+ return hardResult(shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} requirements section is ambiguous`, "check_failed"));
517
522
  }
518
523
  const requirementBody = issueBody.slice(resolution.bodyStart, resolution.bodyEnd);
519
524
  const missingRequirements = checkedRequirements.filter((item) => !hasCheckedRequirement(requirementBody, item));
520
525
  if (missingRequirements.length === 0) {
521
- return null;
526
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} contains all checked requirements`);
522
527
  }
523
528
  return shared.failResult(CHECK_TYPE, `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`, "check_failed");
524
529
  }
525
530
  function checkIssueType(context, remoteData, shared) {
526
- if (!context.config.verify_issue_type || !context.hasPush) {
527
- return null;
531
+ if (!context.hasPush) {
532
+ return infoResult('Issue Type audit is not applicable because push capability is unavailable');
528
533
  }
529
534
  if (remoteData.issueType === undefined) {
530
- return null;
535
+ return infoResult('Issue Type audit is not applicable because the provider does not expose Issue Type');
531
536
  }
532
537
  if (!remoteData.issueType) {
533
538
  if (context.repoOwnerType === "User") {
534
- return null;
539
+ return infoResult('Issue Type audit is not applicable for a user-owned repository');
535
540
  }
536
541
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no Issue Type set`, "check_failed");
537
542
  }
@@ -539,14 +544,14 @@ function checkIssueType(context, remoteData, shared) {
539
544
  if (expectedType && remoteData.issueType !== expectedType) {
540
545
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`, "check_failed");
541
546
  }
542
- return null;
547
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has the expected Issue Type`);
543
548
  }
544
549
  function checkIssueFields(context, remoteData, shared) {
545
- if (!context.config.verify_issue_fields || !context.hasPush) {
546
- return null;
550
+ if (!context.hasPush) {
551
+ return infoResult('Issue field audit is not applicable because push capability is unavailable');
547
552
  }
548
553
  if (remoteData.issueFields === undefined) {
549
- return null;
554
+ return infoResult('Issue field audit is not applicable because the provider does not expose Issue fields');
550
555
  }
551
556
  for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
552
557
  const expectedRaw = context.task.metadata[metadataKey];
@@ -565,20 +570,20 @@ function checkIssueFields(context, remoteData, shared) {
565
570
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`, "check_failed");
566
571
  }
567
572
  }
568
- return null;
573
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
569
574
  }
570
575
  function checkPrAssignee(context, remoteData, shared) {
571
- if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
572
- return null;
576
+ if (!context.audit.enabled || !context.hasPush || !context.prNumber) {
577
+ return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
573
578
  }
574
579
  if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
575
580
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no assignee`, "check_failed");
576
581
  }
577
- return null;
582
+ return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
578
583
  }
579
584
  function checkMilestone(context, remoteData, shared) {
580
- if (!context.config.verify_milestone || !context.hasTriage) {
581
- return null;
585
+ if (!context.hasTriage) {
586
+ return infoResult('Milestone audit is not applicable because triage capability is unavailable');
582
587
  }
583
588
  if (!remoteData.issue?.milestone?.title) {
584
589
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
@@ -586,7 +591,7 @@ function checkMilestone(context, remoteData, shared) {
586
591
  if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
587
592
  return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no milestone set`, "check_failed");
588
593
  }
589
- if (context.config.verify_milestone_specific) {
594
+ if (context.audit.requireSpecificMilestone) {
590
595
  const issueTitle = remoteData.issue.milestone.title;
591
596
  if (VERSION_LINE_REGEX.test(issueTitle)) {
592
597
  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 +600,7 @@ function checkMilestone(context, remoteData, shared) {
595
600
  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
601
  }
597
602
  }
598
- return null;
603
+ return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
599
604
  }
600
605
  function findCommentByMarker(comments, marker) {
601
606
  return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;