@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,141 +1,14 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
1
|
import {
|
|
5
2
|
recoverSandboxControl,
|
|
6
3
|
requestSandboxControl,
|
|
7
4
|
requestSandboxTaskFinalization,
|
|
8
|
-
requestSandboxTaskControl,
|
|
9
|
-
requestSandboxTaskWorkflow,
|
|
10
|
-
resolveVisibleActiveShortId,
|
|
11
5
|
SandboxControlClientError
|
|
12
6
|
} from '../sandbox/control/client.ts';
|
|
13
7
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
14
8
|
import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
15
9
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
|
|
16
|
-
import {
|
|
17
|
-
controllerProofFromContext,
|
|
18
|
-
verifyCodexSandboxControllerContextWithWarnings
|
|
19
|
-
} from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
20
10
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
21
11
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
|
|
22
|
-
import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.ts';
|
|
23
|
-
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
24
|
-
import { routeOrchestration } from '../task/orchestration.ts';
|
|
25
|
-
import { parseArtifactName } from '../task/artifact-name.ts';
|
|
26
|
-
import type { LifecycleAuthorityRequestV1 } from '../task/control-authority.ts';
|
|
27
|
-
|
|
28
|
-
function valueAfter(args: readonly string[], flag: string): string | undefined {
|
|
29
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
30
|
-
if (args[index] === flag) return args[index + 1];
|
|
31
|
-
if (args[index]?.startsWith(`${flag}=`)) return args[index]!.slice(flag.length + 1);
|
|
32
|
-
}
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function withoutValue(args: readonly string[], flag: string): string[] {
|
|
37
|
-
const result: string[] = [];
|
|
38
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
39
|
-
if (args[index] === flag) { index += 1; continue; }
|
|
40
|
-
if (args[index]?.startsWith(`${flag}=`)) continue;
|
|
41
|
-
result.push(args[index]!);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function lifecycleIds(authorityRef: string, taskId: string): Readonly<{ operationId: string; lifecycleRequestId: string }> {
|
|
47
|
-
const digest = (label: string) => createHash('sha256')
|
|
48
|
-
.update(`${label}\0${taskId}\0${authorityRef}`, 'utf8')
|
|
49
|
-
.digest('hex');
|
|
50
|
-
return { operationId: digest('operation'), lifecycleRequestId: digest('lifecycle-request') };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function lifecycleAuthorityIdentity(
|
|
54
|
-
authorityRef: string,
|
|
55
|
-
taskId: string,
|
|
56
|
-
requestId: string,
|
|
57
|
-
phase: LifecycleAuthorityRequestV1['phase'],
|
|
58
|
-
family: LifecycleAuthorityRequestV1['family'],
|
|
59
|
-
artifact: string,
|
|
60
|
-
round: number
|
|
61
|
-
): LifecycleAuthorityRequestV1 | undefined {
|
|
62
|
-
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
63
|
-
if (!authorityRef || !contextPath) return undefined;
|
|
64
|
-
const routed = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() });
|
|
65
|
-
const buildIdentity = computeLifecycleBuildIdentity(process.cwd());
|
|
66
|
-
const hookDefinitionHash = createHash('sha256')
|
|
67
|
-
.update(readFileSync(path.join(process.cwd(), '.codex', 'hooks.json')))
|
|
68
|
-
.digest('hex');
|
|
69
|
-
const ids = lifecycleIds(authorityRef, taskId);
|
|
70
|
-
return {
|
|
71
|
-
version: 1,
|
|
72
|
-
requestId,
|
|
73
|
-
operationId: ids.operationId,
|
|
74
|
-
phase,
|
|
75
|
-
taskId,
|
|
76
|
-
family,
|
|
77
|
-
artifact,
|
|
78
|
-
round,
|
|
79
|
-
lifecycleRequestId: ids.lifecycleRequestId,
|
|
80
|
-
authorityRef,
|
|
81
|
-
expectedControlGeneration: routed.context.controlGeneration,
|
|
82
|
-
expectedControllerInstanceDigest: routed.context.controllerInstanceDigest,
|
|
83
|
-
expectedBuildIdentityDigest: createHash('sha256').update(JSON.stringify(buildIdentity)).digest('hex'),
|
|
84
|
-
expectedHookDefinitionHash: hookDefinitionHash
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function lifecycleAuthoritySelector(args: readonly string[]): LifecycleAuthorityRequestV1 | undefined {
|
|
89
|
-
if (!isCanonicalCodexPrepare(args)) return undefined;
|
|
90
|
-
const authorityRef = valueAfter(args, '--capability-ref');
|
|
91
|
-
const taskId = args[0];
|
|
92
|
-
if (!authorityRef || !taskId) return undefined;
|
|
93
|
-
const routed = routeOrchestration(taskId);
|
|
94
|
-
if (!routed.next || !['analysis', 'plan', 'code'].includes(routed.next.stage)) return undefined;
|
|
95
|
-
return lifecycleAuthorityIdentity(
|
|
96
|
-
authorityRef,
|
|
97
|
-
taskId,
|
|
98
|
-
randomUUID(),
|
|
99
|
-
'orchestration.prepare',
|
|
100
|
-
routed.next.stage === 'analysis' ? 'analysis' : routed.next.stage === 'plan' ? 'plan' : 'code',
|
|
101
|
-
routed.next.artifact,
|
|
102
|
-
routed.next.round
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function lifecycleWorkflowAuthoritySelector(
|
|
107
|
-
originalArgs: readonly string[],
|
|
108
|
-
workflow: ReturnType<typeof createTaskWorkflowRequest>
|
|
109
|
-
): LifecycleAuthorityRequestV1 | undefined {
|
|
110
|
-
const authorityRef = valueAfter(originalArgs, '--capability-ref');
|
|
111
|
-
if (!authorityRef) return undefined;
|
|
112
|
-
if (workflow.operation === 'artifact-finalize-local') {
|
|
113
|
-
const family = valueAfter(workflow.args, '--family');
|
|
114
|
-
const artifact = valueAfter(workflow.args, '--artifact');
|
|
115
|
-
const parsed = artifact ? parseArtifactName(artifact) : null;
|
|
116
|
-
if ((family !== 'analysis' && family !== 'plan' && family !== 'code')
|
|
117
|
-
|| !parsed || parsed.family !== family) return undefined;
|
|
118
|
-
return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id,
|
|
119
|
-
'artifact.finalize-local', family, artifact!, parsed.round);
|
|
120
|
-
}
|
|
121
|
-
if (workflow.operation !== 'event') return undefined;
|
|
122
|
-
const completed = /^(analysis|plan|code)\.completed$/u.exec(workflow.args[1] ?? '');
|
|
123
|
-
const artifact = valueAfter(workflow.args, '--artifact');
|
|
124
|
-
const parsed = artifact ? parseArtifactName(artifact) : null;
|
|
125
|
-
if (!completed || !parsed || parsed.family !== completed[1]) return undefined;
|
|
126
|
-
return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id,
|
|
127
|
-
'task-event.completed', completed[1] as LifecycleAuthorityRequestV1['family'], artifact!, parsed.round);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function isCanonicalCodexPrepare(args: readonly string[]): boolean {
|
|
131
|
-
if (args[1] !== 'prepare') return false;
|
|
132
|
-
const clients: string[] = [];
|
|
133
|
-
for (let index = 2; index < args.length; index += 1) {
|
|
134
|
-
if (args[index] === '--client') clients.push(args[index + 1] ?? '');
|
|
135
|
-
else if (args[index]?.startsWith('--client=')) clients.push(args[index]!.slice('--client='.length));
|
|
136
|
-
}
|
|
137
|
-
return clients.length === 1 && clients[0] === 'codex';
|
|
138
|
-
}
|
|
139
12
|
|
|
140
13
|
type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
|
|
141
14
|
|
|
@@ -263,66 +136,9 @@ async function sandboxControl(args: string[]): Promise<void> {
|
|
|
263
136
|
sandboxFinalizationClient(commandArgs);
|
|
264
137
|
return;
|
|
265
138
|
}
|
|
266
|
-
if (family === 'task-workflow') {
|
|
267
|
-
const command = commandArgs[0] as Parameters<typeof createTaskWorkflowRequest>[0] | undefined;
|
|
268
|
-
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
269
|
-
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
270
|
-
if (!command || !taskId || !generation) {
|
|
271
|
-
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`);
|
|
272
|
-
process.exitCode = 1;
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
let workflow;
|
|
276
|
-
try {
|
|
277
|
-
const args = withoutValue(commandArgs.slice(1), '--capability-ref');
|
|
278
|
-
if (/^\d+$/.test(args[0] ?? '')) {
|
|
279
|
-
const resolved = resolveVisibleActiveShortId(args[0]!);
|
|
280
|
-
if (resolved !== taskId) throw new Error('SANDBOX_TASK_REF_MISMATCH');
|
|
281
|
-
args[0] = resolved;
|
|
282
|
-
}
|
|
283
|
-
workflow = createTaskWorkflowRequest(command, args, taskId, generation);
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
287
|
-
process.exitCode = 1;
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
let response;
|
|
291
|
-
try {
|
|
292
|
-
const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
|
|
293
|
-
response = requestSandboxTaskWorkflow({
|
|
294
|
-
workflow,
|
|
295
|
-
...(authority ? { authority } : {})
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
300
|
-
writeClientError(error);
|
|
301
|
-
process.exitCode = error.detail.retryable ? 75 : 1;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
process.stdout.write(response.stdout);
|
|
305
|
-
process.stderr.write(response.stderr);
|
|
306
|
-
process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
139
|
let response;
|
|
310
140
|
try {
|
|
311
|
-
|
|
312
|
-
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
313
|
-
const proof = contextPath
|
|
314
|
-
? controllerProofFromContext(verifyCodexSandboxControllerContextWithWarnings(contextPath).context)
|
|
315
|
-
: null;
|
|
316
|
-
const authority = lifecycleAuthoritySelector(commandArgs);
|
|
317
|
-
response = requestSandboxTaskControl({
|
|
318
|
-
family,
|
|
319
|
-
args: commandArgs,
|
|
320
|
-
controllerProof: authority ? null : proof,
|
|
321
|
-
...(authority ? { authority } : {})
|
|
322
|
-
});
|
|
323
|
-
} else {
|
|
324
|
-
response = requestSandboxControl({ family, args: commandArgs });
|
|
325
|
-
}
|
|
141
|
+
response = requestSandboxControl({ family, args: commandArgs });
|
|
326
142
|
} catch (error) {
|
|
327
143
|
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
328
144
|
writeClientError(error);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.ts';
|
|
2
2
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
|
|
3
3
|
|
|
4
|
-
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
|
|
4
|
+
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`;
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
function taskArtifact(args: string[] = []): void {
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
import { lifecycleIntentCatalog } from '../task/lifecycle.ts';
|
|
8
8
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
|
|
9
9
|
import type { TaskLifecycleResult } from '../task/lifecycle.ts';
|
|
10
|
-
import type { LifecycleRecoveryResult } from '../
|
|
10
|
+
import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
|
|
11
11
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
12
12
|
|
|
13
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --
|
|
13
|
+
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`;
|
|
14
14
|
|
|
15
15
|
function usageFailure(message: string): void {
|
|
16
16
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -22,7 +22,7 @@ export type TaskOperationEffect = TaskViewAccessEffect;
|
|
|
22
22
|
export type TaskRefSource = 'argv' | 'environment' | 'none' | 'delegated' | 'input';
|
|
23
23
|
|
|
24
24
|
export type SandboxControlTransportDecision = Readonly<{
|
|
25
|
-
kind: 'direct-host' | 'broker-client' | 'fail-closed';
|
|
25
|
+
kind: 'direct-host' | 'sandbox-local' | 'broker-client' | 'fail-closed';
|
|
26
26
|
reasonCode: string | null;
|
|
27
27
|
}>;
|
|
28
28
|
|
|
@@ -90,6 +90,7 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
|
|
|
90
90
|
descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
|
|
91
91
|
descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
|
|
92
92
|
descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
|
|
93
|
+
descriptor('internal', 'platform-comment', 'stage-summary', 'task-bound', 'artifact-write'),
|
|
93
94
|
descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
|
|
94
95
|
descriptor('internal', 'platform-issue', 'inspect', 'task-bound', 'diagnostic'),
|
|
95
96
|
descriptor('internal', 'platform-issue', 'create', 'task-bound', 'remote-write'),
|
|
@@ -276,6 +277,7 @@ export function resolveTaskOperation(
|
|
|
276
277
|
export function resolveDelegatedTaskOperation(args: readonly string[]): TaskOperationDescriptor | null {
|
|
277
278
|
const [operation, ...rest] = args;
|
|
278
279
|
if (operation !== 'client' || !rest[0]) return null;
|
|
280
|
+
if (rest[0] === 'task-orchestration') return null;
|
|
279
281
|
return resolveTaskOperation('internal', rest[0], rest.slice(1));
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -339,7 +341,7 @@ function taskMarkerState(env: NodeJS.ProcessEnv): TaskMarkerState {
|
|
|
339
341
|
|
|
340
342
|
export function resolveSandboxControlTransport(
|
|
341
343
|
env: NodeJS.ProcessEnv = process.env,
|
|
342
|
-
options: Readonly<{ statusMountPath?: string }> = {}
|
|
344
|
+
options: Readonly<{ statusMountPath?: string; localWorkflow?: boolean }> = {}
|
|
343
345
|
): SandboxControlTransportDecision {
|
|
344
346
|
const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
|
|
345
347
|
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
@@ -406,13 +408,15 @@ export function resolveSandboxControlTransport(
|
|
|
406
408
|
if (!hasCompleteConfig) {
|
|
407
409
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
408
410
|
}
|
|
409
|
-
return { kind: 'broker-client', reasonCode: null };
|
|
411
|
+
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
410
412
|
}
|
|
411
413
|
if (!hasAnyMarker) return { kind: 'direct-host', reasonCode: null };
|
|
412
414
|
if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
|
|
413
415
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
414
416
|
}
|
|
415
|
-
return
|
|
417
|
+
return options.localWorkflow
|
|
418
|
+
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
|
|
419
|
+
: { kind: 'broker-client', reasonCode: null };
|
|
416
420
|
}
|
|
417
421
|
|
|
418
422
|
export function hasTaskBoundMarker(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -5,7 +5,7 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
|
5
5
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
6
6
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
|
|
7
7
|
|
|
8
|
-
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--
|
|
8
|
+
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';
|
|
9
9
|
|
|
10
10
|
function failUsage(message: string): void {
|
|
11
11
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -27,7 +27,7 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
27
27
|
let request;
|
|
28
28
|
try { request = parseReviewCommand(args); }
|
|
29
29
|
catch (error) { failUsage(error instanceof Error ? error.message : String(error)); return; }
|
|
30
|
-
const { stage, artifact,
|
|
30
|
+
const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
|
|
31
31
|
const operation = args[1]!;
|
|
32
32
|
const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
|
|
33
33
|
if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
|
|
@@ -38,8 +38,8 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
38
38
|
try {
|
|
39
39
|
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
|
|
40
40
|
let current = operation === 'preflight'
|
|
41
|
-
? preflightReviewSummary({ taskRef: args[0]!, stage, artifact,
|
|
42
|
-
: finalizeReviewSummary({ taskRef: args[0]!, stage, artifact,
|
|
41
|
+
? preflightReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
|
|
42
|
+
: finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
|
|
43
43
|
if (operation === 'preflight') return current;
|
|
44
44
|
if (current.status !== 'failed' || !overrideTicket) return current;
|
|
45
45
|
if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
|
|
@@ -49,7 +49,7 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
49
49
|
target: overrideTarget, scope: overrideScope
|
|
50
50
|
}, {
|
|
51
51
|
effectExecutor: (capability) => {
|
|
52
|
-
const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact,
|
|
52
|
+
const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
|
|
53
53
|
current = retried;
|
|
54
54
|
return retried.status === 'failed' || retried.status === 'planned'
|
|
55
55
|
? { 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'}` }
|
|
@@ -140,11 +140,6 @@ function snapshotValidation(repoRoot: string, commit: string, options: ValidateO
|
|
|
140
140
|
cleanupError = error;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
try {
|
|
144
|
-
git(repoRoot, ['worktree', 'prune']);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
cleanupError ??= error;
|
|
147
|
-
}
|
|
148
143
|
try {
|
|
149
144
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
150
145
|
} catch (error) {
|
|
@@ -189,6 +189,13 @@ function canonicalizeCommentBody(value: string): SanitizationResult<string> {
|
|
|
189
189
|
return sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
function canonicalizeSummaryBody(value: string): SanitizationResult<string> {
|
|
193
|
+
const sanitized = canonicalizeCommentBody(value);
|
|
194
|
+
return sanitized.ok
|
|
195
|
+
? ok(`${sanitized.value.replace(/\n+$/, '')}\n`)
|
|
196
|
+
: sanitized;
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
function renderSafeCodeFence(value: string, language = '', reservedMarkers: readonly RegExp[] = [CONTROL_MARKER_PATTERN]): string {
|
|
193
200
|
const content = markerSafe(normalized(value), reservedMarkers);
|
|
194
201
|
const longestRun = Math.max(0, ...(content.match(/`+/g) || []).map((run) => run.length));
|
|
@@ -249,6 +256,7 @@ export {
|
|
|
249
256
|
fenceRanges,
|
|
250
257
|
renderSafeCodeFence,
|
|
251
258
|
canonicalizeCommentBody,
|
|
259
|
+
canonicalizeSummaryBody,
|
|
252
260
|
sanitizeMarkdownDocument,
|
|
253
261
|
splitDocumentPlaceholder
|
|
254
262
|
};
|
|
@@ -331,13 +331,14 @@ function issueSnapshot(value: any, graph?: { type: any; values: Array<{ name: st
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
function commentSnapshot(value: any): RemoteCommentSnapshot {
|
|
334
|
+
function commentSnapshot(value: any, createdSequence: number | null = null): RemoteCommentSnapshot {
|
|
335
335
|
return {
|
|
336
336
|
id: String(value.id),
|
|
337
337
|
author: value.user?.login ? { id: String(value.user.login), name: String(value.user.login) } : null,
|
|
338
338
|
body: String(value.body || ''),
|
|
339
339
|
createdAt: String(value.created_at || ''),
|
|
340
340
|
updatedAt: String(value.updated_at || value.created_at || '')
|
|
341
|
+
,createdSequence
|
|
341
342
|
};
|
|
342
343
|
}
|
|
343
344
|
|
|
@@ -603,7 +604,15 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
|
|
|
603
604
|
const response = client.json<any>(['api', '--paginate', '--slurp', `repos/${repository(context)}/issues/${number}/comments?per_page=100`], { cwd: context.workingDirectory });
|
|
604
605
|
if (!response.ok) return response;
|
|
605
606
|
const values = Array.isArray(response.value) ? response.value.flatMap((entry: any) => Array.isArray(entry) ? entry : [entry]) : [];
|
|
606
|
-
|
|
607
|
+
const comments = values.filter((entry: any) => entry && typeof entry === 'object');
|
|
608
|
+
let previous = 0;
|
|
609
|
+
const ordered = comments.length === values.length && comments.every((entry: any) => {
|
|
610
|
+
const id = Number(entry.id);
|
|
611
|
+
if (!Number.isSafeInteger(id) || id <= previous) return false;
|
|
612
|
+
previous = id;
|
|
613
|
+
return true;
|
|
614
|
+
});
|
|
615
|
+
return { ok: true, value: comments.map((comment: any) => commentSnapshot(comment, ordered ? Number(comment.id) : null)) };
|
|
607
616
|
},
|
|
608
617
|
async write({ context, parent, body, existingComment }) {
|
|
609
618
|
const number = resourceIdentityNumber(parent);
|
|
@@ -33,7 +33,12 @@ import {
|
|
|
33
33
|
} from './comment-safety.ts';
|
|
34
34
|
import type { FenceRange } from './comment-safety.ts';
|
|
35
35
|
|
|
36
|
-
type RemoteComment = {
|
|
36
|
+
type RemoteComment = {
|
|
37
|
+
id: number | string;
|
|
38
|
+
body: string;
|
|
39
|
+
user?: { login?: string };
|
|
40
|
+
createdSequence?: number | null;
|
|
41
|
+
};
|
|
37
42
|
type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
|
|
38
43
|
type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
|
|
39
44
|
type SyncOptions = {
|
|
@@ -45,6 +50,7 @@ type SyncOptions = {
|
|
|
45
50
|
backfill?: boolean;
|
|
46
51
|
client?: PlatformClient;
|
|
47
52
|
runtimeVersion?: string;
|
|
53
|
+
summaryAuthorization?: { sha256: string };
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
@@ -68,8 +74,6 @@ const ARTIFACT_TITLES: Record<string, string> = {
|
|
|
68
74
|
'review-plan': '技术方案审查',
|
|
69
75
|
code: '实现报告',
|
|
70
76
|
'review-code': '代码审查',
|
|
71
|
-
'manual-validation': '人工验证报告',
|
|
72
|
-
'validation-run': '验证运行证据',
|
|
73
77
|
'pr-review': 'PR 审查报告'
|
|
74
78
|
};
|
|
75
79
|
|
|
@@ -146,7 +150,7 @@ function taskCommentLanguage(repoRoot: string): string {
|
|
|
146
150
|
|
|
147
151
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
148
152
|
const stem = path.basename(artifact, '.md');
|
|
149
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|
|
|
153
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
150
154
|
if (!match) throw new Error(`unsupported artifact '${artifact}'`);
|
|
151
155
|
const base = ARTIFACT_TITLES[match[1]!]!;
|
|
152
156
|
const round = match[2] ? Number(match[2]) : 1;
|
|
@@ -457,6 +461,40 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
|
|
|
457
461
|
});
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function taskManagedComment(comment: RemoteComment, taskId: string): boolean {
|
|
465
|
+
const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
|
|
466
|
+
return marker === MARKERS.task(taskId)
|
|
467
|
+
|| marker === MARKERS.summary(taskId)
|
|
468
|
+
|| marker.startsWith(`<!-- sync-issue:${taskId}:`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boolean; known: boolean; summary: RemoteComment | null } {
|
|
472
|
+
const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
|
|
473
|
+
const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
|
|
474
|
+
if (summary.length !== 1) return { ok: false, known: false, summary: null };
|
|
475
|
+
if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
|
|
476
|
+
return { ok: false, known: false, summary: summary[0]! };
|
|
477
|
+
}
|
|
478
|
+
const sequence = summary[0]!.createdSequence!;
|
|
479
|
+
return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
|
|
483
|
+
return provider.comments?.list
|
|
484
|
+
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
|
|
485
|
+
? {
|
|
486
|
+
ok: true as const,
|
|
487
|
+
value: response.value.map((comment: any) => ({
|
|
488
|
+
id: providerCommentId(comment.id, provider),
|
|
489
|
+
body: comment.body,
|
|
490
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined,
|
|
491
|
+
createdSequence: comment.createdSequence
|
|
492
|
+
}))
|
|
493
|
+
}
|
|
494
|
+
: response)
|
|
495
|
+
: unsupportedProviderOperation(provider, 'comments.list');
|
|
496
|
+
}
|
|
497
|
+
|
|
460
498
|
function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
|
|
461
499
|
const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
|
|
462
500
|
if (new Set(markers).size !== markers.length) return { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
|
|
@@ -527,18 +565,7 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
527
565
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
528
566
|
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
529
567
|
const issue = resourceIdentityNumber(issueIdentityFromTask);
|
|
530
|
-
const listed = loaded.value.provider.
|
|
531
|
-
? await loaded.value.provider.comments.list({
|
|
532
|
-
context: providerOperationContext(loaded.value),
|
|
533
|
-
parent: issueIdentityFromTask
|
|
534
|
-
}).then((response) => response.ok
|
|
535
|
-
? { ok: true as const, value: response.value.map((comment) => ({
|
|
536
|
-
id: providerCommentId(comment.id, loaded.value.provider),
|
|
537
|
-
body: comment.body,
|
|
538
|
-
user: comment.author?.name ? { login: comment.author.name } : undefined
|
|
539
|
-
})) }
|
|
540
|
-
: response)
|
|
541
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
568
|
+
const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
542
569
|
if (!listed.ok) {
|
|
543
570
|
return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
|
|
544
571
|
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
@@ -569,7 +596,6 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
569
596
|
}
|
|
570
597
|
}
|
|
571
598
|
|
|
572
|
-
|
|
573
599
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
574
600
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
575
601
|
const operations = existing.map((comment): PlatformOperation => ({
|
|
@@ -646,6 +672,65 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
646
672
|
}
|
|
647
673
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
648
674
|
}
|
|
675
|
+
if (options.kind === 'summary') {
|
|
676
|
+
const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
677
|
+
if (!refreshed.ok) {
|
|
678
|
+
return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
|
|
679
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const position = summaryPosition(refreshed.value, resolved.taskId);
|
|
683
|
+
if (!position.ok && !position.known) {
|
|
684
|
+
return platformResult('blocked', {
|
|
685
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
686
|
+
error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (!position.ok && position.summary) {
|
|
690
|
+
const owner = position.summary.user?.login;
|
|
691
|
+
if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
|
|
692
|
+
|| !owner || owner !== context.platform.currentUser) {
|
|
693
|
+
return platformResult('failed', {
|
|
694
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
695
|
+
error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const deleted = loaded.value.provider.comments?.delete
|
|
699
|
+
? await loaded.value.provider.comments.delete({
|
|
700
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
|
|
701
|
+
comment: { kind: 'id', value: String(position.summary.id) },
|
|
702
|
+
mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
|
|
703
|
+
})
|
|
704
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
705
|
+
if (!deleted.ok) {
|
|
706
|
+
return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
|
|
707
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const written = loaded.value.provider.comments?.write
|
|
711
|
+
? await loaded.value.provider.comments.write({
|
|
712
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
|
|
713
|
+
mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
|
|
714
|
+
})
|
|
715
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
716
|
+
if (!written.ok) {
|
|
717
|
+
return platformResult(written.error.retryable ? 'blocked' : 'failed', {
|
|
718
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
|
|
722
|
+
operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
|
|
723
|
+
const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
724
|
+
if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
|
|
725
|
+
const error = reconciled.ok
|
|
726
|
+
? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
|
|
727
|
+
: reconciled.error;
|
|
728
|
+
return platformResult(error.retryable ? 'blocked' : 'failed', {
|
|
729
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
649
734
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
650
735
|
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
651
736
|
...contextFields(context),
|
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
MechanicalChangeReport,
|
|
36
36
|
PrChangeReport
|
|
37
37
|
} from './pr-change-report.ts';
|
|
38
|
-
import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
|
|
38
|
+
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
|
|
39
39
|
import { readManualValidationCompletion } from '../task/manual-validation-completion.ts';
|
|
40
40
|
|
|
41
41
|
type SummaryComment = { id: number | string; body: string };
|
|
@@ -582,6 +582,7 @@ async function syncPullRequestSummary(
|
|
|
582
582
|
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
583
583
|
if (!initial.ok) return fail(initial.status, context, initial.error);
|
|
584
584
|
const manual = options.manualValidation;
|
|
585
|
+
let manualEvidenceDigest: string | null = null;
|
|
585
586
|
const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
|
|
586
587
|
if (hasFinalManualValidation && (!manual || manual.phase !== 'final')) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
|
|
587
588
|
if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
|
|
@@ -599,6 +600,7 @@ async function syncPullRequestSummary(
|
|
|
599
600
|
});
|
|
600
601
|
if (!completion.ok) return fail('failed', context, platformError(completion.error), prNumber);
|
|
601
602
|
const { receipt, transaction } = completion.value;
|
|
603
|
+
manualEvidenceDigest = receipt.evidenceDigest;
|
|
602
604
|
if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
|
|
603
605
|
if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', message: 'final summary body does not match the canonical receipt projection', retryable: false }, prNumber);
|
|
604
606
|
}
|
|
@@ -635,7 +637,13 @@ async function syncPullRequestSummary(
|
|
|
635
637
|
}
|
|
636
638
|
const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
|
|
637
639
|
if (!replaced.ok) return fail('failed', context, platformError(replaced.error));
|
|
638
|
-
const
|
|
640
|
+
const renderedBody = manual?.phase === 'final'
|
|
641
|
+
? replaced.value.replace(
|
|
642
|
+
MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
|
|
643
|
+
`<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
|
|
644
|
+
)
|
|
645
|
+
: replaced.value;
|
|
646
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
|
|
639
647
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
|
|
640
648
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
641
649
|
message: 'Summary contains an invalid or duplicated reserved control marker',
|
|
@@ -218,13 +218,17 @@ function issueType(value: unknown, label: string): NonNullable<IssueSnapshot['is
|
|
|
218
218
|
|
|
219
219
|
function validateComment(value: unknown): RemoteCommentSnapshot {
|
|
220
220
|
const item = record(value, 'comment');
|
|
221
|
-
exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt'], 'comment');
|
|
221
|
+
exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt', 'createdSequence'], 'comment');
|
|
222
222
|
return {
|
|
223
223
|
id: stringValue(item.id, 'comment.id'),
|
|
224
224
|
author: author(item.author, 'comment.author'),
|
|
225
225
|
body: stringValue(item.body, 'comment.body', true),
|
|
226
226
|
createdAt: utcTimestamp(item.createdAt, 'comment.createdAt'),
|
|
227
|
-
updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt')
|
|
227
|
+
updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt'),
|
|
228
|
+
createdSequence: item.createdSequence === null ? null : (() => {
|
|
229
|
+
if (!Number.isSafeInteger(item.createdSequence) || Number(item.createdSequence) < 1) throw new Error('comment.createdSequence must be a positive safe integer or null');
|
|
230
|
+
return Number(item.createdSequence);
|
|
231
|
+
})()
|
|
228
232
|
};
|
|
229
233
|
}
|
|
230
234
|
|