@fitlab-ai/agent-infra 0.9.11 → 0.9.12
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 +6 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/git/workflow.js +43 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/internal/git-workflow.js +8 -1
- package/dist/lib/internal/platform-pr.js +8 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/platform/github-client.js +7 -1
- package/dist/lib/platform/pr-checks.js +8 -5
- package/dist/lib/platform/pr-summary.js +12 -8
- package/dist/lib/platform/pull-requests.js +393 -91
- package/dist/lib/platform/verification-required.js +13 -2
- package/dist/lib/platform/verification-sync.js +9 -4
- package/dist/lib/pr-review/evidence-grading.js +12 -4
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -2
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +195 -101
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/decision-details.js +3 -48
- package/dist/lib/task/delivery-target.js +97 -0
- package/dist/lib/task/delivery.js +153 -0
- package/dist/lib/task/human-override.js +5 -2
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/orchestration.js +55 -62
- package/dist/lib/task/pr-delivery-fact.js +182 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +8 -0
- package/dist/lib/task/verification-engine.js +48 -24
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/update.js +9 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +3 -0
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-pr.ts +8 -1
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/platform/github-client.ts +8 -1
- package/lib/platform/pr-checks.ts +7 -5
- package/lib/platform/pr-summary.ts +10 -7
- package/lib/platform/pull-requests.ts +425 -87
- package/lib/platform/verification-required.ts +12 -2
- package/lib/platform/verification-sync.ts +9 -4
- package/lib/pr-review/evidence-grading.ts +7 -4
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/server/process-state.ts +2 -2
- package/lib/task/artifact-lifecycle.ts +2 -2
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +174 -87
- package/lib/task/create.ts +34 -9
- package/lib/task/decision-details.ts +2 -62
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/human-override.ts +5 -2
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/orchestration.ts +62 -65
- package/lib/task/pr-delivery-fact.ts +254 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +10 -0
- package/lib/task/verification-engine.ts +45 -28
- package/lib/task/verification-review.ts +2 -0
- package/lib/update.ts +10 -0
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +4 -2
- package/templates/.agents/rules/README.zh-CN.md +4 -2
- package/templates/.agents/rules/create-issue.en.md +11 -2
- package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
- package/templates/.agents/rules/issue-sync.en.md +26 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
- package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
- package/templates/.agents/rules/milestone-inference.en.md +12 -2
- package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
- package/templates/.agents/rules/pr-sync.en.md +22 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
- package/templates/.agents/rules/release-commands.en.md +42 -3
- package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
- package/templates/.agents/rules/security-alerts.en.md +17 -2
- package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
- package/templates/.agents/rules/sync-content-generation.en.md +8 -0
- package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/security-alerts.github.sh +151 -0
- package/templates/.agents/scripts/security-alerts.sh +31 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/commit/SKILL.en.md +3 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/config/verify.json +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
- 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 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
- 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/skills/update-agent-infra/scripts/sync-templates.js +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/templates/task.en.md +5 -1
- package/templates/.agents/templates/task.zh-CN.md +5 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/feature-development.en.yaml +9 -8
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/refactoring.en.yaml +9 -8
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
- package/templates/.agents/rules/create-issue.github.en.md +0 -14
- package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
- package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
- package/templates/.agents/rules/issue-sync.github.en.md +0 -40
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
- package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
- package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
- package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
- package/templates/.agents/rules/pr-sync.github.en.md +0 -43
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
- package/templates/.agents/rules/release-commands.github.en.md +0 -44
- package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
- package/templates/.agents/rules/security-alerts.github.en.md +0 -43
- package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
- /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
- /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
const REMOTE_RE = /^(?!-)(?!.*\.\.)(?!.*[~^:?*\\\s])[A-Za-z0-9._/-]+$/;
|
|
5
|
+
const REF_RE = /^(?!-)(?!.*\.\.)(?!.*(?:^|\/)\.)(?!.*[~^:?*\\\s])[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
|
|
6
|
+
function validateRemote(value) {
|
|
7
|
+
return typeof value === 'string' && value.length > 0 && REMOTE_RE.test(value);
|
|
8
|
+
}
|
|
9
|
+
function validateBaseRef(value) {
|
|
10
|
+
return typeof value === 'string' && value.length > 0 && !value.startsWith('refs/') && REF_RE.test(value);
|
|
11
|
+
}
|
|
12
|
+
function invalid(field, value) {
|
|
13
|
+
return { ok: false, code: 'DELIVERY_TARGET_INVALID', message: `${field} is invalid: ${String(value ?? '')}` };
|
|
14
|
+
}
|
|
15
|
+
function bindDeliveryTarget(input) {
|
|
16
|
+
const remote = input.existing?.remote ?? input.explicit?.remote ?? input.defaults?.remote;
|
|
17
|
+
const baseRef = input.existing?.baseRef ?? input.explicit?.baseRef ?? input.defaults?.baseRef;
|
|
18
|
+
if (input.existing?.remote !== undefined && input.explicit?.remote !== undefined && input.existing.remote !== input.explicit.remote) {
|
|
19
|
+
return { ok: false, code: 'DELIVERY_TARGET_CONFLICT', message: 'Explicit delivery remote does not match the task binding' };
|
|
20
|
+
}
|
|
21
|
+
if (input.existing?.baseRef !== undefined && input.explicit?.baseRef !== undefined && input.existing.baseRef !== input.explicit.baseRef) {
|
|
22
|
+
return { ok: false, code: 'DELIVERY_TARGET_CONFLICT', message: 'Explicit delivery base ref does not match the task binding' };
|
|
23
|
+
}
|
|
24
|
+
if (!validateRemote(remote))
|
|
25
|
+
return invalid('delivery remote', remote);
|
|
26
|
+
if (!validateBaseRef(baseRef))
|
|
27
|
+
return invalid('delivery base ref', baseRef);
|
|
28
|
+
return { ok: true, value: { remote, baseRef } };
|
|
29
|
+
}
|
|
30
|
+
function readDeliveryDefaults(repoRoot) {
|
|
31
|
+
try {
|
|
32
|
+
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
33
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
34
|
+
if (!config.delivery || typeof config.delivery !== 'object' || Array.isArray(config.delivery)) {
|
|
35
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: 'delivery config must declare remote and baseRef' };
|
|
36
|
+
}
|
|
37
|
+
const delivery = config.delivery;
|
|
38
|
+
if (typeof delivery.remote !== 'string' || typeof delivery.baseRef !== 'string') {
|
|
39
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: 'delivery config must declare remote and baseRef' };
|
|
40
|
+
}
|
|
41
|
+
return bindDeliveryTarget({
|
|
42
|
+
defaults: {
|
|
43
|
+
remote: delivery.remote,
|
|
44
|
+
baseRef: delivery.baseRef
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { ok: false, code: 'DELIVERY_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolveDeliveryTarget(repoRoot, existing = {}, explicit = {}) {
|
|
53
|
+
const defaults = readDeliveryDefaults(repoRoot);
|
|
54
|
+
if (!defaults.ok)
|
|
55
|
+
return defaults;
|
|
56
|
+
return bindDeliveryTarget({ defaults: defaults.value, existing, explicit });
|
|
57
|
+
}
|
|
58
|
+
function gitText(repoRoot, args) {
|
|
59
|
+
return execFileSync('git', [...args], {
|
|
60
|
+
cwd: repoRoot,
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
63
|
+
}).trim();
|
|
64
|
+
}
|
|
65
|
+
function resolveTargetHead(repoRoot, target) {
|
|
66
|
+
try {
|
|
67
|
+
const output = gitText(repoRoot, ['ls-remote', '--refs', target.remote, `refs/heads/${target.baseRef}`]);
|
|
68
|
+
const head = output.split(/\s+/)[0] ?? '';
|
|
69
|
+
if (!/^[a-f0-9]{40}$/i.test(head)) {
|
|
70
|
+
return { ok: false, code: 'DELIVERY_TARGET_UNAVAILABLE', message: `Delivery target ${target.remote}/${target.baseRef} is unavailable` };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
gitText(repoRoot, ['cat-file', '-e', `${head}^{commit}`]);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { ok: false, code: 'DELIVERY_TARGET_OBJECT_MISSING', message: `Delivery target commit ${head} is not available locally` };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, value: target, head };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { ok: false, code: 'DELIVERY_TARGET_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveDiffBase(repoRoot, reviewedHead, targetHead) {
|
|
85
|
+
try {
|
|
86
|
+
const diffBase = gitText(repoRoot, ['merge-base', reviewedHead, targetHead]);
|
|
87
|
+
if (!/^[a-f0-9]{40}$/i.test(diffBase)) {
|
|
88
|
+
return { ok: false, code: 'DELIVERY_DIFF_BASE_UNAVAILABLE', message: 'Git returned an invalid diff base' };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, diffBase };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return { ok: false, code: 'DELIVERY_DIFF_BASE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export { bindDeliveryTarget, readDeliveryDefaults, resolveDeliveryTarget, resolveTargetHead, resolveDiffBase, validateBaseRef, validateRemote };
|
|
97
|
+
//# sourceMappingURL=delivery-target.js.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
5
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
|
+
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead, validateBaseRef, validateRemote } from './delivery-target.js';
|
|
7
|
+
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
|
|
8
|
+
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
9
|
+
import { withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.js';
|
|
10
|
+
function classifyDeliveryState(localHead, remoteHead, lastDelivered) {
|
|
11
|
+
if (remoteHead === null)
|
|
12
|
+
return { state: 'absent', shouldPush: true };
|
|
13
|
+
if (remoteHead === localHead)
|
|
14
|
+
return { state: 'same', shouldPush: false };
|
|
15
|
+
if (lastDelivered && remoteHead === lastDelivered)
|
|
16
|
+
return { state: 'known-old', shouldPush: true };
|
|
17
|
+
return { state: 'unknown-drift', shouldPush: false };
|
|
18
|
+
}
|
|
19
|
+
function git(cwd, args) {
|
|
20
|
+
const result = spawnSync('git', [...args], { cwd, encoding: 'utf8' });
|
|
21
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
22
|
+
}
|
|
23
|
+
function gitValue(cwd, args) {
|
|
24
|
+
const result = git(cwd, args);
|
|
25
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
26
|
+
}
|
|
27
|
+
function validateBranch(cwd, branch) {
|
|
28
|
+
return git(cwd, ['check-ref-format', `refs/heads/${branch}`]).status === 0;
|
|
29
|
+
}
|
|
30
|
+
function validateReviewTargetCompatibility(repoRoot, taskDir, target, reviewedHead) {
|
|
31
|
+
const artifact = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
32
|
+
if (!artifact.ok || !artifact.path)
|
|
33
|
+
return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'A current review-code artifact is required before delivery' };
|
|
34
|
+
let content;
|
|
35
|
+
try {
|
|
36
|
+
content = fs.readFileSync(artifact.path, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: error instanceof Error ? error.message : String(error) };
|
|
40
|
+
}
|
|
41
|
+
const savedReviewedHead = extractReviewedHead(content) || extractReviewBaseline(content);
|
|
42
|
+
const savedTargetHead = extractReviewTargetHead(content);
|
|
43
|
+
const savedDiffBase = extractReviewDiffBase(content);
|
|
44
|
+
if (!savedReviewedHead || !savedTargetHead || !savedDiffBase) {
|
|
45
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: 'review-code must save reviewed head, target head, and diff base before delivery' };
|
|
46
|
+
}
|
|
47
|
+
if (savedReviewedHead !== reviewedHead) {
|
|
48
|
+
return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'review-code reviewed head does not match last_reviewed_commit' };
|
|
49
|
+
}
|
|
50
|
+
const saved = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
|
|
51
|
+
if (!saved.ok || saved.diffBase !== savedDiffBase) {
|
|
52
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: saved.ok ? 'saved review diff base does not match saved target head' : saved.message };
|
|
53
|
+
}
|
|
54
|
+
const currentTarget = resolveTargetHead(repoRoot, target);
|
|
55
|
+
if (!currentTarget.ok)
|
|
56
|
+
return { ok: false, code: currentTarget.code, message: currentTarget.message };
|
|
57
|
+
const current = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head);
|
|
58
|
+
if (!current.ok)
|
|
59
|
+
return { ok: false, code: current.code, message: current.message };
|
|
60
|
+
if (current.diffBase !== savedDiffBase) {
|
|
61
|
+
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true };
|
|
64
|
+
}
|
|
65
|
+
function failure(taskId, code, message, retryable = false, status = retryable ? 'blocked' : 'failed') {
|
|
66
|
+
return { status, changed: false, taskId, remoteHead: null, localHead: null, state: null, error: { code, message, retryable } };
|
|
67
|
+
}
|
|
68
|
+
function deliverUnlocked(taskId, taskDir, repoRoot, options) {
|
|
69
|
+
let frontmatter;
|
|
70
|
+
try {
|
|
71
|
+
frontmatter = parseTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return failure(taskId, 'TASK_CONTEXT_UNREADABLE', error instanceof Error ? error.message : String(error));
|
|
75
|
+
}
|
|
76
|
+
if (frontmatter.status !== 'active')
|
|
77
|
+
return failure(taskId, 'TASK_STATE_INVALID', 'task branch delivery requires an active task');
|
|
78
|
+
const branch = frontmatter.branch ?? '';
|
|
79
|
+
if (!branch)
|
|
80
|
+
return failure(taskId, 'DELIVERY_BRANCH_MISSING', 'task branch is missing');
|
|
81
|
+
if (!validateBranch(repoRoot, branch))
|
|
82
|
+
return failure(taskId, 'DELIVERY_BRANCH_INVALID', 'task branch is invalid');
|
|
83
|
+
if (!frontmatter.delivery_remote || !frontmatter.delivery_base_ref) {
|
|
84
|
+
return failure(taskId, 'DELIVERY_TARGET_MISSING', 'task delivery target is not bound; recreate or repair task metadata before delivery');
|
|
85
|
+
}
|
|
86
|
+
const target = resolveDeliveryTarget(repoRoot, {
|
|
87
|
+
remote: frontmatter.delivery_remote,
|
|
88
|
+
baseRef: frontmatter.delivery_base_ref
|
|
89
|
+
}, {
|
|
90
|
+
...(options.remote === undefined ? {} : { remote: options.remote }),
|
|
91
|
+
...(options.baseRef === undefined ? {} : { baseRef: options.baseRef })
|
|
92
|
+
});
|
|
93
|
+
if (!target.ok)
|
|
94
|
+
return failure(taskId, target.code, target.message);
|
|
95
|
+
if (!validateRemote(target.value.remote) || !validateBaseRef(target.value.baseRef))
|
|
96
|
+
return failure(taskId, 'DELIVERY_TARGET_INVALID', 'task delivery target is invalid');
|
|
97
|
+
const localHead = gitValue(repoRoot, ['rev-parse', 'HEAD']);
|
|
98
|
+
if (!localHead)
|
|
99
|
+
return failure(taskId, 'DELIVERY_LOCAL_HEAD_UNAVAILABLE', 'Unable to resolve local HEAD');
|
|
100
|
+
const status = git(repoRoot, ['status', '--porcelain=v1']);
|
|
101
|
+
if (status.status !== 0)
|
|
102
|
+
return failure(taskId, 'DELIVERY_GIT_INSPECT_FAILED', status.stderr.trim() || 'Unable to inspect Git state');
|
|
103
|
+
if (status.stdout.trim())
|
|
104
|
+
return failure(taskId, 'DELIVERY_WORKTREE_DIRTY', 'Working tree must be clean before branch delivery');
|
|
105
|
+
const reviewed = frontmatter.last_reviewed_commit ?? '';
|
|
106
|
+
if (reviewed !== localHead)
|
|
107
|
+
return failure(taskId, 'DELIVERY_REVIEW_REQUIRED', 'Local HEAD must equal last_reviewed_commit before delivery');
|
|
108
|
+
const reviewTarget = validateReviewTargetCompatibility(repoRoot, taskDir, target.value, reviewed);
|
|
109
|
+
if (!reviewTarget.ok)
|
|
110
|
+
return failure(taskId, reviewTarget.code, reviewTarget.message, reviewTarget.code.includes('UNAVAILABLE'), reviewTarget.code.includes('UNAVAILABLE') ? 'blocked' : 'failed');
|
|
111
|
+
const ref = `refs/heads/${branch}`;
|
|
112
|
+
const remoteOutput = git(repoRoot, ['ls-remote', '--refs', target.value.remote, ref]);
|
|
113
|
+
if (remoteOutput.status !== 0)
|
|
114
|
+
return failure(taskId, 'DELIVERY_REMOTE_UNAVAILABLE', remoteOutput.stderr.trim() || 'Unable to inspect the delivery remote', true);
|
|
115
|
+
const remoteHead = remoteOutput.stdout.trim().split(/\s+/)[0] || null;
|
|
116
|
+
const classification = classifyDeliveryState(localHead, remoteHead, frontmatter.delivery_remote_head ?? null);
|
|
117
|
+
if (classification.state === 'unknown-drift')
|
|
118
|
+
return failure(taskId, 'DELIVERY_REMOTE_DRIFT', `Remote branch ${branch} has an unexpected head; refusing to overwrite it`);
|
|
119
|
+
if (options.dryRun)
|
|
120
|
+
return { status: 'planned', changed: classification.shouldPush, taskId, remoteHead, localHead, state: classification.state, error: null };
|
|
121
|
+
if (classification.shouldPush) {
|
|
122
|
+
const args = classification.state === 'known-old'
|
|
123
|
+
? ['push', `--force-with-lease=${ref}:${frontmatter.delivery_remote_head}`, target.value.remote, `HEAD:${ref}`]
|
|
124
|
+
: ['push', `--force-with-lease=${ref}:`, target.value.remote, `HEAD:${ref}`];
|
|
125
|
+
const pushed = git(repoRoot, args);
|
|
126
|
+
if (pushed.status !== 0)
|
|
127
|
+
return failure(taskId, 'DELIVERY_PUSH_FAILED', pushed.stderr.trim() || 'Task branch delivery was rejected', true, 'blocked');
|
|
128
|
+
}
|
|
129
|
+
const verified = gitValue(repoRoot, ['ls-remote', '--refs', target.value.remote, ref])?.split(/\s+/)[0] ?? null;
|
|
130
|
+
if (verified !== localHead)
|
|
131
|
+
return failure(taskId, 'DELIVERY_REMOTE_VERIFY_FAILED', `Expected remote head ${localHead}, received ${verified ?? 'unavailable'}`, true, 'blocked');
|
|
132
|
+
const written = writeTask({
|
|
133
|
+
taskRef: taskId,
|
|
134
|
+
expectedState: 'active',
|
|
135
|
+
mutations: [{ kind: 'frontmatter', set: { delivery_remote: target.value.remote, delivery_base_ref: target.value.baseRef, delivery_remote_head: localHead, assigned_to: options.agent } }]
|
|
136
|
+
}, { repoRoot, metadataProvider: captureTaskWriteMetadata });
|
|
137
|
+
if (written.status === 'failed')
|
|
138
|
+
return failure(taskId, 'DELIVERY_TASK_SYNC_FAILED', written.error.message, true, 'blocked');
|
|
139
|
+
return { status: classification.shouldPush ? 'applied' : 'no-op', changed: classification.shouldPush || written.changed, taskId, remoteHead: localHead, localHead, state: classification.state, error: null };
|
|
140
|
+
}
|
|
141
|
+
function deliverTaskBranch(taskRef, options) {
|
|
142
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
143
|
+
if (!resolved.ok)
|
|
144
|
+
return failure(resolved.taskId, resolved.code, resolved.message);
|
|
145
|
+
try {
|
|
146
|
+
return withRepositoryMutationLock(resolved.repoRoot, () => withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-delivery', () => deliverUnlocked(resolved.taskId, resolved.taskDir, resolved.repoRoot, options)));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return failure(resolved.taskId, 'DELIVERY_LOCK_FAILED', error instanceof Error ? error.message : String(error), true, 'blocked');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export { classifyDeliveryState, deliverTaskBranch };
|
|
153
|
+
//# sourceMappingURL=delivery.js.map
|
|
@@ -19,6 +19,7 @@ import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
|
19
19
|
import { verifyTaskEvent } from './verification.js';
|
|
20
20
|
import { verifyInProcess } from './verification-engine.js';
|
|
21
21
|
import { canonicalTimestamp, writeTask } from './write.js';
|
|
22
|
+
import { readPrDeliveryFact } from './pr-delivery-fact.js';
|
|
22
23
|
const LIFECYCLE_PRODUCER = 'lifecycle.apply';
|
|
23
24
|
const HUMAN_OVERRIDE_SECTION_ALIASES = ['人工豁免账本', 'Human Override Ledger'];
|
|
24
25
|
const HUMAN_OVERRIDE_COLUMNS = [
|
|
@@ -405,7 +406,8 @@ function resourceForSnapshot(policy, options, frontmatter) {
|
|
|
405
406
|
return number === null ? null : { kind: 'issue', number };
|
|
406
407
|
}
|
|
407
408
|
if (policy.producerId === 'platform.pull-request') {
|
|
408
|
-
const
|
|
409
|
+
const fact = frontmatter ? readPrDeliveryFact(frontmatter) : null;
|
|
410
|
+
const number = options.probePullRequestNumber ?? (fact?.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null);
|
|
409
411
|
return number === null ? null : { kind: 'pull-request', number };
|
|
410
412
|
}
|
|
411
413
|
return null;
|
|
@@ -468,7 +470,8 @@ function probePlatformFailure(taskRef, policy, resolved, options) {
|
|
|
468
470
|
return error('OVERRIDE_DOCUMENT_INVALID', cause instanceof Error ? cause.message : String(cause));
|
|
469
471
|
}
|
|
470
472
|
const issueNumber = options.probeIssueNumber ?? toPositiveInteger(frontmatter.issue_number);
|
|
471
|
-
const
|
|
473
|
+
const fact = readPrDeliveryFact(frontmatter);
|
|
474
|
+
const pullRequestNumber = options.probePullRequestNumber ?? (fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null);
|
|
472
475
|
const operation = policy.producerId === 'platform.issue'
|
|
473
476
|
? issueNumber === null
|
|
474
477
|
? null
|
|
@@ -14,6 +14,7 @@ const lifecycleIntentCatalog = [
|
|
|
14
14
|
];
|
|
15
15
|
const lifecycleFailureCatalog = [
|
|
16
16
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
17
|
+
'LIFECYCLE_DIRECTORY_COPY_FAILED',
|
|
17
18
|
'LIFECYCLE_DOCUMENT_INVALID',
|
|
18
19
|
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
19
20
|
'LIFECYCLE_IDENTITY_CONFLICT',
|
|
@@ -295,6 +296,65 @@ function parseJournal(content) {
|
|
|
295
296
|
}
|
|
296
297
|
return value;
|
|
297
298
|
}
|
|
299
|
+
function directoryManifest(root) {
|
|
300
|
+
const entries = [];
|
|
301
|
+
const visit = (current, relative = '') => {
|
|
302
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
303
|
+
const relativePath = relative ? path.join(relative, entry.name) : entry.name;
|
|
304
|
+
const absolute = path.join(current, entry.name);
|
|
305
|
+
const stat = fs.lstatSync(absolute);
|
|
306
|
+
if (stat.isSymbolicLink())
|
|
307
|
+
throw new Error(`symbolic link is not allowed in lifecycle directory: ${relativePath}`);
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
entries.push({ relativePath, kind: 'directory', size: 0, digest: '' });
|
|
310
|
+
visit(absolute, relativePath);
|
|
311
|
+
}
|
|
312
|
+
else if (entry.isFile()) {
|
|
313
|
+
const digest = createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
314
|
+
entries.push({ relativePath, kind: 'file', size: stat.size, digest });
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
throw new Error(`unsupported lifecycle directory entry: ${relativePath}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
visit(root);
|
|
322
|
+
return entries;
|
|
323
|
+
}
|
|
324
|
+
function copyDirectoryVerified(source, target) {
|
|
325
|
+
if (fs.existsSync(target))
|
|
326
|
+
throw new Error(`target directory already exists: ${target}`);
|
|
327
|
+
const copy = (currentSource, currentTarget) => {
|
|
328
|
+
fs.mkdirSync(currentTarget);
|
|
329
|
+
for (const entry of fs.readdirSync(currentSource, { withFileTypes: true })) {
|
|
330
|
+
const sourcePath = path.join(currentSource, entry.name);
|
|
331
|
+
const targetPath = path.join(currentTarget, entry.name);
|
|
332
|
+
const stat = fs.lstatSync(sourcePath);
|
|
333
|
+
if (stat.isSymbolicLink())
|
|
334
|
+
throw new Error(`symbolic link is not allowed in lifecycle directory: ${entry.name}`);
|
|
335
|
+
if (entry.isDirectory())
|
|
336
|
+
copy(sourcePath, targetPath);
|
|
337
|
+
else if (entry.isFile())
|
|
338
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
339
|
+
else
|
|
340
|
+
throw new Error(`unsupported lifecycle directory entry: ${entry.name}`);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
copy(source, target);
|
|
344
|
+
const sourceManifest = directoryManifest(source);
|
|
345
|
+
const targetManifest = directoryManifest(target);
|
|
346
|
+
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
347
|
+
throw new Error('copied lifecycle directory failed manifest verification');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function removeCopiedSource(source, target) {
|
|
351
|
+
const sourceManifest = directoryManifest(source);
|
|
352
|
+
const targetManifest = directoryManifest(target);
|
|
353
|
+
if (JSON.stringify(sourceManifest) !== JSON.stringify(targetManifest)) {
|
|
354
|
+
throw new Error('lifecycle source and target diverged before source cleanup');
|
|
355
|
+
}
|
|
356
|
+
fs.rmSync(source, { recursive: true, force: false });
|
|
357
|
+
}
|
|
298
358
|
function applyTaskLifecycle(requestInput, options = {}) {
|
|
299
359
|
const normalized = normalizedRequest(requestInput);
|
|
300
360
|
if ('code' in normalized)
|
|
@@ -356,7 +416,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
356
416
|
if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
|
|
357
417
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
358
418
|
}
|
|
359
|
-
if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
|
|
419
|
+
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
360
420
|
return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
361
421
|
}
|
|
362
422
|
const content = io.readFileSync(currentTaskFile);
|
|
@@ -501,18 +561,55 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
501
561
|
(options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath);
|
|
502
562
|
}
|
|
503
563
|
catch (error) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
564
|
+
if (error.code === 'EXDEV') {
|
|
565
|
+
try {
|
|
566
|
+
copyDirectoryVerified(sourcePath, targetPath);
|
|
567
|
+
completed.add('directory-moved');
|
|
568
|
+
journal.completedSteps = [...completed];
|
|
569
|
+
const copiedJournalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
570
|
+
writeJournal(initialJournalPath, journal, io);
|
|
571
|
+
writeJournal(copiedJournalPath, journal, io);
|
|
572
|
+
removeCopiedSource(sourcePath, targetPath);
|
|
573
|
+
journalPath = copiedJournalPath;
|
|
574
|
+
}
|
|
575
|
+
catch (copyError) {
|
|
576
|
+
if (fs.existsSync(targetPath))
|
|
577
|
+
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
578
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(copyError) };
|
|
579
|
+
rememberJournalFailure(initialJournalPath, journal, io, failure);
|
|
580
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) };
|
|
585
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
586
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
587
|
+
}
|
|
507
588
|
}
|
|
508
|
-
completed.
|
|
509
|
-
|
|
510
|
-
|
|
589
|
+
if (completed.has('directory-moved') && !fs.existsSync(sourcePath)) {
|
|
590
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
591
|
+
}
|
|
592
|
+
else if (!completed.has('directory-moved')) {
|
|
593
|
+
completed.add('directory-moved');
|
|
594
|
+
journal.completedSteps = [...completed];
|
|
595
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
596
|
+
try {
|
|
597
|
+
writeJournal(journalPath, journal, io);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (completed.has('directory-moved') && sourcePath !== targetPath && fs.existsSync(sourcePath) && fs.existsSync(targetPath)) {
|
|
511
605
|
try {
|
|
512
|
-
|
|
606
|
+
removeCopiedSource(sourcePath, targetPath);
|
|
607
|
+
journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
513
608
|
}
|
|
514
609
|
catch (error) {
|
|
515
|
-
|
|
610
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_COPY_FAILED', message: String(error) };
|
|
611
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
612
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
516
613
|
}
|
|
517
614
|
}
|
|
518
615
|
let shortId = { effect: 'unchanged', shortId: null, changed: false };
|
|
@@ -10,13 +10,14 @@ import { resolveTaskRef } from './resolve-ref.js';
|
|
|
10
10
|
import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
|
|
11
11
|
import { normalizeAgentClients } from '../agent-clients/config.js';
|
|
12
12
|
import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.js';
|
|
13
|
-
import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
|
|
14
13
|
import { getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
|
|
15
14
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
16
15
|
import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots } from './workspace-snapshot.js';
|
|
17
16
|
import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
|
|
18
17
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
19
18
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
19
|
+
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.js';
|
|
20
|
+
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
|
|
20
21
|
function supportsLifecycleDelegation(client) {
|
|
21
22
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
22
23
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -106,9 +107,8 @@ function isCompletionEvidence(value) {
|
|
|
106
107
|
&& exactText(value.headTree)
|
|
107
108
|
&& exactText(value.worktreeTree)
|
|
108
109
|
&& exactText(value.lastReviewedCommit)
|
|
109
|
-
&& Number.isSafeInteger(value.prNumber)
|
|
110
|
-
&& value.
|
|
111
|
-
&& exactText(value.prHead);
|
|
110
|
+
&& (value.prNumber === null || (Number.isSafeInteger(value.prNumber) && value.prNumber > 0))
|
|
111
|
+
&& (value.prHead === null || exactText(value.prHead));
|
|
112
112
|
}
|
|
113
113
|
const ORCHESTRATION_RUN_KEYS = [
|
|
114
114
|
'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
|
|
@@ -340,6 +340,43 @@ function highestRound(taskDir, family) {
|
|
|
340
340
|
function artifactName(family, round) {
|
|
341
341
|
return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
|
|
342
342
|
}
|
|
343
|
+
function validateSavedCurrentDiffBase(repoRoot, metadata, taskDir, reviewedHead) {
|
|
344
|
+
const reviewRound = highestRound(taskDir, 'review-code');
|
|
345
|
+
const reviewContent = fs.readFileSync(path.join(taskDir, artifactName('review-code', reviewRound)), 'utf8');
|
|
346
|
+
const savedTargetHead = extractReviewTargetHead(reviewContent);
|
|
347
|
+
const savedDiffBase = extractReviewDiffBase(reviewContent);
|
|
348
|
+
const savedReviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
349
|
+
if (!savedTargetHead && !savedDiffBase && !savedReviewedHead)
|
|
350
|
+
return null;
|
|
351
|
+
if (savedReviewedHead && savedReviewedHead !== reviewedHead) {
|
|
352
|
+
return { code: 'ORCHESTRATION_REVIEWED_HEAD_MISMATCH', message: 'review-code saved reviewed head does not match last_reviewed_commit' };
|
|
353
|
+
}
|
|
354
|
+
if (!savedTargetHead || !savedDiffBase) {
|
|
355
|
+
return { code: 'ORCHESTRATION_REVIEW_TARGET_INVALID', message: 'review-code must save target head and diff base together' };
|
|
356
|
+
}
|
|
357
|
+
const savedBase = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
|
|
358
|
+
if (!savedBase.ok)
|
|
359
|
+
return { code: 'ORCHESTRATION_REVIEW_TARGET_INVALID', message: savedBase.message };
|
|
360
|
+
if (savedBase.diffBase !== savedDiffBase) {
|
|
361
|
+
return { code: 'ORCHESTRATION_REVIEW_DIFF_BASE_MISMATCH', message: 'saved review diff base does not match reviewed head and saved target head' };
|
|
362
|
+
}
|
|
363
|
+
const target = resolveDeliveryTarget(repoRoot, {
|
|
364
|
+
remote: typeof metadata.delivery_remote === 'string' ? metadata.delivery_remote : undefined,
|
|
365
|
+
baseRef: typeof metadata.delivery_base_ref === 'string' ? metadata.delivery_base_ref : undefined
|
|
366
|
+
});
|
|
367
|
+
if (!target.ok)
|
|
368
|
+
return { code: target.code, message: target.message };
|
|
369
|
+
const currentTarget = resolveTargetHead(repoRoot, target.value);
|
|
370
|
+
if (!currentTarget.ok)
|
|
371
|
+
return { code: currentTarget.code, message: currentTarget.message };
|
|
372
|
+
const currentBase = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head);
|
|
373
|
+
if (!currentBase.ok)
|
|
374
|
+
return { code: currentBase.code, message: currentBase.message };
|
|
375
|
+
if (currentBase.diffBase !== savedDiffBase) {
|
|
376
|
+
return { code: 'ORCHESTRATION_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
343
380
|
function hasReviewAfterArtifact(taskDir, family, reviewFamily) {
|
|
344
381
|
const artifact = inspectArtifactDirectory(taskDir, family);
|
|
345
382
|
const review = inspectArtifactDirectory(taskDir, reviewFamily);
|
|
@@ -487,29 +524,8 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
487
524
|
if (run?.status === 'completed') {
|
|
488
525
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
489
526
|
}
|
|
490
|
-
let config;
|
|
491
|
-
try {
|
|
492
|
-
config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
496
|
-
}
|
|
497
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
498
|
-
return failed('ORCHESTRATION_CONFIG_INVALID', 'project configuration must be an object', resolved.taskId);
|
|
499
|
-
}
|
|
500
|
-
const projectConfig = config;
|
|
501
|
-
if (!Object.hasOwn(projectConfig, 'prFlow') || projectConfig.prFlow === 'disabled') {
|
|
502
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
503
|
-
}
|
|
504
|
-
if (projectConfig.prFlow !== 'required') {
|
|
505
|
-
return failed('ORCHESTRATION_CONFIG_INVALID', "project configuration 'prFlow' must be 'required' or 'disabled'", resolved.taskId);
|
|
506
|
-
}
|
|
507
|
-
const prNumber = Number(metadata.pr_number);
|
|
508
|
-
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
509
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
510
|
-
}
|
|
511
527
|
if (!run) {
|
|
512
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
528
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
513
529
|
}
|
|
514
530
|
if (run.status !== 'running' || run.pendingDelegation !== null) {
|
|
515
531
|
return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'clean completion requires an idle running orchestration', resolved.taskId);
|
|
@@ -523,34 +539,15 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
523
539
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
524
540
|
}
|
|
525
541
|
if (before.headTree !== before.worktreeTree) {
|
|
526
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
542
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
527
543
|
}
|
|
528
544
|
const lastReviewedCommit = String(metadata.last_reviewed_commit ?? '');
|
|
529
545
|
if (before.head !== lastReviewedCommit) {
|
|
530
546
|
return failed('ORCHESTRATION_REVIEWED_HEAD_MISMATCH', 'local HEAD does not match last_reviewed_commit', resolved.taskId);
|
|
531
547
|
}
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
inspection = inspect(resolved.taskId, { cwd: gitRoot });
|
|
536
|
-
}
|
|
537
|
-
catch (error) {
|
|
538
|
-
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
539
|
-
}
|
|
540
|
-
if (inspection.status === 'blocked') {
|
|
541
|
-
return failed('ORCHESTRATION_PR_INSPECTION_BLOCKED', inspection.error?.message ?? 'pull request inspection is blocked', resolved.taskId);
|
|
542
|
-
}
|
|
543
|
-
if (inspection.status === 'failed') {
|
|
544
|
-
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', inspection.error?.message ?? 'pull request inspection failed', resolved.taskId);
|
|
545
|
-
}
|
|
546
|
-
const inspectedPrNumber = inspection.task?.prNumber ?? inspection.prNumber;
|
|
547
|
-
const prHead = inspection.pullRequest?.head?.sha;
|
|
548
|
-
if (typeof prHead !== 'string') {
|
|
549
|
-
return failed('ORCHESTRATION_PR_INSPECTION_INVALID', 'pull request inspection returned incomplete identity evidence', resolved.taskId);
|
|
550
|
-
}
|
|
551
|
-
if (inspectedPrNumber !== prNumber || prHead !== before.head) {
|
|
552
|
-
return failed('ORCHESTRATION_PR_HEAD_MISMATCH', 'pull request identity does not match the reviewed local HEAD', resolved.taskId);
|
|
553
|
-
}
|
|
548
|
+
const diffBaseError = validateSavedCurrentDiffBase(resolved.repoRoot, metadata, resolved.taskDir, lastReviewedCommit);
|
|
549
|
+
if (diffBaseError)
|
|
550
|
+
return failed(diffBaseError.code, diffBaseError.message, resolved.taskId);
|
|
554
551
|
let after;
|
|
555
552
|
try {
|
|
556
553
|
after = capture(gitRoot);
|
|
@@ -559,7 +556,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
559
556
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
560
557
|
}
|
|
561
558
|
if (after.headTree !== after.worktreeTree) {
|
|
562
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
559
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
563
560
|
}
|
|
564
561
|
if (after.head !== before.head || after.headTree !== before.headTree) {
|
|
565
562
|
return failed('ORCHESTRATION_REPOSITORY_CHANGED', 'repository changed while clean completion evidence was collected', resolved.taskId);
|
|
@@ -572,8 +569,8 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
572
569
|
headTree: after.headTree,
|
|
573
570
|
worktreeTree: after.worktreeTree,
|
|
574
571
|
lastReviewedCommit,
|
|
575
|
-
prNumber,
|
|
576
|
-
prHead
|
|
572
|
+
prNumber: null,
|
|
573
|
+
prHead: null
|
|
577
574
|
};
|
|
578
575
|
const completed = withUpdatedRun(run, {
|
|
579
576
|
status: 'completed',
|
|
@@ -668,10 +665,7 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
668
665
|
}
|
|
669
666
|
const updated = withUpdatedRun(run, {
|
|
670
667
|
nextStage: next.stage,
|
|
671
|
-
pendingDelegation: receipt
|
|
672
|
-
commitAuthorization: next.stage === 'commit' && run.commitAuthorization.issuedAt === null
|
|
673
|
-
? { issuedAt: (options.now ?? (() => new Date().toISOString()))(), consumedAt: null }
|
|
674
|
-
: run.commitAuthorization
|
|
668
|
+
pendingDelegation: receipt
|
|
675
669
|
});
|
|
676
670
|
saveRun(resolved.taskDir, updated);
|
|
677
671
|
return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next, error: null };
|
|
@@ -1093,16 +1087,15 @@ function advanceOrchestration(taskRef, options = {}) {
|
|
|
1093
1087
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
1094
1088
|
if (!result.ok)
|
|
1095
1089
|
return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
1096
|
-
|
|
1090
|
+
if (result.receipt.stage === 'commit') {
|
|
1091
|
+
return failed('ORCHESTRATED_COMMIT_REMOVED', 'Orchestrated commit is no longer a lifecycle stage; use local checkpoint delivery from code-task', resolved.taskId);
|
|
1092
|
+
}
|
|
1097
1093
|
const updated = withUpdatedRun(run, {
|
|
1098
|
-
status:
|
|
1094
|
+
status: 'running',
|
|
1099
1095
|
nextStage: null,
|
|
1100
1096
|
stepCount: run.stepCount + 1,
|
|
1101
1097
|
pendingDelegation: null,
|
|
1102
|
-
receipts: Object.freeze([...run.receipts, result.receipt])
|
|
1103
|
-
commitAuthorization: completed
|
|
1104
|
-
? { ...run.commitAuthorization, consumedAt: (options.now ?? (() => new Date().toISOString()))() }
|
|
1105
|
-
: run.commitAuthorization
|
|
1098
|
+
receipts: Object.freeze([...run.receipts, result.receipt])
|
|
1106
1099
|
});
|
|
1107
1100
|
saveRun(resolved.taskDir, updated);
|
|
1108
1101
|
return { status: updated.status, changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|