@fitlab-ai/agent-infra 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +120 -13
- package/dist/lib/platform/issues.js +3 -4
- 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 +161 -156
- package/dist/lib/sandbox/control/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +32 -41
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +27 -1
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +147 -278
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +31 -245
- package/dist/lib/task/orchestration.js +8 -24
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +47 -218
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +132 -13
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +178 -179
- package/lib/sandbox/control/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +31 -48
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +27 -1
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +155 -287
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +29 -285
- package/lib/task/orchestration.ts +12 -41
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +46 -200
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
- 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/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
- 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-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
|
@@ -44,6 +44,7 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
|
|
47
|
+
const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
|
|
47
48
|
|
|
48
49
|
function manualSummaryBody(body: string, phase: 'pending' | 'final', transactionId: string, receiptDigest = '', evidenceDigest = '', prHeadSha = ''): string {
|
|
49
50
|
const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
|
|
@@ -55,10 +56,13 @@ function manualSummaryBody(body: string, phase: 'pending' | 'final', transaction
|
|
|
55
56
|
? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
|
|
56
57
|
: `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
|
|
57
58
|
let inserted = false;
|
|
58
|
-
const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
|
|
59
|
+
const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
|
|
59
60
|
if (inserted) return '';
|
|
60
61
|
inserted = true;
|
|
61
|
-
|
|
62
|
+
const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
|
|
63
|
+
return trailingPlaceholder
|
|
64
|
+
? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
|
|
65
|
+
: `${section}\n\n`;
|
|
62
66
|
});
|
|
63
67
|
return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
|
|
64
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
5
6
|
import {
|
|
@@ -8,15 +9,19 @@ import {
|
|
|
8
9
|
syncPlatformComment
|
|
9
10
|
} from '../platform/issue-comments.ts';
|
|
10
11
|
import type { CommentKind } from '../platform/issue-comments.ts';
|
|
12
|
+
import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
13
|
+
import { platformResult } from '../platform/types.ts';
|
|
11
14
|
import type { PlatformResult } from '../platform/types.ts';
|
|
12
15
|
import { recoverTaskFromComments } from '../platform/task-recovery.ts';
|
|
13
16
|
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
14
17
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
18
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
15
19
|
|
|
16
20
|
const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
|
|
17
21
|
agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
|
|
18
22
|
agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
|
|
19
23
|
agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
|
|
24
|
+
agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
|
|
20
25
|
agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
|
|
21
26
|
`;
|
|
22
27
|
|
|
@@ -64,6 +69,7 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
64
69
|
internalHandlerRoute('platform-comment', 'recover', operation),
|
|
65
70
|
internalHandlerRoute('platform-comment', 'owner', operation),
|
|
66
71
|
internalHandlerRoute('platform-comment', 'backfill', operation),
|
|
72
|
+
internalHandlerRoute('platform-comment', 'stage-summary', operation),
|
|
67
73
|
internalHandlerRoute('platform-comment', 'sync', operation)
|
|
68
74
|
].some(Boolean)) { fail('a valid operation is required'); return; }
|
|
69
75
|
const hasTaskRef = operation !== 'list' && operation !== 'recover';
|
|
@@ -144,6 +150,38 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
144
150
|
finish(await backfillCompletionComments(taskRef!, { cwd, agent: normalizedAgent }));
|
|
145
151
|
return;
|
|
146
152
|
}
|
|
153
|
+
if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
|
|
154
|
+
const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
|
|
155
|
+
if (unexpected) { fail(`stage-summary does not accept '--${unexpected}'`); return; }
|
|
156
|
+
if (typeof parsed.values.bodyFile !== 'string') { fail('stage-summary requires --body-file'); return; }
|
|
157
|
+
let body: string;
|
|
158
|
+
try { body = readBodyFile(parsed.values.bodyFile, cwd); }
|
|
159
|
+
catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
|
|
160
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
161
|
+
if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
|
|
162
|
+
body = canonical.value;
|
|
163
|
+
const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
|
|
164
|
+
if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
|
|
165
|
+
const target = path.join(resolved.taskDir, '.delivery-summary.json');
|
|
166
|
+
const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
|
|
167
|
+
const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
168
|
+
const next = `${JSON.stringify(value)}\n`;
|
|
169
|
+
if (previous === next) {
|
|
170
|
+
finish(platformResult('no-op'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
176
|
+
fs.renameSync(temporary, target);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
|
|
179
|
+
fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
147
185
|
if (!internalHandlerRoute('platform-comment', 'sync', operation)) { fail('operation is not registered'); return; }
|
|
148
186
|
const kind = parsed.values.kind;
|
|
149
187
|
const agent = parsed.values.agent;
|
|
@@ -1,11 +1,7 @@
|
|
|
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
5
|
requestSandboxTaskWorkflow,
|
|
10
6
|
resolveVisibleActiveShortId,
|
|
11
7
|
SandboxControlClientError
|
|
@@ -13,129 +9,9 @@ import {
|
|
|
13
9
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
14
10
|
import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
15
11
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
|
|
16
|
-
import {
|
|
17
|
-
controllerProofFromContext,
|
|
18
|
-
verifyCodexSandboxControllerContextWithWarnings
|
|
19
|
-
} from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
20
12
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
21
13
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
|
|
22
14
|
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
15
|
|
|
140
16
|
type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
|
|
141
17
|
|
|
@@ -274,7 +150,7 @@ async function sandboxControl(args: string[]): Promise<void> {
|
|
|
274
150
|
}
|
|
275
151
|
let workflow;
|
|
276
152
|
try {
|
|
277
|
-
const args =
|
|
153
|
+
const args = commandArgs.slice(1);
|
|
278
154
|
if (/^\d+$/.test(args[0] ?? '')) {
|
|
279
155
|
const resolved = resolveVisibleActiveShortId(args[0]!);
|
|
280
156
|
if (resolved !== taskId) throw new Error('SANDBOX_TASK_REF_MISMATCH');
|
|
@@ -289,11 +165,7 @@ async function sandboxControl(args: string[]): Promise<void> {
|
|
|
289
165
|
}
|
|
290
166
|
let response;
|
|
291
167
|
try {
|
|
292
|
-
|
|
293
|
-
response = requestSandboxTaskWorkflow({
|
|
294
|
-
workflow,
|
|
295
|
-
...(authority ? { authority } : {})
|
|
296
|
-
});
|
|
168
|
+
response = requestSandboxTaskWorkflow({ workflow });
|
|
297
169
|
}
|
|
298
170
|
catch (error) {
|
|
299
171
|
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
@@ -308,21 +180,7 @@ async function sandboxControl(args: string[]): Promise<void> {
|
|
|
308
180
|
}
|
|
309
181
|
let response;
|
|
310
182
|
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
|
-
}
|
|
183
|
+
response = requestSandboxControl({ family, args: commandArgs });
|
|
326
184
|
} catch (error) {
|
|
327
185
|
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
328
186
|
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 {
|
|
@@ -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'),
|
|
@@ -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);
|
|
@@ -27,13 +27,19 @@ import {
|
|
|
27
27
|
import { projectTaskComment } from './task-comment-projection.ts';
|
|
28
28
|
import {
|
|
29
29
|
canonicalizeCommentBody,
|
|
30
|
+
canonicalizeSummaryBody,
|
|
30
31
|
escapeHtmlText,
|
|
31
32
|
fenceRanges,
|
|
32
33
|
renderSafeCodeFence
|
|
33
34
|
} from './comment-safety.ts';
|
|
34
35
|
import type { FenceRange } from './comment-safety.ts';
|
|
35
36
|
|
|
36
|
-
type RemoteComment = {
|
|
37
|
+
type RemoteComment = {
|
|
38
|
+
id: number | string;
|
|
39
|
+
body: string;
|
|
40
|
+
user?: { login?: string };
|
|
41
|
+
createdSequence?: number | null;
|
|
42
|
+
};
|
|
37
43
|
type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
|
|
38
44
|
type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
|
|
39
45
|
type SyncOptions = {
|
|
@@ -45,6 +51,8 @@ type SyncOptions = {
|
|
|
45
51
|
backfill?: boolean;
|
|
46
52
|
client?: PlatformClient;
|
|
47
53
|
runtimeVersion?: string;
|
|
54
|
+
summaryAuthorization?: { sha256: string };
|
|
55
|
+
verifyOnly?: boolean;
|
|
48
56
|
};
|
|
49
57
|
|
|
50
58
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
@@ -457,6 +465,49 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
|
|
|
457
465
|
});
|
|
458
466
|
}
|
|
459
467
|
|
|
468
|
+
function taskManagedComment(comment: RemoteComment, taskId: string): boolean {
|
|
469
|
+
const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
|
|
470
|
+
return marker === MARKERS.task(taskId)
|
|
471
|
+
|| marker === MARKERS.summary(taskId)
|
|
472
|
+
|| marker.startsWith(`<!-- sync-issue:${taskId}:`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boolean; known: boolean; summary: RemoteComment | null } {
|
|
476
|
+
const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
|
|
477
|
+
const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
|
|
478
|
+
if (summary.length !== 1) return { ok: false, known: false, summary: null };
|
|
479
|
+
if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
|
|
480
|
+
return { ok: false, known: false, summary: summary[0]! };
|
|
481
|
+
}
|
|
482
|
+
const sequence = summary[0]!.createdSequence!;
|
|
483
|
+
return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function summaryDigest(comment: RemoteComment): string {
|
|
487
|
+
const body = normalizeCommentContent(comment.body)
|
|
488
|
+
.replace(/^<!-- sync-issue:[^\n]+:summary -->\n## [^\n]+\n\n> [^\n]+\n\n/u, '')
|
|
489
|
+
.replace(/^<details><summary>恢复元数据<\/summary>[\s\S]*?<\/details>\n\n/u, '')
|
|
490
|
+
.replace(/\n---\n\*[^\n]*\*$/u, '');
|
|
491
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
492
|
+
return canonical.ok ? createHash('sha256').update(canonical.value).digest('hex') : '';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
|
|
496
|
+
return provider.comments?.list
|
|
497
|
+
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
|
|
498
|
+
? {
|
|
499
|
+
ok: true as const,
|
|
500
|
+
value: response.value.map((comment: any) => ({
|
|
501
|
+
id: providerCommentId(comment.id, provider),
|
|
502
|
+
body: comment.body,
|
|
503
|
+
user: comment.author?.name ? { login: comment.author.name } : undefined,
|
|
504
|
+
createdSequence: comment.createdSequence
|
|
505
|
+
}))
|
|
506
|
+
}
|
|
507
|
+
: response)
|
|
508
|
+
: unsupportedProviderOperation(provider, 'comments.list');
|
|
509
|
+
}
|
|
510
|
+
|
|
460
511
|
function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
|
|
461
512
|
const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
|
|
462
513
|
if (new Set(markers).size !== markers.length) return { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
|
|
@@ -527,18 +578,7 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
527
578
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
528
579
|
if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
|
|
529
580
|
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');
|
|
581
|
+
const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
542
582
|
if (!listed.ok) {
|
|
543
583
|
return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
|
|
544
584
|
...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
|
|
@@ -569,6 +609,26 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
569
609
|
}
|
|
570
610
|
}
|
|
571
611
|
|
|
612
|
+
if (options.verifyOnly) {
|
|
613
|
+
if (options.kind !== 'summary' || !options.summaryAuthorization) {
|
|
614
|
+
return platformResult('failed', {
|
|
615
|
+
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
616
|
+
error: { code: 'SUMMARY_VERIFICATION_INVALID', message: 'summary verification requires durable authorization', retryable: false }
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const position = summaryPosition(listed.value, resolved.taskId);
|
|
620
|
+
if (!position.ok || !position.summary || summaryDigest(position.summary) !== options.summaryAuthorization.sha256) {
|
|
621
|
+
return platformResult('failed', {
|
|
622
|
+
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
623
|
+
error: { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary marker, body digest, or managed comment order is invalid', retryable: true }
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return platformResult('no-op', {
|
|
627
|
+
...contextFields(context), changed: false, resource: { kind: 'issue', number: issue },
|
|
628
|
+
comment: { kind: options.kind, marker: desired[0]!.marker, ids: [position.summary.id], parts: 1 }, error: null
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
572
632
|
|
|
573
633
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
574
634
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
@@ -646,6 +706,65 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
646
706
|
}
|
|
647
707
|
operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
|
|
648
708
|
}
|
|
709
|
+
if (options.kind === 'summary') {
|
|
710
|
+
const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
711
|
+
if (!refreshed.ok) {
|
|
712
|
+
return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
|
|
713
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
const position = summaryPosition(refreshed.value, resolved.taskId);
|
|
717
|
+
if (!position.ok && !position.known) {
|
|
718
|
+
return platformResult('blocked', {
|
|
719
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
720
|
+
error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (!position.ok && position.summary) {
|
|
724
|
+
const owner = position.summary.user?.login;
|
|
725
|
+
if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
|
|
726
|
+
|| !owner || owner !== context.platform.currentUser) {
|
|
727
|
+
return platformResult('failed', {
|
|
728
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
|
|
729
|
+
error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const deleted = loaded.value.provider.comments?.delete
|
|
733
|
+
? await loaded.value.provider.comments.delete({
|
|
734
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
|
|
735
|
+
comment: { kind: 'id', value: String(position.summary.id) },
|
|
736
|
+
mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
|
|
737
|
+
})
|
|
738
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
|
|
739
|
+
if (!deleted.ok) {
|
|
740
|
+
return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
|
|
741
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const written = loaded.value.provider.comments?.write
|
|
745
|
+
? await loaded.value.provider.comments.write({
|
|
746
|
+
context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
|
|
747
|
+
mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
|
|
748
|
+
})
|
|
749
|
+
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
750
|
+
if (!written.ok) {
|
|
751
|
+
return platformResult(written.error.retryable ? 'blocked' : 'failed', {
|
|
752
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
|
|
756
|
+
operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
|
|
757
|
+
const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
|
|
758
|
+
if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
|
|
759
|
+
const error = reconciled.ok
|
|
760
|
+
? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
|
|
761
|
+
: reconciled.error;
|
|
762
|
+
return platformResult(error.retryable ? 'blocked' : 'failed', {
|
|
763
|
+
...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
649
768
|
const changed = operations.some((operation) => operation.status === 'applied');
|
|
650
769
|
const result = platformResult(changed ? 'applied' : 'no-op', {
|
|
651
770
|
...contextFields(context),
|
package/lib/platform/issues.ts
CHANGED
|
@@ -574,9 +574,8 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
574
574
|
const failure = plan.operations.find((operation) => operation.status === 'failed');
|
|
575
575
|
if (failure) return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
|
|
576
576
|
const planned = plan.operations.filter((operation) => operation.status === 'planned');
|
|
577
|
-
|
|
578
|
-
if (
|
|
579
|
-
if (planned.length === 0) return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
577
|
+
if (options.dryRun) return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
578
|
+
if (planned.length === 0) return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
|
|
580
579
|
const payload = applyRestOperations(snapshot, planned) as Partial<{
|
|
581
580
|
title: string; body: string; labels: string[]; assignees: string[];
|
|
582
581
|
milestone: string | null; state: 'open' | 'closed'; fields: Record<string, string | number | null>;
|
|
@@ -611,7 +610,7 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
611
610
|
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
|
|
612
611
|
});
|
|
613
612
|
}
|
|
614
|
-
return result(
|
|
613
|
+
return result('applied', base.resolved.taskId, base.issueNumber, {
|
|
615
614
|
changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
616
615
|
resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
|
|
617
616
|
operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|