@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.
- package/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +22 -12
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +10 -5
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +84 -32
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +184 -162
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +90 -22
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +28 -2
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +66 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +85 -18
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +198 -11
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +2 -0
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +22 -12
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +9 -5
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +98 -31
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +203 -188
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +1 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +113 -22
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +28 -2
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +55 -38
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +91 -15
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +198 -9
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +2 -0
- package/lib/task/workflow-dispatch.ts +171 -0
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +11 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- 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 {
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
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
|
|
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(
|
|
174
|
+
function resolveExpectedValues(skillName, artifactFile) {
|
|
156
175
|
const defaults = getDefaults();
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
187
|
-
commentMarker:
|
|
188
|
-
prCommentMarker:
|
|
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:
|
|
211
|
-
includeFields:
|
|
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 (
|
|
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 &&
|
|
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.
|
|
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
|
|
294
|
-
return
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
317
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
351
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
396
|
-
return
|
|
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, "
|
|
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
|
|
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.
|
|
429
|
-
return
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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(
|
|
434
|
+
const expectedBody = shared.normalizeContent(extractCommentBody(renderedTask.body));
|
|
444
435
|
const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
|
|
445
436
|
if (expectedBody === commentBody) {
|
|
446
|
-
return
|
|
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.
|
|
452
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
465
|
-
return
|
|
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
|
|
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.
|
|
480
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
501
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
524
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
543
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
569
|
-
return
|
|
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
|
|
565
|
+
return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
|
|
575
566
|
}
|
|
576
567
|
function checkMilestone(context, remoteData, shared) {
|
|
577
|
-
if (!context.
|
|
578
|
-
return
|
|
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.
|
|
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
|
|
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
|
|
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)) {
|