@fitlab-ai/agent-infra 0.9.15 → 0.10.0
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/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +0 -1
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/issue-comments.js +16 -10
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +6 -2
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/workflow-executor.js +4 -14
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/task/artifact-command.js +6 -28
- package/dist/lib/task/artifact-operations.js +16 -230
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +73 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +38 -7
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +164 -22
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +66 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +148 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +1 -0
- package/dist/lib/task/review-finalization.js +104 -77
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +0 -1
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +0 -1
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/issue-comments.ts +18 -10
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +6 -2
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/protocol.ts +5 -0
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/workflow-executor.ts +4 -14
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/task/artifact-command.ts +8 -27
- package/lib/task/artifact-operations.ts +21 -251
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +99 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +47 -14
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +187 -23
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +50 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +176 -132
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +1 -0
- package/lib/task/review-finalization.ts +106 -87
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +0 -1
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- 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 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- 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 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
|
@@ -7,9 +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 '../task/lifecycle-recovery.ts';
|
|
10
11
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
11
12
|
|
|
12
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
|
|
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: --stage <stage> --round <round> --artifact <artifact> --reason <reason>\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
|
|
13
14
|
|
|
14
15
|
function usageFailure(message: string): void {
|
|
15
16
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -61,9 +62,9 @@ async function taskLifecycle(args: string[] = []): Promise<void> {
|
|
|
61
62
|
const result = await dispatchTaskControlOperation(
|
|
62
63
|
createDirectHostExecutionContext({ repoRoot }),
|
|
63
64
|
operation
|
|
64
|
-
) as TaskLifecycleResult & { humanOverride?: unknown };
|
|
65
|
+
) as (TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown };
|
|
65
66
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
66
|
-
if (result.status === 'failed') process.exitCode = 1;
|
|
67
|
+
if (result.status === 'failed' || result.status === 'owner-unknown' || result.status === 'conflict') process.exitCode = 1;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export { taskLifecycle };
|
|
@@ -136,7 +136,6 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
|
|
|
136
136
|
descriptor('internal', 'task-activity', 'pr-review-terminate', 'task-bound', 'progress'),
|
|
137
137
|
descriptor('internal', 'task-artifact', 'inspect', 'task-bound', 'diagnostic'),
|
|
138
138
|
descriptor('internal', 'task-artifact', 'init', 'task-bound', 'artifact-write'),
|
|
139
|
-
descriptor('internal', 'task-artifact', 'repair', 'task-bound', 'artifact-write'),
|
|
140
139
|
descriptor('internal', 'task-artifact', 'finalize-local', 'task-bound', 'artifact-write'),
|
|
141
140
|
descriptor('internal', 'task-orchestration', 'status', 'task-bound', 'diagnostic'),
|
|
142
141
|
descriptor('internal', 'task-orchestration', 'progress', 'task-bound', 'progress'),
|
|
@@ -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> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
|
|
8
|
+
const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--recovery-id <id>] [--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, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
|
|
30
|
+
const { stage, artifact, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
|
|
31
31
|
const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
|
|
32
32
|
if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
|
|
33
33
|
const resolved = resolveTaskRef(args[0]!);
|
|
@@ -36,7 +36,7 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
36
36
|
let humanOverride: unknown = null;
|
|
37
37
|
try {
|
|
38
38
|
result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', async () => {
|
|
39
|
-
let current = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
|
|
39
|
+
let current = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
|
|
40
40
|
if (current.status !== 'failed' || !overrideTicket) return current;
|
|
41
41
|
if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
|
|
42
42
|
const consumed = await consumeHumanOverride({
|
|
@@ -45,7 +45,7 @@ async function taskReview(args: string[] = []): Promise<void> {
|
|
|
45
45
|
target: overrideTarget, scope: overrideScope
|
|
46
46
|
}, {
|
|
47
47
|
effectExecutor: (capability) => {
|
|
48
|
-
const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
|
|
48
|
+
const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
|
|
49
49
|
current = retried;
|
|
50
50
|
return retried.status === 'failed' || retried.status === 'planned'
|
|
51
51
|
? { 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'}` }
|
|
@@ -50,6 +50,7 @@ const MARKERS = {
|
|
|
50
50
|
summary: (taskId: string) => `<!-- sync-issue:${taskId}:summary -->`,
|
|
51
51
|
cancel: (taskId: string) => `<!-- sync-issue:${taskId}:cancel -->`
|
|
52
52
|
};
|
|
53
|
+
const COMMENT_BYTE_LIMIT = 60_000;
|
|
53
54
|
|
|
54
55
|
const ARTIFACT_TITLES: Record<string, string> = {
|
|
55
56
|
analysis: '需求分析',
|
|
@@ -67,6 +68,10 @@ function normalizeCommentContent(content: string): string {
|
|
|
67
68
|
return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function isTaskCommentTooLarge(content: string): boolean {
|
|
72
|
+
return Buffer.byteLength(content, 'utf8') > COMMENT_BYTE_LIMIT;
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
function splitFrontmatter(content: string): { frontmatter: string | null; body: string } {
|
|
71
76
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
72
77
|
if (!match) return { frontmatter: null, body: content };
|
|
@@ -269,7 +274,7 @@ function chunkArtifactComment(input: {
|
|
|
269
274
|
byteLimit?: number;
|
|
270
275
|
backfill?: boolean;
|
|
271
276
|
}): RenderedChunk[] {
|
|
272
|
-
const byteLimit = input.byteLimit ||
|
|
277
|
+
const byteLimit = input.byteLimit || COMMENT_BYTE_LIMIT;
|
|
273
278
|
const identity = artifactIdentity(input.artifact);
|
|
274
279
|
const safeBody = sanitizeCommentBody(input.body, 'artifact body');
|
|
275
280
|
const single = buildArtifactChunk(
|
|
@@ -323,12 +328,6 @@ function flattenComments(value: unknown): RemoteComment[] {
|
|
|
323
328
|
);
|
|
324
329
|
}
|
|
325
330
|
|
|
326
|
-
function issueNumberFromTask(content: string): number | null {
|
|
327
|
-
const value = parseTaskFrontmatter(content).issue_number;
|
|
328
|
-
const number = Number(value);
|
|
329
|
-
return Number.isInteger(number) && number > 0 ? number : null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
331
|
function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
|
|
333
332
|
const result = client.json<unknown>([
|
|
334
333
|
'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
|
|
@@ -452,11 +451,18 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
452
451
|
}
|
|
453
452
|
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
454
453
|
let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
|
|
455
|
-
try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent)
|
|
454
|
+
try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent)); }
|
|
456
455
|
catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
457
456
|
if (!issueIdentityFromTask) {
|
|
458
457
|
return platformResult('no-op', {
|
|
459
|
-
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid
|
|
458
|
+
error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (options.kind === 'task' && isTaskCommentTooLarge(taskContent)) {
|
|
462
|
+
return platformResult('no-op', {
|
|
463
|
+
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
|
|
464
|
+
operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
|
|
465
|
+
error: null
|
|
460
466
|
});
|
|
461
467
|
}
|
|
462
468
|
let desired: RenderedChunk[];
|
|
@@ -614,7 +620,7 @@ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: strin
|
|
|
614
620
|
if (!resolved.ok) return platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } });
|
|
615
621
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
616
622
|
let issueIdentity: ReturnType<typeof taskIssueIdentity>;
|
|
617
|
-
try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content)
|
|
623
|
+
try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content)); }
|
|
618
624
|
catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
619
625
|
if (!issueIdentity) return platformResult('no-op', { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
|
|
620
626
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
@@ -640,6 +646,7 @@ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: strin
|
|
|
640
646
|
}
|
|
641
647
|
|
|
642
648
|
export {
|
|
649
|
+
COMMENT_BYTE_LIMIT,
|
|
643
650
|
MARKERS,
|
|
644
651
|
chunkArtifactComment,
|
|
645
652
|
findMarkerComments,
|
|
@@ -648,6 +655,7 @@ export {
|
|
|
648
655
|
listPlatformComments,
|
|
649
656
|
normalizeCommentContent,
|
|
650
657
|
renderTaskComment,
|
|
658
|
+
isTaskCommentTooLarge,
|
|
651
659
|
syncPlatformComment,
|
|
652
660
|
validateRelatedMarkerSet,
|
|
653
661
|
writeComment
|
package/lib/platform/issues.ts
CHANGED
|
@@ -85,7 +85,7 @@ async function resolvedContext(taskRef: string, options: SharedOptions) {
|
|
|
85
85
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
86
86
|
const frontmatter = parseTaskFrontmatter(content);
|
|
87
87
|
let issueIdentity: ResourceIdentity | null;
|
|
88
|
-
try { issueIdentity = taskIssueIdentity(frontmatter
|
|
88
|
+
try { issueIdentity = taskIssueIdentity(frontmatter); }
|
|
89
89
|
catch (error) {
|
|
90
90
|
return { ok: false as const, output: result('failed', resolved.taskId, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
|
|
91
91
|
}
|
|
@@ -3,9 +3,11 @@ import path from 'node:path';
|
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { escapeHtmlText } from './comment-safety.ts';
|
|
6
|
+
import { isResourceIdentity } from './resource-identity.ts';
|
|
7
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
6
8
|
|
|
7
9
|
const REPORT_FILE_NAME = 'pr-change-report.json';
|
|
8
|
-
const REPORT_VERSION =
|
|
10
|
+
const REPORT_VERSION = 2 as const;
|
|
9
11
|
const CANONICAL_REPORT_PLACEHOLDER = '<!-- canonical-pr-change-report -->';
|
|
10
12
|
const CANONICAL_REPORT_HEADING = '### PR 代码增减';
|
|
11
13
|
const PRECHECK_IDS = [
|
|
@@ -83,7 +85,7 @@ type PrecheckCandidate = {
|
|
|
83
85
|
|
|
84
86
|
type PullRequestIdentity = {
|
|
85
87
|
repository: string;
|
|
86
|
-
|
|
88
|
+
resource: ResourceIdentity;
|
|
87
89
|
base: { repository: string; ref: string; sha: string };
|
|
88
90
|
head: { repository: string; ref: string; sha: string };
|
|
89
91
|
};
|
|
@@ -279,8 +281,8 @@ function validatePrChangeReport(value: unknown): ValidationResult<PrChangeReport
|
|
|
279
281
|
}
|
|
280
282
|
const identity = value.identity;
|
|
281
283
|
const identityRepository = identity.repository;
|
|
282
|
-
const
|
|
283
|
-
if (!isNonEmptyString(identityRepository) ||
|
|
284
|
+
const identityResource = identity.resource;
|
|
285
|
+
if (!isNonEmptyString(identityRepository) || !isResourceIdentity(identityResource)) return invalid('change report identity is invalid');
|
|
284
286
|
const parseIdentityPart = (part: unknown, name: string): ValidationResult<{ repository: string; ref: string; sha: string }> => {
|
|
285
287
|
if (!isRecord(part) || !isNonEmptyString(part.repository) || !isNonEmptyString(part.ref) || !isSha(part.sha)) return invalid(`change report identity.${name} is invalid`);
|
|
286
288
|
return { ok: true, value: { repository: part.repository, ref: part.ref, sha: part.sha } };
|
|
@@ -315,7 +317,7 @@ function validatePrChangeReport(value: unknown): ValidationResult<PrChangeReport
|
|
|
315
317
|
version: REPORT_VERSION,
|
|
316
318
|
identity: {
|
|
317
319
|
repository: identityRepository,
|
|
318
|
-
|
|
320
|
+
resource: identityResource,
|
|
319
321
|
base: base.value,
|
|
320
322
|
head: head.value
|
|
321
323
|
},
|
|
@@ -16,6 +16,8 @@ import { platformResult } from './types.ts';
|
|
|
16
16
|
import type { PlatformResult } from './types.ts';
|
|
17
17
|
import type { OperationWarning } from '../task/operation-outcome.ts';
|
|
18
18
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
19
|
+
import { resourceIdentityEquals } from './resource-identity.ts';
|
|
20
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
19
21
|
import type { PlatformChangeRequestSnapshot } from './snapshots.ts';
|
|
20
22
|
import type { ChangeRequestSnapshot } from './provider-contract.ts';
|
|
21
23
|
import {
|
|
@@ -102,12 +104,17 @@ function taskReportPath(taskDir: string): string {
|
|
|
102
104
|
return path.join(taskDir, 'pr-change-report.json');
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
function readBoundTaskSnapshot(taskMdPath: string): {
|
|
107
|
+
function readBoundTaskSnapshot(taskMdPath: string): {
|
|
108
|
+
taskContent: string;
|
|
109
|
+
boundFact: Extract<PrDeliveryFact, { state: 'bound' }> | null;
|
|
110
|
+
factError: { code: string; message: string } | null;
|
|
111
|
+
} {
|
|
106
112
|
const taskContent = fs.readFileSync(taskMdPath, 'utf8');
|
|
107
113
|
const frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
108
114
|
const fact = readPrDeliveryFact(frontmatter);
|
|
109
115
|
const boundFact = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact : null;
|
|
110
|
-
|
|
116
|
+
const factError = fact.status === 'invalid' ? { code: fact.error.code, message: fact.error.message } : null;
|
|
117
|
+
return { taskContent, boundFact, factError };
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
function boundPrNumberForWarning(taskMdPath: string): number | null {
|
|
@@ -189,7 +196,7 @@ function inspectionError(error: { code: string; message: string; retryable: bool
|
|
|
189
196
|
async function inspectBoundPullRequest(
|
|
190
197
|
context: PlatformResult,
|
|
191
198
|
repoRoot: string,
|
|
192
|
-
|
|
199
|
+
prIdentity: ResourceIdentity,
|
|
193
200
|
loadedContext?: LoadedContext
|
|
194
201
|
): Promise<{ ok: true; value: PlatformChangeRequestSnapshot } | { ok: false; error: PlatformResult['error']; status: PlatformResult['status'] }> {
|
|
195
202
|
if (!context.platform.repository) {
|
|
@@ -199,16 +206,15 @@ async function inspectBoundPullRequest(
|
|
|
199
206
|
? { ok: true as const, value: loadedContext }
|
|
200
207
|
: await resolvePlatformProviderContext({ cwd: repoRoot });
|
|
201
208
|
if (!loaded.ok) return { ok: false, status: loaded.context.status, error: loaded.context.error };
|
|
202
|
-
const target = providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber));
|
|
203
209
|
const inspected = loaded.value.provider.changeRequests?.inspect
|
|
204
|
-
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target })
|
|
210
|
+
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: prIdentity })
|
|
205
211
|
: unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
|
|
206
212
|
if (!inspected.ok) {
|
|
207
213
|
const error = providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED');
|
|
208
214
|
return { ok: false, status: inspectionError(error), error };
|
|
209
215
|
}
|
|
210
216
|
const remote: ChangeRequestSnapshot = inspected.value;
|
|
211
|
-
const number = remote.number ?? resourceIdentityNumber(remote.identity) ??
|
|
217
|
+
const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
|
|
212
218
|
const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
|
|
213
219
|
const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
|
|
214
220
|
return {
|
|
@@ -216,6 +222,7 @@ async function inspectBoundPullRequest(
|
|
|
216
222
|
value: {
|
|
217
223
|
repository: context.platform.repository,
|
|
218
224
|
number,
|
|
225
|
+
identity: remote.identity ?? prIdentity,
|
|
219
226
|
nodeId: remote.id,
|
|
220
227
|
url: remote.displayUrl || '',
|
|
221
228
|
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
@@ -237,21 +244,22 @@ async function inspectBoundPullRequest(
|
|
|
237
244
|
function sameIdentity(left: PlatformChangeRequestSnapshot, right: PlatformChangeRequestSnapshot): boolean {
|
|
238
245
|
return JSON.stringify({
|
|
239
246
|
repository: left.repository,
|
|
240
|
-
|
|
247
|
+
identity: left.identity ?? null,
|
|
241
248
|
base: left.base,
|
|
242
249
|
head: left.head
|
|
243
250
|
}) === JSON.stringify({
|
|
244
251
|
repository: right.repository,
|
|
245
|
-
|
|
252
|
+
identity: right.identity ?? null,
|
|
246
253
|
base: right.base,
|
|
247
254
|
head: right.head
|
|
248
255
|
});
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
function identityFromSnapshot(snapshot: PlatformChangeRequestSnapshot) {
|
|
259
|
+
if (!snapshot.identity) throw new Error('pull-request snapshot has no canonical identity');
|
|
252
260
|
return {
|
|
253
261
|
repository: snapshot.repository,
|
|
254
|
-
|
|
262
|
+
resource: snapshot.identity,
|
|
255
263
|
base: { ...snapshot.base },
|
|
256
264
|
head: { ...snapshot.head }
|
|
257
265
|
};
|
|
@@ -286,7 +294,7 @@ function currentReportCheck(
|
|
|
286
294
|
const digest = taskIntentDigest(taskContent);
|
|
287
295
|
if (!digest.ok) return { ok: false, error: platformError(digest.error) };
|
|
288
296
|
const reportIdentity = report.identity;
|
|
289
|
-
if (reportIdentity.repository !== snapshot.repository || reportIdentity.
|
|
297
|
+
if (reportIdentity.repository !== snapshot.repository || !resourceIdentityEquals(reportIdentity.resource, snapshot.identity) ||
|
|
290
298
|
JSON.stringify(reportIdentity.base) !== JSON.stringify(snapshot.base) || JSON.stringify(reportIdentity.head) !== JSON.stringify(snapshot.head)) return {
|
|
291
299
|
ok: false,
|
|
292
300
|
error: { code: 'PR_CHANGE_REPORT_STALE', message: 'Change report identity does not match the authoritative pull request snapshot', retryable: false }
|
|
@@ -343,14 +351,22 @@ async function summaryContext(taskRef: string, options: SummaryOptions = {}): Pr
|
|
|
343
351
|
};
|
|
344
352
|
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
345
353
|
const frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
346
|
-
const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
347
|
-
const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? resourceIdentityNumber(fact.fact.identity.resource) : null;
|
|
348
354
|
const reportPath = taskReportPath(resolved.taskDir);
|
|
355
|
+
const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
356
|
+
if (fact.status === 'invalid') return {
|
|
357
|
+
...platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }),
|
|
358
|
+
task: { id: resolved.taskId, prNumber: null },
|
|
359
|
+
pullRequest: null,
|
|
360
|
+
changeReport: { status: 'invalid', path: reportPath, taskIntentSha256: null, reason: fact.error.message },
|
|
361
|
+
artifacts: []
|
|
362
|
+
};
|
|
363
|
+
const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
364
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
349
365
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
350
366
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
351
367
|
let pullRequest: PlatformChangeRequestSnapshot | null = null;
|
|
352
|
-
if (loaded.ok &&
|
|
353
|
-
const inspected = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
368
|
+
if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
|
|
369
|
+
const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
354
370
|
if (inspected.ok) pullRequest = inspected.value;
|
|
355
371
|
}
|
|
356
372
|
const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
|
|
@@ -370,12 +386,13 @@ async function summaryContext(taskRef: string, options: SummaryOptions = {}): Pr
|
|
|
370
386
|
|
|
371
387
|
async function summaryCommentState(taskRef: string, options: SummaryOptions = {}): Promise<SummaryCommentStateResult> {
|
|
372
388
|
const context = await summaryContext(taskRef, options);
|
|
373
|
-
|
|
389
|
+
const prIdentity = context.pullRequest?.identity;
|
|
390
|
+
if (!prIdentity) return { ...context, comment: null };
|
|
374
391
|
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
|
|
375
392
|
if (!loaded.ok || !loaded.value.provider.comments?.list) return { ...context, comment: null };
|
|
376
393
|
const listed = await loaded.value.provider.comments.list({
|
|
377
394
|
context: providerOperationContext(loaded.value),
|
|
378
|
-
parent:
|
|
395
|
+
parent: prIdentity
|
|
379
396
|
});
|
|
380
397
|
if (!listed.ok) return { ...context, comment: null };
|
|
381
398
|
const matches = listed.value.filter((comment) => comment.body.includes(summaryMarker(context.task.id!)));
|
|
@@ -397,24 +414,38 @@ type ReportWriteOptions = {
|
|
|
397
414
|
async function reportWrite(taskRef: string, options: ReportWriteOptions): Promise<ReportWriteResult> {
|
|
398
415
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
399
416
|
if (!resolved.ok) return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), report: null };
|
|
417
|
+
let preflight: ReturnType<typeof readBoundTaskSnapshot>;
|
|
418
|
+
try {
|
|
419
|
+
preflight = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return {
|
|
422
|
+
...platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } }),
|
|
423
|
+
report: null
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (preflight.factError) return {
|
|
427
|
+
...platformResult('failed', { error: { code: preflight.factError.code, message: preflight.factError.message, retryable: false } }),
|
|
428
|
+
report: null
|
|
429
|
+
};
|
|
400
430
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
401
431
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
402
432
|
if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) return { ...context, report: null };
|
|
403
433
|
let knownPrNumber: number | null = null;
|
|
404
434
|
try {
|
|
405
435
|
return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
|
|
406
|
-
const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
407
|
-
if (
|
|
408
|
-
...basePlatformResult('failed', context, resolved.taskId, null, { code:
|
|
436
|
+
const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
437
|
+
if (factError) return {
|
|
438
|
+
...basePlatformResult('failed', context, resolved.taskId, null, { code: factError.code, message: factError.message, retryable: false }),
|
|
409
439
|
report: null
|
|
410
440
|
};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false }),
|
|
441
|
+
if (!boundFact) return {
|
|
442
|
+
...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
|
|
414
443
|
report: null
|
|
415
444
|
};
|
|
445
|
+
const boundPrIdentity = boundFact.identity.resource;
|
|
446
|
+
const boundPrNumber = resourceIdentityNumber(boundPrIdentity);
|
|
416
447
|
knownPrNumber = boundPrNumber;
|
|
417
|
-
const initial = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
448
|
+
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
|
|
418
449
|
if (!initial.ok) return { ...basePlatformResult(initial.status, context, resolved.taskId, boundPrNumber, initial.error), report: null };
|
|
419
450
|
const digest = taskIntentDigest(taskContent);
|
|
420
451
|
if (!digest.ok) return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(digest.error)), report: null };
|
|
@@ -438,7 +469,7 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
|
|
|
438
469
|
};
|
|
439
470
|
const built = buildPrChangeReport(identityFromSnapshot(initial.value), digest.value.sha256, mechanical.value, candidate.value);
|
|
440
471
|
if (!built.ok) return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(built.error)), report: null };
|
|
441
|
-
const final = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
472
|
+
const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
|
|
442
473
|
if (!final.ok) return { ...basePlatformResult(final.status, context, resolved.taskId, boundPrNumber, final.error), report: null };
|
|
443
474
|
if (!sameIdentity(initial.value, final.value)) return {
|
|
444
475
|
...basePlatformResult('blocked', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_HEAD_RACE', message: 'Pull request identity changed while generating the change report', retryable: true }),
|
|
@@ -535,26 +566,27 @@ async function syncPullRequestSummary(
|
|
|
535
566
|
}
|
|
536
567
|
try {
|
|
537
568
|
const execute = async (): Promise<PullRequestSummaryResult> => {
|
|
538
|
-
const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
569
|
+
const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
570
|
+
if (factError) return {
|
|
571
|
+
...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
|
|
572
|
+
result: null, warnings: []
|
|
573
|
+
};
|
|
539
574
|
if (!boundFact) return {
|
|
540
575
|
...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
|
|
541
576
|
result: null, warnings: []
|
|
542
577
|
};
|
|
543
578
|
const prIdentity = boundFact.identity.resource;
|
|
544
579
|
const boundPrNumber = resourceIdentityNumber(prIdentity);
|
|
545
|
-
if (!boundPrNumber) return {
|
|
546
|
-
...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false } }),
|
|
547
|
-
result: null, warnings: []
|
|
548
|
-
};
|
|
549
580
|
knownPrNumber = boundPrNumber;
|
|
550
581
|
const prNumber = boundPrNumber;
|
|
551
|
-
const initial = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
582
|
+
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
552
583
|
if (!initial.ok) return fail(initial.status, context, initial.error);
|
|
553
584
|
const manual = options.manualValidation;
|
|
554
585
|
const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
|
|
555
586
|
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);
|
|
556
587
|
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);
|
|
557
588
|
if (manual) {
|
|
589
|
+
if (prNumber === null) return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
|
|
558
590
|
if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha)) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
|
|
559
591
|
if (manual.phase === 'final') {
|
|
560
592
|
if (manual.authority !== 'coordinator') return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires coordinator authority', retryable: false }, prNumber);
|
|
@@ -621,7 +653,7 @@ async function syncPullRequestSummary(
|
|
|
621
653
|
? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
|
|
622
654
|
: null;
|
|
623
655
|
const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route } as const;
|
|
624
|
-
const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
656
|
+
const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
625
657
|
if (!beforeWrite.ok) return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
|
|
626
658
|
if (!sameIdentity(initial.value, beforeWrite.value)) return fail('blocked', context, {
|
|
627
659
|
code: 'PR_SUMMARY_HEAD_RACE',
|
|
@@ -651,7 +683,7 @@ async function syncPullRequestSummary(
|
|
|
651
683
|
})
|
|
652
684
|
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
653
685
|
if (!written.ok) return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
|
|
654
|
-
const after = await inspectBoundPullRequest(context, resolved.repoRoot,
|
|
686
|
+
const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
655
687
|
const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
|
|
656
688
|
const compensateWrittenSummary = async () => reconciliation.action === 'create'
|
|
657
689
|
? written.value.remoteId && loaded.value.provider.comments?.delete
|
|
@@ -376,7 +376,7 @@ async function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
|
376
376
|
}) };
|
|
377
377
|
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
378
378
|
let issueIdentity: ResourceIdentity | null;
|
|
379
|
-
try { issueIdentity = taskIssueIdentity(frontmatter
|
|
379
|
+
try { issueIdentity = taskIssueIdentity(frontmatter); }
|
|
380
380
|
catch (error) { return { ok: false as const, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) }; }
|
|
381
381
|
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
382
382
|
const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
|
|
@@ -941,7 +941,7 @@ async function resolveExternalPullRequest(taskRef: string, options: ResolveExter
|
|
|
941
941
|
if (!base.issueIdentity) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
|
|
942
942
|
platform: base.context.platform,
|
|
943
943
|
capabilities: base.context.capabilities,
|
|
944
|
-
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid
|
|
944
|
+
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid platform issue identity', retryable: false }
|
|
945
945
|
}));
|
|
946
946
|
const inspected = base.provider.changeRequests?.listClosing
|
|
947
947
|
? await base.provider.changeRequests.listClosing({
|
|
@@ -1430,7 +1430,7 @@ function skipPlatformPullRequestFactUnlocked(
|
|
|
1430
1430
|
});
|
|
1431
1431
|
}
|
|
1432
1432
|
let issueIdentity: ResourceIdentity | null;
|
|
1433
|
-
try { issueIdentity = taskIssueIdentity(frontmatter
|
|
1433
|
+
try { issueIdentity = taskIssueIdentity(frontmatter); }
|
|
1434
1434
|
catch (error) { return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
1435
1435
|
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
1436
1436
|
const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
2
2
|
import { parseResourceIdentity } from './resource-identity.ts';
|
|
3
|
-
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
|
|
4
3
|
import { parse as parseYaml } from 'yaml';
|
|
5
4
|
|
|
6
|
-
function taskIssueIdentity(
|
|
7
|
-
frontmatter: Record<string, string | number | boolean | null>,
|
|
8
|
-
_declaration?: ProviderIdentityDeclaration,
|
|
9
|
-
runtimeVersion = VERSION
|
|
10
|
-
): ResourceIdentity | null {
|
|
5
|
+
function taskIssueIdentity(frontmatter: Record<string, string | number | boolean | null>): ResourceIdentity | null {
|
|
11
6
|
const serialized = frontmatter.platform_issue_identity;
|
|
12
7
|
if (typeof serialized === 'string' && serialized.trim()) {
|
|
13
8
|
try {
|
|
@@ -20,13 +15,7 @@ function taskIssueIdentity(
|
|
|
20
15
|
return null;
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
|
|
25
|
-
// TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
|
|
26
|
-
if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
27
|
-
throw legacyCompatibilityError('issue_number', runtimeVersion);
|
|
28
|
-
}
|
|
29
|
-
return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
|
|
18
|
+
return null;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
function taskIssueIdentityError(error: unknown): { code: string; message: string } {
|
|
@@ -12,6 +12,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
|
|
|
12
12
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
|
|
13
13
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
|
|
14
14
|
import { taskIssueIdentity } from "./task-identities.ts";
|
|
15
|
+
import { isTaskCommentTooLarge } from "./issue-comments.ts";
|
|
15
16
|
import {
|
|
16
17
|
CONTROL_MARKER_PATTERN,
|
|
17
18
|
canonicalizeCommentBody,
|
|
@@ -113,8 +114,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
|
|
|
113
114
|
}
|
|
114
115
|
const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
|
|
115
116
|
const prNumber = resourceIdentityNumber(prIdentity);
|
|
116
|
-
if (config.when === "
|
|
117
|
-
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no
|
|
117
|
+
if (config.when === "platform_issue_identity_exists" && !issueIdentity) {
|
|
118
|
+
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no platform_issue_identity") };
|
|
118
119
|
}
|
|
119
120
|
if (config.when === "pr_fact_bound" && !prIdentity) {
|
|
120
121
|
return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
|
|
@@ -541,6 +542,9 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
|
|
|
541
542
|
if (!context.config.verify_task_comment_content) {
|
|
542
543
|
return null;
|
|
543
544
|
}
|
|
545
|
+
if (isTaskCommentTooLarge(context.task.content)) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
544
548
|
|
|
545
549
|
const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
|
|
546
550
|
const comment = findCommentByMarker(remoteData.comments, taskMarker);
|
|
@@ -37,11 +37,6 @@ export class HostResolutionError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function toPositiveNumber(value: unknown): number | null {
|
|
41
|
-
const n = Number(value);
|
|
42
|
-
return Number.isInteger(n) && n > 0 ? n : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
/**
|
|
46
41
|
* Parse the PR body for closing-issue references. Handles the conventional
|
|
47
42
|
* `Closes #1` / `Fixes: #2, #3` shapes (case-insensitive, comma/space
|
|
@@ -68,7 +63,7 @@ export function extractClosingIssueNumbers(body: string): number[] {
|
|
|
68
63
|
/**
|
|
69
64
|
* Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
|
|
70
65
|
* PR number or to one of the PR's closing issues. A task is emitted at most once;
|
|
71
|
-
* a verified fact identity hit takes priority over the issue-
|
|
66
|
+
* a verified fact identity hit takes priority over the issue-identity reverse lookup.
|
|
72
67
|
*/
|
|
73
68
|
export function collectHostCandidates(input: {
|
|
74
69
|
prNumber?: number;
|
|
@@ -97,7 +92,7 @@ export function collectHostCandidates(input: {
|
|
|
97
92
|
let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
98
93
|
try { frontmatter = parseTypedTaskFrontmatter(content); } catch { continue; }
|
|
99
94
|
const issueIdentity = taskIssueIdentity(frontmatter);
|
|
100
|
-
const issueNumber = resourceIdentityNumber(issueIdentity)
|
|
95
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
101
96
|
const fact = readPrDeliveryFact(frontmatter);
|
|
102
97
|
const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
103
98
|
const prNumber = resourceIdentityNumber(prIdentity);
|
|
@@ -209,8 +204,8 @@ function isTrusted(head: HeadState): boolean {
|
|
|
209
204
|
/**
|
|
210
205
|
* Derive the S1(a) trust factor `taskIssueMatches` (PL-3) mechanically from
|
|
211
206
|
* resolved data: a unique host is trusted on the issue dimension only when the
|
|
212
|
-
* task's bound
|
|
213
|
-
* hosts and tasks without a
|
|
207
|
+
* task's bound issue identity is among the PR's closing issues. Ambiguous/none
|
|
208
|
+
* hosts and tasks without a numeric issue identity cannot be confirmed -> false
|
|
214
209
|
* (fail closed to S2/audit). `resolve-host` emits this value so the prompt layer
|
|
215
210
|
* does not derive the trust factor ad hoc (CD-2).
|
|
216
211
|
*/
|