@fitlab-ai/agent-infra 0.9.8 → 0.9.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +137 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/delegation-receipts.js +243 -2
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +113 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +163 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/delegation-receipts.ts +258 -1
- package/lib/task/finalization.ts +719 -0
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +217 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +118 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- 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/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- 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 +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -19,14 +19,16 @@ import { planPullRequestMetadata } from './pull-request-metadata.ts';
|
|
|
19
19
|
import { platformResult } from './types.ts';
|
|
20
20
|
import type { PlatformOperation, PlatformResult } from './types.ts';
|
|
21
21
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
|
|
22
|
-
import { inspectCreatePrCommitGate } from '../task/commit-finalization.ts';
|
|
23
22
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
23
|
+
import { mergeOperationWarnings, type OperationWarning } from '../task/operation-outcome.ts';
|
|
24
24
|
|
|
25
25
|
type PullRequestSnapshot = PlatformChangeRequestSnapshot;
|
|
26
26
|
|
|
27
27
|
type PullRequestResult = PlatformResult & {
|
|
28
28
|
task: { id: string | null; issueNumber: number | null; prNumber: number | null };
|
|
29
29
|
pullRequest: PullRequestSnapshot | null;
|
|
30
|
+
result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
|
|
31
|
+
warnings: readonly OperationWarning[];
|
|
30
32
|
};
|
|
31
33
|
type InspectionOptions = { cwd?: string; client?: unknown };
|
|
32
34
|
type SharedOptions = { cwd?: string; client?: GitHubClient };
|
|
@@ -40,7 +42,8 @@ type CreateOptions = SharedOptions & {
|
|
|
40
42
|
dryRun?: boolean;
|
|
41
43
|
};
|
|
42
44
|
type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
|
|
43
|
-
type
|
|
45
|
+
type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
|
|
46
|
+
type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
|
|
44
47
|
type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
|
|
45
48
|
type ExternalPullRequestSelection =
|
|
46
49
|
| { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
|
|
@@ -102,6 +105,12 @@ type ClosingPullRequestPage = {
|
|
|
102
105
|
} } } };
|
|
103
106
|
};
|
|
104
107
|
|
|
108
|
+
function warningResultForPrimary(primaryResult: PullRequestPrimaryResult): NonNullable<PullRequestResult['result']> {
|
|
109
|
+
if (primaryResult === 'pr_created') return 'pr_created_with_warnings';
|
|
110
|
+
if (primaryResult === 'pr_reused') return 'pr_reused_with_warnings';
|
|
111
|
+
return 'no_op_with_warnings';
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
function result(
|
|
106
115
|
status: PlatformResult['status'],
|
|
107
116
|
taskId: string | null,
|
|
@@ -109,12 +118,15 @@ function result(
|
|
|
109
118
|
prNumber: number | null,
|
|
110
119
|
overrides: Partial<PullRequestResult> = {}
|
|
111
120
|
): PullRequestResult {
|
|
112
|
-
|
|
121
|
+
const output = {
|
|
113
122
|
...platformResult(status),
|
|
114
123
|
task: { id: taskId, issueNumber, prNumber },
|
|
115
124
|
pullRequest: null,
|
|
125
|
+
result: null,
|
|
126
|
+
warnings: [],
|
|
116
127
|
...overrides
|
|
117
128
|
};
|
|
129
|
+
return { ...output, warnings: mergeOperationWarnings(output.warnings) };
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
function externalResult(
|
|
@@ -311,9 +323,9 @@ function expectedHead(repository: string, head: string): { repository: string; r
|
|
|
311
323
|
return { repository: `${head.slice(0, colon)}/${repoName}`, ref: head.slice(colon + 1) };
|
|
312
324
|
}
|
|
313
325
|
|
|
314
|
-
function selectPullRequest(remotes: RemotePullRequest[], repository: string, head: string, base: string):
|
|
326
|
+
function selectPullRequest(remotes: RemotePullRequest[], repository: string, head: string, base: string, expectedSha?: string):
|
|
315
327
|
| { status: 'resolved'; pullRequest: PullRequestSnapshot }
|
|
316
|
-
| { status: 'missing' | 'ambiguous'; pullRequest: null } {
|
|
328
|
+
| { status: 'missing' | 'ambiguous' | 'head-mismatch'; pullRequest: null } {
|
|
317
329
|
const wanted = expectedHead(repository, head);
|
|
318
330
|
const matches = remotes.flatMap((remote) => {
|
|
319
331
|
const normalized = normalizePullRequest(remote, repository);
|
|
@@ -321,10 +333,94 @@ function selectPullRequest(remotes: RemotePullRequest[], repository: string, hea
|
|
|
321
333
|
normalized.head.ref === wanted.ref && normalized.base.repository === repository && normalized.base.ref === base
|
|
322
334
|
? [normalized] : [];
|
|
323
335
|
});
|
|
336
|
+
if (expectedSha && matches.some((match) => match.head.sha !== expectedSha)) {
|
|
337
|
+
return { status: 'head-mismatch', pullRequest: null };
|
|
338
|
+
}
|
|
324
339
|
if (matches.length === 1) return { status: 'resolved', pullRequest: matches[0]! };
|
|
325
340
|
return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
|
|
326
341
|
}
|
|
327
342
|
|
|
343
|
+
function repositoryHead(cwd: string): string {
|
|
344
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
345
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
346
|
+
}).trim();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function remoteBranchHead(cwd: string, repository: string, head: string):
|
|
350
|
+
| { ok: true; value: string }
|
|
351
|
+
| { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
352
|
+
const wanted = expectedHead(repository, head);
|
|
353
|
+
if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref)) return {
|
|
354
|
+
ok: false,
|
|
355
|
+
error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
|
|
356
|
+
};
|
|
357
|
+
const remotes = configuredGitRemotes(cwd);
|
|
358
|
+
const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
|
|
359
|
+
?? remotes.find((item) => item.name === 'origin');
|
|
360
|
+
if (!remote) return {
|
|
361
|
+
ok: false,
|
|
362
|
+
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
|
|
363
|
+
};
|
|
364
|
+
try {
|
|
365
|
+
const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
|
|
366
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
367
|
+
}).trim();
|
|
368
|
+
const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
|
|
369
|
+
const sha = match?.split(/\s+/)[0] || null;
|
|
370
|
+
if (!sha || !/^[a-f0-9]{40}$/i.test(sha)) return {
|
|
371
|
+
ok: false,
|
|
372
|
+
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
|
|
373
|
+
};
|
|
374
|
+
return { ok: true, value: sha };
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function verifyCreateHead(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string) {
|
|
384
|
+
let localHead: string;
|
|
385
|
+
try {
|
|
386
|
+
localHead = repositoryHead(base.resolved.repoRoot);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return { ok: false as const, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
|
|
389
|
+
}
|
|
390
|
+
const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository!, head);
|
|
391
|
+
if (!remote.ok) return remote;
|
|
392
|
+
if (remote.value !== localHead) return {
|
|
393
|
+
ok: false as const,
|
|
394
|
+
error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
|
|
395
|
+
};
|
|
396
|
+
return { ok: true as const, value: localHead };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function validateBoundPullRequest(
|
|
400
|
+
base: ReturnType<typeof resolvedContext> & { ok: true },
|
|
401
|
+
pullRequest: PullRequestSnapshot | null,
|
|
402
|
+
head: string,
|
|
403
|
+
baseRef: string,
|
|
404
|
+
expectedSha: string
|
|
405
|
+
): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
406
|
+
if (!pullRequest) return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: { code: 'PR_BIND_RECHECK_FAILED', message: 'Bound pull request identity is unavailable', retryable: false }
|
|
409
|
+
};
|
|
410
|
+
const wanted = expectedHead(base.context.platform.repository!, head);
|
|
411
|
+
if (
|
|
412
|
+
pullRequest.head.repository !== wanted.repository
|
|
413
|
+
|| pullRequest.head.ref !== wanted.ref
|
|
414
|
+
|| pullRequest.head.sha !== expectedSha
|
|
415
|
+
|| pullRequest.base.repository !== base.context.platform.repository
|
|
416
|
+
|| pullRequest.base.ref !== baseRef
|
|
417
|
+
) return {
|
|
418
|
+
ok: false,
|
|
419
|
+
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Bound pull request identity does not match the current expected head/base', retryable: false }
|
|
420
|
+
};
|
|
421
|
+
return { ok: true };
|
|
422
|
+
}
|
|
423
|
+
|
|
328
424
|
function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
329
425
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
330
426
|
if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
|
|
@@ -464,18 +560,21 @@ registerPlatformCapabilities('github', {
|
|
|
464
560
|
}
|
|
465
561
|
});
|
|
466
562
|
|
|
467
|
-
function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string, target: string) {
|
|
563
|
+
function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string, target: string, expectedSha?: string) {
|
|
468
564
|
const repository = base.context.platform.repository!;
|
|
469
565
|
const listed = base.client.json<RemotePullRequest[]>([
|
|
470
566
|
'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
|
|
471
567
|
], { cwd: base.resolved.repoRoot });
|
|
472
568
|
if (!listed.ok) return listed;
|
|
473
|
-
const selected = selectPullRequest(listed.value, repository, head, target);
|
|
569
|
+
const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
|
|
474
570
|
return selected.status === 'resolved'
|
|
475
571
|
? { ok: true as const, value: selected.pullRequest }
|
|
476
572
|
: { ok: false as const, error: {
|
|
477
|
-
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
478
|
-
|
|
573
|
+
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
574
|
+
: selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
|
|
575
|
+
message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
|
|
576
|
+
: selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
|
|
577
|
+
: 'No pull request matches the exact head/base identity',
|
|
479
578
|
retryable: false
|
|
480
579
|
} };
|
|
481
580
|
}
|
|
@@ -755,24 +854,41 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
|
|
|
755
854
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
756
855
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
757
856
|
});
|
|
758
|
-
if (base.prNumber)
|
|
759
|
-
|
|
760
|
-
|
|
857
|
+
if (base.prNumber) {
|
|
858
|
+
const headCheck = verifyCreateHead(base, options.head);
|
|
859
|
+
if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
860
|
+
platform: base.context.platform,
|
|
861
|
+
capabilities: base.context.capabilities,
|
|
862
|
+
error: headCheck.error
|
|
863
|
+
});
|
|
864
|
+
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
865
|
+
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
866
|
+
if (!identity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
867
|
+
platform: base.context.platform,
|
|
868
|
+
capabilities: base.context.capabilities,
|
|
869
|
+
pullRequest: inspected.pullRequest,
|
|
870
|
+
error: identity.error
|
|
871
|
+
});
|
|
872
|
+
return { ...inspected, result: 'no_op' };
|
|
873
|
+
}
|
|
874
|
+
const headCheck = verifyCreateHead(base, options.head);
|
|
875
|
+
if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
761
876
|
platform: base.context.platform,
|
|
762
877
|
capabilities: base.context.capabilities,
|
|
763
|
-
error:
|
|
764
|
-
code: gate.code!,
|
|
765
|
-
message: `${gate.message}; action=${gate.action}`,
|
|
766
|
-
retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
|
|
767
|
-
}
|
|
878
|
+
error: headCheck.error
|
|
768
879
|
});
|
|
769
|
-
const
|
|
880
|
+
const expectedHeadSha = headCheck.value;
|
|
881
|
+
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
770
882
|
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
771
883
|
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
772
884
|
});
|
|
885
|
+
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
886
|
+
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
887
|
+
});
|
|
773
888
|
if (options.dryRun) return result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
774
889
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
775
|
-
operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
|
|
890
|
+
operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
|
|
891
|
+
result: located.ok ? 'pr_reused' : 'pr_created', error: null
|
|
776
892
|
});
|
|
777
893
|
const started = writeCreateStarted(base, options.agent);
|
|
778
894
|
if (started.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -789,7 +905,7 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
|
|
|
789
905
|
});
|
|
790
906
|
if (response.ok) pullRequest = normalizePullRequest(response.value, repository);
|
|
791
907
|
else if (response.error.retryable) {
|
|
792
|
-
const recovered = locatePullRequest(base, options.head, options.base);
|
|
908
|
+
const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
793
909
|
if (recovered.ok) pullRequest = recovered.value;
|
|
794
910
|
else return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
795
911
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
@@ -803,8 +919,44 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
|
|
|
803
919
|
if (!pullRequest) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
804
920
|
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
805
921
|
});
|
|
922
|
+
if (pullRequest.head.sha !== expectedHeadSha) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
923
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
924
|
+
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
925
|
+
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
926
|
+
});
|
|
806
927
|
created = true;
|
|
807
928
|
}
|
|
929
|
+
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
930
|
+
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
931
|
+
const error = bindHeadCheck.ok
|
|
932
|
+
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
933
|
+
: bindHeadCheck.error;
|
|
934
|
+
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
935
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
936
|
+
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
|
|
940
|
+
if (!beforeBind.ok || !beforeBind.value) {
|
|
941
|
+
const error = beforeBind.ok
|
|
942
|
+
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
943
|
+
: beforeBind.error;
|
|
944
|
+
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
945
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
946
|
+
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (
|
|
950
|
+
beforeBind.value.head.sha !== expectedHeadSha
|
|
951
|
+
|| beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
|
|
952
|
+
|| beforeBind.value.base.ref !== options.base
|
|
953
|
+
|| beforeBind.value.base.repository !== base.context.platform.repository
|
|
954
|
+
) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
955
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
956
|
+
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
|
|
957
|
+
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
958
|
+
});
|
|
959
|
+
pullRequest = beforeBind.value;
|
|
808
960
|
const refreshed = resolvedContext(taskRef, options);
|
|
809
961
|
if (!refreshed.ok) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
810
962
|
pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
|
|
@@ -819,7 +971,8 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
|
|
|
819
971
|
return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
820
972
|
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
821
973
|
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
822
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
974
|
+
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
975
|
+
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
823
976
|
});
|
|
824
977
|
}
|
|
825
978
|
|
|
@@ -831,7 +984,6 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
|
|
|
831
984
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
832
985
|
});
|
|
833
986
|
}
|
|
834
|
-
if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
|
|
835
987
|
try {
|
|
836
988
|
return withTaskExecutionLock(
|
|
837
989
|
base.resolved.repoRoot,
|
|
@@ -860,23 +1012,40 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
|
|
|
860
1012
|
}
|
|
861
1013
|
|
|
862
1014
|
function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
|
|
1015
|
+
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1016
|
+
const softenFailure = (output: PullRequestResult): PullRequestResult => {
|
|
1017
|
+
const warning = output.error && output.task.prNumber !== null
|
|
1018
|
+
&& !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
|
|
1019
|
+
? {
|
|
1020
|
+
code: output.error.code,
|
|
1021
|
+
message: output.error.message,
|
|
1022
|
+
retryable: output.error.retryable,
|
|
1023
|
+
step: 'pr-metadata',
|
|
1024
|
+
target: `pull-request:${output.task.prNumber}`,
|
|
1025
|
+
severity: 'ACTION_REQUIRED' as const
|
|
1026
|
+
}
|
|
1027
|
+
: null;
|
|
1028
|
+
return warning
|
|
1029
|
+
? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
|
|
1030
|
+
: output;
|
|
1031
|
+
};
|
|
863
1032
|
const base = resolvedContext(taskRef, options);
|
|
864
|
-
if (!base.ok) return base.output;
|
|
865
|
-
if (!base.prNumber) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1033
|
+
if (!base.ok) return softenFailure(base.output);
|
|
1034
|
+
if (!base.prNumber) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
866
1035
|
error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
|
|
867
|
-
});
|
|
1036
|
+
}));
|
|
868
1037
|
const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, base.prNumber, base.resolved.repoRoot);
|
|
869
|
-
if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
|
|
870
|
-
if (!options.metadata && !options.closingIssue) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1038
|
+
if (!fetched.ok) return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
|
|
1039
|
+
if (!options.metadata && !options.closingIssue) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
871
1040
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
872
|
-
});
|
|
873
|
-
if (!base.issueNumber) return result('degraded', base.resolved.taskId, null, base.prNumber, {
|
|
1041
|
+
}));
|
|
1042
|
+
if (!base.issueNumber) return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
|
|
874
1043
|
pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
|
|
875
|
-
});
|
|
1044
|
+
}));
|
|
876
1045
|
const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
877
|
-
if (!issue.issue) return result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1046
|
+
if (!issue.issue) return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
878
1047
|
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
|
|
879
|
-
});
|
|
1048
|
+
}));
|
|
880
1049
|
const planned = planPullRequestMetadata({
|
|
881
1050
|
pullRequest: fetched.value,
|
|
882
1051
|
issue: issue.issue,
|
|
@@ -886,9 +1055,9 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
886
1055
|
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
887
1056
|
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
888
1057
|
const operations: PlatformOperation[] = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
|
|
889
|
-
if (options.dryRun) return result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1058
|
+
if (options.dryRun) return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
890
1059
|
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
|
|
891
|
-
});
|
|
1060
|
+
}));
|
|
892
1061
|
const payload: Record<string, unknown> = {};
|
|
893
1062
|
for (const operation of planned) {
|
|
894
1063
|
if (operation.status !== 'planned') continue;
|
|
@@ -897,26 +1066,26 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
897
1066
|
if (operation.name === 'closing-issue') payload.body = operation.value;
|
|
898
1067
|
if (operation.name === 'milestone') {
|
|
899
1068
|
const milestones = base.client.json<unknown[]>(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
|
|
900
|
-
if (!milestones.ok) return result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error });
|
|
1069
|
+
if (!milestones.ok) return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
|
|
901
1070
|
const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) as Array<{ title?: string; number?: number }>;
|
|
902
1071
|
payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
|
|
903
1072
|
}
|
|
904
1073
|
}
|
|
905
1074
|
if (Object.keys(payload).length === 0) {
|
|
906
1075
|
const degraded = planned.some((operation) => operation.status === 'skipped');
|
|
907
|
-
return result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1076
|
+
return softenFailure(result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
908
1077
|
platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, operations, error: null
|
|
909
|
-
});
|
|
1078
|
+
}));
|
|
910
1079
|
}
|
|
911
1080
|
const patched = base.client.json<RemotePullRequest>(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
|
|
912
1081
|
cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
|
|
913
1082
|
});
|
|
914
|
-
if (!patched.ok) return result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error });
|
|
915
|
-
return result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1083
|
+
if (!patched.ok) return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
|
|
1084
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
916
1085
|
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
917
1086
|
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
|
|
918
1087
|
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
919
|
-
});
|
|
1088
|
+
}));
|
|
920
1089
|
}
|
|
921
1090
|
|
|
922
1091
|
export {
|
|
@@ -933,4 +1102,5 @@ export {
|
|
|
933
1102
|
selectPullRequest,
|
|
934
1103
|
syncPlatformPullRequest
|
|
935
1104
|
};
|
|
936
|
-
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
|
|
1105
|
+
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
|
|
1106
|
+
export { warningResultForPrimary };
|