@fitlab-ai/agent-infra 0.11.1 → 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.
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +120 -13
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +28 -43
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/orchestration.js +0 -18
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +132 -13
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +23 -50
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/orchestration.ts +0 -34
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/lib/task/workflow-command.ts +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
|
@@ -43,7 +43,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
43
43
|
'platform-security': ['read', 'dismiss'],
|
|
44
44
|
'platform-metadata': ['init-labels', 'init-milestones'],
|
|
45
45
|
'platform-context': ['resolve'],
|
|
46
|
-
'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
|
|
46
|
+
'platform-comment': ['list', 'recover', 'owner', 'backfill', 'stage-summary', 'sync'],
|
|
47
47
|
'platform-issue': ['inspect', 'create', 'bind', 'sync'],
|
|
48
48
|
'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
|
|
49
49
|
'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
4
5
|
import { checkPlatformCommentOwner, listPlatformComments, syncPlatformComment } from '../platform/issue-comments.js';
|
|
6
|
+
import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
7
|
+
import { platformResult } from '../platform/types.js';
|
|
5
8
|
import { recoverTaskFromComments } from '../platform/task-recovery.js';
|
|
6
9
|
import { backfillCompletionComments } from '../platform/completion-backfill.js';
|
|
7
10
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
11
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
8
12
|
const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
|
|
9
13
|
agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
|
|
10
14
|
agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
|
|
11
15
|
agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
|
|
16
|
+
agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
|
|
12
17
|
agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
|
|
13
18
|
`;
|
|
14
19
|
function fail(message) {
|
|
@@ -63,6 +68,7 @@ async function platformComment(args = []) {
|
|
|
63
68
|
internalHandlerRoute('platform-comment', 'recover', operation),
|
|
64
69
|
internalHandlerRoute('platform-comment', 'owner', operation),
|
|
65
70
|
internalHandlerRoute('platform-comment', 'backfill', operation),
|
|
71
|
+
internalHandlerRoute('platform-comment', 'stage-summary', operation),
|
|
66
72
|
internalHandlerRoute('platform-comment', 'sync', operation)
|
|
67
73
|
].some(Boolean)) {
|
|
68
74
|
fail('a valid operation is required');
|
|
@@ -177,6 +183,59 @@ async function platformComment(args = []) {
|
|
|
177
183
|
finish(await backfillCompletionComments(taskRef, { cwd, agent: normalizedAgent }));
|
|
178
184
|
return;
|
|
179
185
|
}
|
|
186
|
+
if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
|
|
187
|
+
const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
|
|
188
|
+
if (unexpected) {
|
|
189
|
+
fail(`stage-summary does not accept '--${unexpected}'`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (typeof parsed.values.bodyFile !== 'string') {
|
|
193
|
+
fail('stage-summary requires --body-file');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let body;
|
|
197
|
+
try {
|
|
198
|
+
body = readBodyFile(parsed.values.bodyFile, cwd);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
205
|
+
if (!canonical.ok) {
|
|
206
|
+
fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
body = canonical.value;
|
|
210
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: cwd });
|
|
211
|
+
if (!resolved.ok || resolved.state !== 'active') {
|
|
212
|
+
fail('stage-summary requires an active task');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const target = path.join(resolved.taskDir, '.delivery-summary.json');
|
|
216
|
+
const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
|
|
217
|
+
const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
218
|
+
const next = `${JSON.stringify(value)}\n`;
|
|
219
|
+
if (previous === next) {
|
|
220
|
+
finish(platformResult('no-op'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
224
|
+
try {
|
|
225
|
+
fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
226
|
+
fs.renameSync(temporary, target);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
try {
|
|
230
|
+
fs.unlinkSync(temporary);
|
|
231
|
+
}
|
|
232
|
+
catch { /* preserve primary error */ }
|
|
233
|
+
fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
180
239
|
if (!internalHandlerRoute('platform-comment', 'sync', operation)) {
|
|
181
240
|
fail('operation is not registered');
|
|
182
241
|
return;
|
|
@@ -1,118 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskControl, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
|
|
1
|
+
import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
|
|
5
2
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
6
3
|
import { serveSandboxControl } from '../sandbox/control/server.js';
|
|
7
4
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
|
|
8
|
-
import { controllerProofFromContext, verifyCodexSandboxControllerContextWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
|
|
9
5
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
10
6
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
|
|
11
7
|
import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.js';
|
|
12
|
-
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
13
|
-
import { routeOrchestration } from '../task/orchestration.js';
|
|
14
|
-
import { parseArtifactName } from '../task/artifact-name.js';
|
|
15
|
-
function valueAfter(args, flag) {
|
|
16
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
-
if (args[index] === flag)
|
|
18
|
-
return args[index + 1];
|
|
19
|
-
if (args[index]?.startsWith(`${flag}=`))
|
|
20
|
-
return args[index].slice(flag.length + 1);
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
function withoutValue(args, flag) {
|
|
25
|
-
const result = [];
|
|
26
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
-
if (args[index] === flag) {
|
|
28
|
-
index += 1;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (args[index]?.startsWith(`${flag}=`))
|
|
32
|
-
continue;
|
|
33
|
-
result.push(args[index]);
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
37
|
-
function lifecycleIds(authorityRef, taskId) {
|
|
38
|
-
const digest = (label) => createHash('sha256')
|
|
39
|
-
.update(`${label}\0${taskId}\0${authorityRef}`, 'utf8')
|
|
40
|
-
.digest('hex');
|
|
41
|
-
return { operationId: digest('operation'), lifecycleRequestId: digest('lifecycle-request') };
|
|
42
|
-
}
|
|
43
|
-
function lifecycleAuthorityIdentity(authorityRef, taskId, requestId, phase, family, artifact, round) {
|
|
44
|
-
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
45
|
-
if (!authorityRef || !contextPath)
|
|
46
|
-
return undefined;
|
|
47
|
-
const routed = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() });
|
|
48
|
-
const buildIdentity = computeLifecycleBuildIdentity(process.cwd());
|
|
49
|
-
const hookDefinitionHash = createHash('sha256')
|
|
50
|
-
.update(readFileSync(path.join(process.cwd(), '.codex', 'hooks.json')))
|
|
51
|
-
.digest('hex');
|
|
52
|
-
const ids = lifecycleIds(authorityRef, taskId);
|
|
53
|
-
return {
|
|
54
|
-
version: 1,
|
|
55
|
-
requestId,
|
|
56
|
-
operationId: ids.operationId,
|
|
57
|
-
phase,
|
|
58
|
-
taskId,
|
|
59
|
-
family,
|
|
60
|
-
artifact,
|
|
61
|
-
round,
|
|
62
|
-
lifecycleRequestId: ids.lifecycleRequestId,
|
|
63
|
-
authorityRef,
|
|
64
|
-
expectedControlGeneration: routed.context.controlGeneration,
|
|
65
|
-
expectedControllerInstanceDigest: routed.context.controllerInstanceDigest,
|
|
66
|
-
expectedBuildIdentityDigest: createHash('sha256').update(JSON.stringify(buildIdentity)).digest('hex'),
|
|
67
|
-
expectedHookDefinitionHash: hookDefinitionHash
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function lifecycleAuthoritySelector(args) {
|
|
71
|
-
if (!isCanonicalCodexPrepare(args))
|
|
72
|
-
return undefined;
|
|
73
|
-
const authorityRef = valueAfter(args, '--capability-ref');
|
|
74
|
-
const taskId = args[0];
|
|
75
|
-
if (!authorityRef || !taskId)
|
|
76
|
-
return undefined;
|
|
77
|
-
const routed = routeOrchestration(taskId);
|
|
78
|
-
if (!routed.next || !['analysis', 'plan', 'code'].includes(routed.next.stage))
|
|
79
|
-
return undefined;
|
|
80
|
-
return lifecycleAuthorityIdentity(authorityRef, taskId, randomUUID(), 'orchestration.prepare', routed.next.stage === 'analysis' ? 'analysis' : routed.next.stage === 'plan' ? 'plan' : 'code', routed.next.artifact, routed.next.round);
|
|
81
|
-
}
|
|
82
|
-
function lifecycleWorkflowAuthoritySelector(originalArgs, workflow) {
|
|
83
|
-
const authorityRef = valueAfter(originalArgs, '--capability-ref');
|
|
84
|
-
if (!authorityRef)
|
|
85
|
-
return undefined;
|
|
86
|
-
if (workflow.operation === 'artifact-finalize-local') {
|
|
87
|
-
const family = valueAfter(workflow.args, '--family');
|
|
88
|
-
const artifact = valueAfter(workflow.args, '--artifact');
|
|
89
|
-
const parsed = artifact ? parseArtifactName(artifact) : null;
|
|
90
|
-
if ((family !== 'analysis' && family !== 'plan' && family !== 'code')
|
|
91
|
-
|| !parsed || parsed.family !== family)
|
|
92
|
-
return undefined;
|
|
93
|
-
return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'artifact.finalize-local', family, artifact, parsed.round);
|
|
94
|
-
}
|
|
95
|
-
if (workflow.operation !== 'event')
|
|
96
|
-
return undefined;
|
|
97
|
-
const completed = /^(analysis|plan|code)\.completed$/u.exec(workflow.args[1] ?? '');
|
|
98
|
-
const artifact = valueAfter(workflow.args, '--artifact');
|
|
99
|
-
const parsed = artifact ? parseArtifactName(artifact) : null;
|
|
100
|
-
if (!completed || !parsed || parsed.family !== completed[1])
|
|
101
|
-
return undefined;
|
|
102
|
-
return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'task-event.completed', completed[1], artifact, parsed.round);
|
|
103
|
-
}
|
|
104
|
-
function isCanonicalCodexPrepare(args) {
|
|
105
|
-
if (args[1] !== 'prepare')
|
|
106
|
-
return false;
|
|
107
|
-
const clients = [];
|
|
108
|
-
for (let index = 2; index < args.length; index += 1) {
|
|
109
|
-
if (args[index] === '--client')
|
|
110
|
-
clients.push(args[index + 1] ?? '');
|
|
111
|
-
else if (args[index]?.startsWith('--client='))
|
|
112
|
-
clients.push(args[index].slice('--client='.length));
|
|
113
|
-
}
|
|
114
|
-
return clients.length === 1 && clients[0] === 'codex';
|
|
115
|
-
}
|
|
116
8
|
function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
|
|
117
9
|
process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
|
|
118
10
|
}
|
|
@@ -251,7 +143,7 @@ async function sandboxControl(args) {
|
|
|
251
143
|
}
|
|
252
144
|
let workflow;
|
|
253
145
|
try {
|
|
254
|
-
const args =
|
|
146
|
+
const args = commandArgs.slice(1);
|
|
255
147
|
if (/^\d+$/.test(args[0] ?? '')) {
|
|
256
148
|
const resolved = resolveVisibleActiveShortId(args[0]);
|
|
257
149
|
if (resolved !== taskId)
|
|
@@ -267,11 +159,7 @@ async function sandboxControl(args) {
|
|
|
267
159
|
}
|
|
268
160
|
let response;
|
|
269
161
|
try {
|
|
270
|
-
|
|
271
|
-
response = requestSandboxTaskWorkflow({
|
|
272
|
-
workflow,
|
|
273
|
-
...(authority ? { authority } : {})
|
|
274
|
-
});
|
|
162
|
+
response = requestSandboxTaskWorkflow({ workflow });
|
|
275
163
|
}
|
|
276
164
|
catch (error) {
|
|
277
165
|
if (!(error instanceof SandboxControlClientError))
|
|
@@ -287,22 +175,7 @@ async function sandboxControl(args) {
|
|
|
287
175
|
}
|
|
288
176
|
let response;
|
|
289
177
|
try {
|
|
290
|
-
|
|
291
|
-
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
292
|
-
const proof = contextPath
|
|
293
|
-
? controllerProofFromContext(verifyCodexSandboxControllerContextWithWarnings(contextPath).context)
|
|
294
|
-
: null;
|
|
295
|
-
const authority = lifecycleAuthoritySelector(commandArgs);
|
|
296
|
-
response = requestSandboxTaskControl({
|
|
297
|
-
family,
|
|
298
|
-
args: commandArgs,
|
|
299
|
-
controllerProof: authority ? null : proof,
|
|
300
|
-
...(authority ? { authority } : {})
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
response = requestSandboxControl({ family, args: commandArgs });
|
|
305
|
-
}
|
|
178
|
+
response = requestSandboxControl({ family, args: commandArgs });
|
|
306
179
|
}
|
|
307
180
|
catch (error) {
|
|
308
181
|
if (!(error instanceof SandboxControlClientError))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
|
|
2
2
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
|
|
3
|
-
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact
|
|
3
|
+
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize a workflow artifact without changing task state.\n`;
|
|
4
4
|
function taskArtifact(args = []) {
|
|
5
5
|
if (!ensureInternalHandlerRoute('task-artifact', args))
|
|
6
6
|
return;
|
|
@@ -50,6 +50,7 @@ function internalTaskRoutes() {
|
|
|
50
50
|
descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
|
|
51
51
|
descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
|
|
52
52
|
descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
|
|
53
|
+
descriptor('internal', 'platform-comment', 'stage-summary', 'task-bound', 'artifact-write'),
|
|
53
54
|
descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
|
|
54
55
|
descriptor('internal', 'platform-issue', 'inspect', 'task-bound', 'diagnostic'),
|
|
55
56
|
descriptor('internal', 'platform-issue', 'create', 'task-bound', 'remote-write'),
|
|
@@ -4,7 +4,7 @@ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task
|
|
|
4
4
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
5
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
6
6
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
|
|
7
|
-
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--
|
|
7
|
+
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
|
|
8
8
|
function failUsage(message) {
|
|
9
9
|
process.stdout.write(`${JSON.stringify({
|
|
10
10
|
status: 'failed',
|
|
@@ -30,7 +30,7 @@ async function taskReview(args = []) {
|
|
|
30
30
|
failUsage(error instanceof Error ? error.message : String(error));
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
const { stage, artifact,
|
|
33
|
+
const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
|
|
34
34
|
const operation = args[1];
|
|
35
35
|
const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
|
|
36
36
|
if (dryRunConflict) {
|
|
@@ -47,8 +47,8 @@ async function taskReview(args = []) {
|
|
|
47
47
|
try {
|
|
48
48
|
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
|
|
49
49
|
let current = operation === 'preflight'
|
|
50
|
-
? preflightReviewSummary({ taskRef: args[0], stage, artifact,
|
|
51
|
-
: finalizeReviewSummary({ taskRef: args[0], stage, artifact,
|
|
50
|
+
? preflightReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
|
|
51
|
+
: finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
|
|
52
52
|
if (operation === 'preflight')
|
|
53
53
|
return current;
|
|
54
54
|
if (current.status !== 'failed' || !overrideTicket)
|
|
@@ -63,7 +63,7 @@ async function taskReview(args = []) {
|
|
|
63
63
|
target: overrideTarget, scope: overrideScope
|
|
64
64
|
}, {
|
|
65
65
|
effectExecutor: (capability) => {
|
|
66
|
-
const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact,
|
|
66
|
+
const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
|
|
67
67
|
current = retried;
|
|
68
68
|
return retried.status === 'failed' || retried.status === 'planned'
|
|
69
69
|
? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
|
|
@@ -135,12 +135,6 @@ function snapshotValidation(repoRoot, commit, options) {
|
|
|
135
135
|
cleanupError = error;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
try {
|
|
139
|
-
git(repoRoot, ['worktree', 'prune']);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
cleanupError ??= error;
|
|
143
|
-
}
|
|
144
138
|
try {
|
|
145
139
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
146
140
|
}
|
|
@@ -171,6 +171,12 @@ function sanitizeMarkdownDocument(value, options = {}) {
|
|
|
171
171
|
function canonicalizeCommentBody(value) {
|
|
172
172
|
return sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
173
173
|
}
|
|
174
|
+
function canonicalizeSummaryBody(value) {
|
|
175
|
+
const sanitized = canonicalizeCommentBody(value);
|
|
176
|
+
return sanitized.ok
|
|
177
|
+
? ok(`${sanitized.value.replace(/\n+$/, '')}\n`)
|
|
178
|
+
: sanitized;
|
|
179
|
+
}
|
|
174
180
|
function renderSafeCodeFence(value, language = '', reservedMarkers = [CONTROL_MARKER_PATTERN]) {
|
|
175
181
|
const content = markerSafe(normalized(value), reservedMarkers);
|
|
176
182
|
const longestRun = Math.max(0, ...(content.match(/`+/g) || []).map((run) => run.length));
|
|
@@ -225,5 +231,5 @@ function splitDocumentPlaceholder(value, placeholder) {
|
|
|
225
231
|
}
|
|
226
232
|
return ok({ prefix: source.slice(0, position), suffix: source.slice(position + placeholder.length) });
|
|
227
233
|
}
|
|
228
|
-
export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
|
|
234
|
+
export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, canonicalizeSummaryBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
|
|
229
235
|
//# sourceMappingURL=comment-safety.js.map
|
|
@@ -278,13 +278,14 @@ function issueSnapshot(value, graph) {
|
|
|
278
278
|
displayUrl: value.url
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
|
-
function commentSnapshot(value) {
|
|
281
|
+
function commentSnapshot(value, createdSequence = null) {
|
|
282
282
|
return {
|
|
283
283
|
id: String(value.id),
|
|
284
284
|
author: value.user?.login ? { id: String(value.user.login), name: String(value.user.login) } : null,
|
|
285
285
|
body: String(value.body || ''),
|
|
286
286
|
createdAt: String(value.created_at || ''),
|
|
287
|
-
updatedAt: String(value.updated_at || value.created_at || '')
|
|
287
|
+
updatedAt: String(value.updated_at || value.created_at || ''),
|
|
288
|
+
createdSequence
|
|
288
289
|
};
|
|
289
290
|
}
|
|
290
291
|
function createReceipt(remoteId) {
|
|
@@ -555,7 +556,16 @@ function createGitHubOperations(client) {
|
|
|
555
556
|
if (!response.ok)
|
|
556
557
|
return response;
|
|
557
558
|
const values = Array.isArray(response.value) ? response.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) : [];
|
|
558
|
-
|
|
559
|
+
const comments = values.filter((entry) => entry && typeof entry === 'object');
|
|
560
|
+
let previous = 0;
|
|
561
|
+
const ordered = comments.length === values.length && comments.every((entry) => {
|
|
562
|
+
const id = Number(entry.id);
|
|
563
|
+
if (!Number.isSafeInteger(id) || id <= previous)
|
|
564
|
+
return false;
|
|
565
|
+
previous = id;
|
|
566
|
+
return true;
|
|
567
|
+
});
|
|
568
|
+
return { ok: true, value: comments.map((comment) => commentSnapshot(comment, ordered ? Number(comment.id) : null)) };
|
|
559
569
|
},
|
|
560
570
|
async write({ context, parent, body, existingComment }) {
|
|
561
571
|
const number = resourceIdentityNumber(parent);
|
|
@@ -10,7 +10,7 @@ import { resourceIdentityNumber } from './resource-identity.js';
|
|
|
10
10
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
11
11
|
import { recordForArtifact, recordForCancel, recordForSummary, recordForTask, renderActivityRecoveryMetadata } from './activity-recovery.js';
|
|
12
12
|
import { projectTaskComment } from './task-comment-projection.js';
|
|
13
|
-
import { canonicalizeCommentBody, escapeHtmlText, fenceRanges, renderSafeCodeFence } from './comment-safety.js';
|
|
13
|
+
import { canonicalizeCommentBody, canonicalizeSummaryBody, escapeHtmlText, fenceRanges, renderSafeCodeFence } from './comment-safety.js';
|
|
14
14
|
function providerCommentId(id, provider) {
|
|
15
15
|
return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
|
|
16
16
|
}
|
|
@@ -373,6 +373,46 @@ function relatedComments(comments, prefix) {
|
|
|
373
373
|
return first === base || (first.startsWith(chunkNamespace) && first.endsWith(' -->'));
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
|
+
function taskManagedComment(comment, taskId) {
|
|
377
|
+
const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
|
|
378
|
+
return marker === MARKERS.task(taskId)
|
|
379
|
+
|| marker === MARKERS.summary(taskId)
|
|
380
|
+
|| marker.startsWith(`<!-- sync-issue:${taskId}:`);
|
|
381
|
+
}
|
|
382
|
+
function summaryPosition(comments, taskId) {
|
|
383
|
+
const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
|
|
384
|
+
const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
|
|
385
|
+
if (summary.length !== 1)
|
|
386
|
+
return { ok: false, known: false, summary: null };
|
|
387
|
+
if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
|
|
388
|
+
return { ok: false, known: false, summary: summary[0] };
|
|
389
|
+
}
|
|
390
|
+
const sequence = summary[0].createdSequence;
|
|
391
|
+
return { ok: managed.every((comment) => sequence >= comment.createdSequence), known: true, summary: summary[0] };
|
|
392
|
+
}
|
|
393
|
+
function summaryDigest(comment) {
|
|
394
|
+
const body = normalizeCommentContent(comment.body)
|
|
395
|
+
.replace(/^<!-- sync-issue:[^\n]+:summary -->\n## [^\n]+\n\n> [^\n]+\n\n/u, '')
|
|
396
|
+
.replace(/^<details><summary>恢复元数据<\/summary>[\s\S]*?<\/details>\n\n/u, '')
|
|
397
|
+
.replace(/\n---\n\*[^\n]*\*$/u, '');
|
|
398
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
399
|
+
return canonical.ok ? createHash('sha256').update(canonical.value).digest('hex') : '';
|
|
400
|
+
}
|
|
401
|
+
async function listedComments(provider, loaded, parent) {
|
|
402
|
+
return provider.comments?.list
|
|
403
|
+
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response) => response.ok
|
|
404
|
+
? {
|
|
405
|
+
ok: true,
|
|
406
|
+
value: response.value.map((comment) => ({
|
|
407
|
+
id: providerCommentId(comment.id, provider),
|
|
408
|
+
body: comment.body,
|
|
409
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined,
|
|
410
|
+
createdSequence: comment.createdSequence
|
|
411
|
+
}))
|
|
412
|
+
}
|
|
413
|
+
: response)
|
|
414
|
+
: unsupportedProviderOperation(provider, 'comments.list');
|
|
415
|
+
}
|
|
376
416
|
function validateRelatedMarkerSet(comments, prefix) {
|
|
377
417
|
const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
|
|
378
418
|
if (new Set(markers).size !== markers.length)
|
|
@@ -442,18 +482,7 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
442
482
|
if (!hasResolvedPlatformContext(context) || !loaded.ok)
|
|
443
483
|
return context;
|
|
444
484
|
const issue = resourceIdentityNumber(issueIdentityFromTask);
|
|
445
|
-
const listed = loaded.value.provider.
|
|
446
|
-
? await loaded.value.provider.comments.list({
|
|
447
|
-
context: providerOperationContext(loaded.value),
|
|
448
|
-
parent: issueIdentityFromTask
|
|
449
|
-
}).then((response) => response.ok
|
|
450
|
-
? { ok: true, value: response.value.map((comment) => ({
|
|
451
|
-
id: providerCommentId(comment.id, loaded.value.provider),
|
|
452
|
-
body: comment.body,
|
|
453
|
-
user: comment.author?.name ? { login: comment.author.name } : undefined
|
|
454
|
-
})) }
|
|
455
|
-
: response)
|
|
456
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
485
|
+
const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
457
486
|
if (!listed.ok) {
|
|
458
487
|
return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
|
|
459
488
|
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
@@ -481,6 +510,25 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
481
510
|
});
|
|
482
511
|
}
|
|
483
512
|
}
|
|
513
|
+
if (options.verifyOnly) {
|
|
514
|
+
if (options.kind !== 'summary' || !options.summaryAuthorization) {
|
|
515
|
+
return platformResult('failed', {
|
|
516
|
+
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
517
|
+
error: { code: 'SUMMARY_VERIFICATION_INVALID', message: 'summary verification requires durable authorization', retryable: false }
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const position = summaryPosition(listed.value, resolved.taskId);
|
|
521
|
+
if (!position.ok || !position.summary || summaryDigest(position.summary) !== options.summaryAuthorization.sha256) {
|
|
522
|
+
return platformResult('failed', {
|
|
523
|
+
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
524
|
+
error: { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary marker, body digest, or managed comment order is invalid', retryable: true }
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return platformResult('no-op', {
|
|
528
|
+
...contextFields(context), changed: false, resource: { kind: 'issue', number: issue },
|
|
529
|
+
comment: { kind: options.kind, marker: desired[0].marker, ids: [position.summary.id], parts: 1 }, error: null
|
|
530
|
+
});
|
|
531
|
+
}
|
|
484
532
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
485
533
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
486
534
|
const operations = existing.map((comment) => ({
|
|
@@ -555,6 +603,65 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
555
603
|
}
|
|
556
604
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
557
605
|
}
|
|
606
|
+
if (options.kind === 'summary') {
|
|
607
|
+
const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
608
|
+
if (!refreshed.ok) {
|
|
609
|
+
return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
|
|
610
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const position = summaryPosition(refreshed.value, resolved.taskId);
|
|
614
|
+
if (!position.ok && !position.known) {
|
|
615
|
+
return platformResult('blocked', {
|
|
616
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
617
|
+
error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (!position.ok && position.summary) {
|
|
621
|
+
const owner = position.summary.user?.login;
|
|
622
|
+
if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
|
|
623
|
+
|| !owner || owner !== context.platform.currentUser) {
|
|
624
|
+
return platformResult('failed', {
|
|
625
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
626
|
+
error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const deleted = loaded.value.provider.comments?.delete
|
|
630
|
+
? await loaded.value.provider.comments.delete({
|
|
631
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
|
|
632
|
+
comment: { kind: 'id', value: String(position.summary.id) },
|
|
633
|
+
mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
|
|
634
|
+
})
|
|
635
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
636
|
+
if (!deleted.ok) {
|
|
637
|
+
return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
|
|
638
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const written = loaded.value.provider.comments?.write
|
|
642
|
+
? await loaded.value.provider.comments.write({
|
|
643
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0].body,
|
|
644
|
+
mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
|
|
645
|
+
})
|
|
646
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
647
|
+
if (!written.ok) {
|
|
648
|
+
return platformResult(written.error.retryable ? 'blocked' : 'failed', {
|
|
649
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
ids.splice(0, ids.length, providerCommentId(written.value.remoteId, loaded.value.provider));
|
|
653
|
+
operations.push({ name: `comment:${desired[0].marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
|
|
654
|
+
const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
655
|
+
if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
|
|
656
|
+
const error = reconciled.ok
|
|
657
|
+
? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
|
|
658
|
+
: reconciled.error;
|
|
659
|
+
return platformResult(error.retryable ? 'blocked' : 'failed', {
|
|
660
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
558
665
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
559
666
|
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
560
667
|
...contextFields(context),
|
|
@@ -87,6 +87,8 @@ type RemoteCommentSnapshot = {
|
|
|
87
87
|
body: string;
|
|
88
88
|
createdAt: string;
|
|
89
89
|
updatedAt: string;
|
|
90
|
+
/** Strict creation order when the provider can prove it for this list snapshot. */
|
|
91
|
+
createdSequence: number | null;
|
|
90
92
|
};
|
|
91
93
|
type ChangeRequestSnapshot = {
|
|
92
94
|
id: string;
|
|
@@ -189,13 +189,18 @@ function issueType(value, label) {
|
|
|
189
189
|
}
|
|
190
190
|
function validateComment(value) {
|
|
191
191
|
const item = record(value, 'comment');
|
|
192
|
-
exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt'], 'comment');
|
|
192
|
+
exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt', 'createdSequence'], 'comment');
|
|
193
193
|
return {
|
|
194
194
|
id: stringValue(item.id, 'comment.id'),
|
|
195
195
|
author: author(item.author, 'comment.author'),
|
|
196
196
|
body: stringValue(item.body, 'comment.body', true),
|
|
197
197
|
createdAt: utcTimestamp(item.createdAt, 'comment.createdAt'),
|
|
198
|
-
updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt')
|
|
198
|
+
updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt'),
|
|
199
|
+
createdSequence: item.createdSequence === null ? null : (() => {
|
|
200
|
+
if (!Number.isSafeInteger(item.createdSequence) || Number(item.createdSequence) < 1)
|
|
201
|
+
throw new Error('comment.createdSequence must be a positive safe integer or null');
|
|
202
|
+
return Number(item.createdSequence);
|
|
203
|
+
})()
|
|
199
204
|
};
|
|
200
205
|
}
|
|
201
206
|
function validateChangeRequest(value, expectedKind, requireIdentity = false) {
|
|
@@ -11,7 +11,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.js";
|
|
|
11
11
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.js";
|
|
12
12
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.js";
|
|
13
13
|
import { taskIssueIdentity } from "./task-identities.js";
|
|
14
|
-
import { renderTaskCommentResult } from "./issue-comments.js";
|
|
14
|
+
import { MARKERS, renderTaskCommentResult } from "./issue-comments.js";
|
|
15
15
|
import { CONTROL_MARKER_PATTERN, canonicalizeCommentBody, renderSafeCodeFence, sanitizeMarkdownDocument } from "./comment-safety.js";
|
|
16
16
|
const CHECK_TYPE = "platform-sync";
|
|
17
17
|
const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
|
|
@@ -258,7 +258,9 @@ async function fetchRemoteData(context, shared) {
|
|
|
258
258
|
}
|
|
259
259
|
return {
|
|
260
260
|
issue,
|
|
261
|
-
comments: facts.value.comments.map((comment) => ({
|
|
261
|
+
comments: facts.value.comments.map((comment) => ({
|
|
262
|
+
id: comment.id, body: comment.body, createdSequence: comment.createdSequence
|
|
263
|
+
})),
|
|
262
264
|
prComments,
|
|
263
265
|
prLabels: changeRequest?.labels || null,
|
|
264
266
|
issueType: issueSnapshot
|
|
@@ -320,6 +322,21 @@ function checkCommentMarker(context, remoteData, shared) {
|
|
|
320
322
|
}
|
|
321
323
|
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
322
324
|
if (comment) {
|
|
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
|
+
}
|
|
323
340
|
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
|
|
324
341
|
}
|
|
325
342
|
return shared.failResult(CHECK_TYPE, `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`, "check_failed");
|