@fitlab-ai/agent-infra 0.11.1 → 0.11.3
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/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- 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 +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- 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/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- 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 +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- 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 +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- 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/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- 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 +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- 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-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- 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-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- 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
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
|
@@ -1,118 +1,9 @@
|
|
|
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, 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
|
-
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
7
|
function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
|
|
117
8
|
process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
|
|
118
9
|
}
|
|
@@ -240,69 +131,9 @@ async function sandboxControl(args) {
|
|
|
240
131
|
sandboxFinalizationClient(commandArgs);
|
|
241
132
|
return;
|
|
242
133
|
}
|
|
243
|
-
if (family === 'task-workflow') {
|
|
244
|
-
const command = commandArgs[0];
|
|
245
|
-
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
246
|
-
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
247
|
-
if (!command || !taskId || !generation) {
|
|
248
|
-
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: 'task-workflow requires a bound task and generation' } })}\n`);
|
|
249
|
-
process.exitCode = 1;
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
let workflow;
|
|
253
|
-
try {
|
|
254
|
-
const args = withoutValue(commandArgs.slice(1), '--capability-ref');
|
|
255
|
-
if (/^\d+$/.test(args[0] ?? '')) {
|
|
256
|
-
const resolved = resolveVisibleActiveShortId(args[0]);
|
|
257
|
-
if (resolved !== taskId)
|
|
258
|
-
throw new Error('SANDBOX_TASK_REF_MISMATCH');
|
|
259
|
-
args[0] = resolved;
|
|
260
|
-
}
|
|
261
|
-
workflow = createTaskWorkflowRequest(command, args, taskId, generation);
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
265
|
-
process.exitCode = 1;
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
let response;
|
|
269
|
-
try {
|
|
270
|
-
const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
|
|
271
|
-
response = requestSandboxTaskWorkflow({
|
|
272
|
-
workflow,
|
|
273
|
-
...(authority ? { authority } : {})
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
if (!(error instanceof SandboxControlClientError))
|
|
278
|
-
throw error;
|
|
279
|
-
writeClientError(error);
|
|
280
|
-
process.exitCode = error.detail.retryable ? 75 : 1;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
process.stdout.write(response.stdout);
|
|
284
|
-
process.stderr.write(response.stderr);
|
|
285
|
-
process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
134
|
let response;
|
|
289
135
|
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
|
-
}
|
|
136
|
+
response = requestSandboxControl({ family, args: commandArgs });
|
|
306
137
|
}
|
|
307
138
|
catch (error) {
|
|
308
139
|
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;
|
|
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
|
|
|
2
2
|
import { lifecycleIntentCatalog } from '../task/lifecycle.js';
|
|
3
3
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
4
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
|
|
5
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --
|
|
5
|
+
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
|
|
6
6
|
function usageFailure(message) {
|
|
7
7
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
8
8
|
process.stderr.write(USAGE);
|
|
@@ -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'),
|
|
@@ -228,6 +229,8 @@ export function resolveDelegatedTaskOperation(args) {
|
|
|
228
229
|
const [operation, ...rest] = args;
|
|
229
230
|
if (operation !== 'client' || !rest[0])
|
|
230
231
|
return null;
|
|
232
|
+
if (rest[0] === 'task-orchestration')
|
|
233
|
+
return null;
|
|
231
234
|
return resolveTaskOperation('internal', rest[0], rest.slice(1));
|
|
232
235
|
}
|
|
233
236
|
const TASK_MARKER_KEYS = [
|
|
@@ -351,14 +354,16 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
|
|
|
351
354
|
if (!hasCompleteConfig) {
|
|
352
355
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
353
356
|
}
|
|
354
|
-
return { kind: 'broker-client', reasonCode: null };
|
|
357
|
+
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
355
358
|
}
|
|
356
359
|
if (!hasAnyMarker)
|
|
357
360
|
return { kind: 'direct-host', reasonCode: null };
|
|
358
361
|
if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
|
|
359
362
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
360
363
|
}
|
|
361
|
-
return
|
|
364
|
+
return options.localWorkflow
|
|
365
|
+
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
|
|
366
|
+
: { kind: 'broker-client', reasonCode: null };
|
|
362
367
|
}
|
|
363
368
|
export function hasTaskBoundMarker(env = process.env) {
|
|
364
369
|
return taskMarkerState(env) === 'task-bound';
|
|
@@ -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);
|
|
@@ -29,8 +29,6 @@ const ARTIFACT_TITLES = {
|
|
|
29
29
|
'review-plan': '技术方案审查',
|
|
30
30
|
code: '实现报告',
|
|
31
31
|
'review-code': '代码审查',
|
|
32
|
-
'manual-validation': '人工验证报告',
|
|
33
|
-
'validation-run': '验证运行证据',
|
|
34
32
|
'pr-review': 'PR 审查报告'
|
|
35
33
|
};
|
|
36
34
|
function normalizeCommentContent(content) {
|
|
@@ -100,7 +98,7 @@ function taskCommentLanguage(repoRoot) {
|
|
|
100
98
|
}
|
|
101
99
|
function artifactIdentity(artifact) {
|
|
102
100
|
const stem = path.basename(artifact, '.md');
|
|
103
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|
|
|
101
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
104
102
|
if (!match)
|
|
105
103
|
throw new Error(`unsupported artifact '${artifact}'`);
|
|
106
104
|
const base = ARTIFACT_TITLES[match[1]];
|
|
@@ -373,6 +371,38 @@ function relatedComments(comments, prefix) {
|
|
|
373
371
|
return first === base || (first.startsWith(chunkNamespace) && first.endsWith(' -->'));
|
|
374
372
|
});
|
|
375
373
|
}
|
|
374
|
+
function taskManagedComment(comment, taskId) {
|
|
375
|
+
const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
|
|
376
|
+
return marker === MARKERS.task(taskId)
|
|
377
|
+
|| marker === MARKERS.summary(taskId)
|
|
378
|
+
|| marker.startsWith(`<!-- sync-issue:${taskId}:`);
|
|
379
|
+
}
|
|
380
|
+
function summaryPosition(comments, taskId) {
|
|
381
|
+
const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
|
|
382
|
+
const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
|
|
383
|
+
if (summary.length !== 1)
|
|
384
|
+
return { ok: false, known: false, summary: null };
|
|
385
|
+
if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
|
|
386
|
+
return { ok: false, known: false, summary: summary[0] };
|
|
387
|
+
}
|
|
388
|
+
const sequence = summary[0].createdSequence;
|
|
389
|
+
return { ok: managed.every((comment) => sequence >= comment.createdSequence), known: true, summary: summary[0] };
|
|
390
|
+
}
|
|
391
|
+
async function listedComments(provider, loaded, parent) {
|
|
392
|
+
return provider.comments?.list
|
|
393
|
+
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response) => response.ok
|
|
394
|
+
? {
|
|
395
|
+
ok: true,
|
|
396
|
+
value: response.value.map((comment) => ({
|
|
397
|
+
id: providerCommentId(comment.id, provider),
|
|
398
|
+
body: comment.body,
|
|
399
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined,
|
|
400
|
+
createdSequence: comment.createdSequence
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
: response)
|
|
404
|
+
: unsupportedProviderOperation(provider, 'comments.list');
|
|
405
|
+
}
|
|
376
406
|
function validateRelatedMarkerSet(comments, prefix) {
|
|
377
407
|
const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
|
|
378
408
|
if (new Set(markers).size !== markers.length)
|
|
@@ -442,18 +472,7 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
442
472
|
if (!hasResolvedPlatformContext(context) || !loaded.ok)
|
|
443
473
|
return context;
|
|
444
474
|
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');
|
|
475
|
+
const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
457
476
|
if (!listed.ok) {
|
|
458
477
|
return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
|
|
459
478
|
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
@@ -555,6 +574,65 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
555
574
|
}
|
|
556
575
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
557
576
|
}
|
|
577
|
+
if (options.kind === 'summary') {
|
|
578
|
+
const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
579
|
+
if (!refreshed.ok) {
|
|
580
|
+
return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
|
|
581
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
const position = summaryPosition(refreshed.value, resolved.taskId);
|
|
585
|
+
if (!position.ok && !position.known) {
|
|
586
|
+
return platformResult('blocked', {
|
|
587
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
588
|
+
error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (!position.ok && position.summary) {
|
|
592
|
+
const owner = position.summary.user?.login;
|
|
593
|
+
if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
|
|
594
|
+
|| !owner || owner !== context.platform.currentUser) {
|
|
595
|
+
return platformResult('failed', {
|
|
596
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
597
|
+
error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const deleted = loaded.value.provider.comments?.delete
|
|
601
|
+
? await loaded.value.provider.comments.delete({
|
|
602
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
|
|
603
|
+
comment: { kind: 'id', value: String(position.summary.id) },
|
|
604
|
+
mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
|
|
605
|
+
})
|
|
606
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
607
|
+
if (!deleted.ok) {
|
|
608
|
+
return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
|
|
609
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const written = loaded.value.provider.comments?.write
|
|
613
|
+
? await loaded.value.provider.comments.write({
|
|
614
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0].body,
|
|
615
|
+
mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
|
|
616
|
+
})
|
|
617
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
618
|
+
if (!written.ok) {
|
|
619
|
+
return platformResult(written.error.retryable ? 'blocked' : 'failed', {
|
|
620
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
ids.splice(0, ids.length, providerCommentId(written.value.remoteId, loaded.value.provider));
|
|
624
|
+
operations.push({ name: `comment:${desired[0].marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
|
|
625
|
+
const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
626
|
+
if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
|
|
627
|
+
const error = reconciled.ok
|
|
628
|
+
? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
|
|
629
|
+
: reconciled.error;
|
|
630
|
+
return platformResult(error.retryable ? 'blocked' : 'failed', {
|
|
631
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
558
636
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
559
637
|
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
560
638
|
...contextFields(context),
|
|
@@ -13,7 +13,7 @@ import { platformResult } from './types.js';
|
|
|
13
13
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
|
|
14
14
|
import { resourceIdentityEquals } from './resource-identity.js';
|
|
15
15
|
import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
|
|
16
|
-
import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
|
|
16
|
+
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
|
|
17
17
|
import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
|
|
18
18
|
function summaryMarker(taskId) {
|
|
19
19
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
@@ -515,6 +515,7 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
515
515
|
if (!initial.ok)
|
|
516
516
|
return fail(initial.status, context, initial.error);
|
|
517
517
|
const manual = options.manualValidation;
|
|
518
|
+
let manualEvidenceDigest = null;
|
|
518
519
|
const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
|
|
519
520
|
if (hasFinalManualValidation && (!manual || manual.phase !== 'final'))
|
|
520
521
|
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
|
|
@@ -538,6 +539,7 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
538
539
|
if (!completion.ok)
|
|
539
540
|
return fail('failed', context, platformError(completion.error), prNumber);
|
|
540
541
|
const { receipt, transaction } = completion.value;
|
|
542
|
+
manualEvidenceDigest = receipt.evidenceDigest;
|
|
541
543
|
if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended)
|
|
542
544
|
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
|
|
543
545
|
if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt))
|
|
@@ -581,7 +583,10 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
581
583
|
const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
|
|
582
584
|
if (!replaced.ok)
|
|
583
585
|
return fail('failed', context, platformError(replaced.error));
|
|
584
|
-
const
|
|
586
|
+
const renderedBody = manual?.phase === 'final'
|
|
587
|
+
? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
|
|
588
|
+
: replaced.value;
|
|
589
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
|
|
585
590
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId))
|
|
586
591
|
return fail('failed', context, {
|
|
587
592
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
@@ -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");
|