@fitlab-ai/agent-infra 0.9.2 → 0.9.4
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 +12 -9
- package/README.zh-CN.md +12 -9
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -108
- package/dist/lib/agent-clients/adapter.js +137 -4
- package/dist/lib/agent-clients/adapters/antigravity-cli.js +64 -0
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +27 -4
- package/dist/lib/agent-clients/adapters/codex.js +9 -0
- package/dist/lib/agent-clients/adapters/index.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +24 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/config.js +42 -4
- package/dist/lib/agent-clients/next-steps.js +11 -2
- package/dist/lib/agent-clients/reconcile.js +2 -1
- package/dist/lib/agent-clients/registry.js +16 -2
- package/dist/lib/agent-clients/schema.js +18 -0
- package/dist/lib/agent-clients/tokens.js +49 -0
- package/dist/lib/agent-clients/types.js +1 -1
- package/dist/lib/defaults.json +1 -1
- package/dist/lib/git/workflow.js +54 -1
- package/dist/lib/internal/agent-client.js +61 -4
- package/dist/lib/internal/git-workflow.js +6 -1
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +29 -2
- package/dist/lib/internal/platform-issue.js +7 -0
- package/dist/lib/internal/platform-pr-review.js +132 -0
- package/dist/lib/internal/platform-pr.js +20 -2
- package/dist/lib/internal/platform-release-notes.js +52 -16
- package/dist/lib/internal/pr-review-grade.js +140 -0
- package/dist/lib/internal/release-workflow.js +13 -6
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +105 -0
- package/dist/lib/internal/task-event.js +17 -1
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +26 -1
- package/dist/lib/internal/task-orchestration.js +167 -18
- package/dist/lib/internal/task-review.js +8 -3
- package/dist/lib/platform/adapters.js +8 -1
- package/dist/lib/platform/completion-backfill.js +122 -0
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/issue-comments.js +3 -2
- package/dist/lib/platform/merged-pr-equivalence.js +115 -5
- package/dist/lib/platform/pr-checks.js +65 -42
- package/dist/lib/platform/pr-review.js +166 -0
- package/dist/lib/platform/pull-requests.js +371 -4
- package/dist/lib/platform/release-notes.js +85 -3
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/pr-review/evidence-grading.js +229 -0
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +10 -13
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +109 -248
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +13 -4
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +103 -0
- package/dist/lib/task/activity-intent.js +307 -0
- package/dist/lib/task/artifact-lifecycle.js +2 -1
- package/dist/lib/task/commands/log.js +15 -14
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +25 -4
- package/dist/lib/task/events.js +67 -28
- package/dist/lib/task/finalization-artifacts.js +35 -0
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/lifecycle-execution.js +46 -0
- package/dist/lib/task/orchestration.js +753 -54
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-finalization.js +29 -9
- package/dist/lib/task/task-execution-lock.js +231 -0
- package/dist/lib/task/verification-checks.js +1 -1
- package/dist/lib/task/verification-engine.js +218 -2
- package/dist/lib/task/verification.js +4 -3
- package/dist/lib/task/workspace-snapshot.js +17 -5
- package/lib/agent-clients/adapter.ts +201 -4
- package/lib/agent-clients/adapters/antigravity-cli.ts +65 -0
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +31 -4
- package/lib/agent-clients/adapters/codex.ts +9 -0
- package/lib/agent-clients/adapters/index.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +27 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/config.ts +49 -4
- package/lib/agent-clients/next-steps.ts +15 -2
- package/lib/agent-clients/reconcile.ts +2 -1
- package/lib/agent-clients/registry.ts +19 -1
- package/lib/agent-clients/schema.ts +18 -0
- package/lib/agent-clients/tokens.ts +53 -0
- package/lib/agent-clients/types.ts +15 -1
- package/lib/defaults.json +1 -1
- package/lib/git/workflow.ts +66 -2
- package/lib/internal/agent-client.ts +59 -4
- package/lib/internal/git-workflow.ts +5 -1
- package/lib/internal/platform-checks.ts +4 -4
- package/lib/internal/platform-comment.ts +17 -2
- package/lib/internal/platform-issue.ts +4 -0
- package/lib/internal/platform-pr-review.ts +104 -0
- package/lib/internal/platform-pr.ts +14 -1
- package/lib/internal/platform-release-notes.ts +59 -16
- package/lib/internal/pr-review-grade.ts +130 -0
- package/lib/internal/release-workflow.ts +13 -6
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +91 -0
- package/lib/internal/task-event.ts +9 -1
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +26 -1
- package/lib/internal/task-orchestration.ts +123 -14
- package/lib/internal/task-review.ts +8 -3
- package/lib/platform/adapters.ts +29 -2
- package/lib/platform/completion-backfill.ts +134 -0
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/issue-comments.ts +3 -2
- package/lib/platform/merged-pr-equivalence.ts +127 -5
- package/lib/platform/pr-checks.ts +73 -46
- package/lib/platform/pr-review.ts +201 -0
- package/lib/platform/pull-requests.ts +444 -3
- package/lib/platform/release-notes.ts +100 -4
- package/lib/platform/releases.ts +16 -4
- package/lib/pr-review/evidence-grading.ts +318 -0
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +14 -12
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +126 -280
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +30 -4
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +143 -0
- package/lib/server/server.schema.json +2 -2
- package/lib/task/activity-intent.ts +417 -0
- package/lib/task/artifact-lifecycle.ts +2 -1
- package/lib/task/commands/log.ts +15 -14
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +34 -5
- package/lib/task/events.ts +75 -30
- package/lib/task/finalization-artifacts.ts +58 -0
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/lifecycle-execution.ts +106 -0
- package/lib/task/orchestration.ts +1019 -72
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-finalization.ts +40 -9
- package/lib/task/task-execution-lock.ts +296 -0
- package/lib/task/verification-checks.ts +1 -1
- package/lib/task/verification-engine.ts +249 -2
- package/lib/task/verification.ts +5 -3
- package/lib/task/workspace-snapshot.ts +26 -5
- package/package.json +3 -3
- package/templates/.agents/QUICKSTART.en.md +25 -8
- package/templates/.agents/QUICKSTART.zh-CN.md +25 -8
- package/templates/.agents/README.en.md +10 -7
- package/templates/.agents/README.zh-CN.md +10 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +15 -4
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/create-issue.github.en.md +4 -2
- package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -2
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/issue-fields.github.en.md +3 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +3 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +5 -3
- package/templates/.agents/rules/issue-pr-commands.github.en.md +5 -3
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +5 -3
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -3
- package/templates/.agents/rules/issue-sync.github.en.md +7 -3
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +8 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +5 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +5 -2
- package/templates/.agents/rules/milestone-inference.github.en.md +4 -2
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -2
- package/templates/.agents/rules/next-step-output.en.md +3 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +3 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +17 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +17 -0
- package/templates/.agents/rules/pr-checks-commands.en.md +3 -3
- package/templates/.agents/rules/pr-checks-commands.github.en.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +3 -3
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +3 -3
- package/templates/.agents/rules/pr-sync.en.md +3 -1
- package/templates/.agents/rules/pr-sync.github.en.md +11 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +11 -1
- package/templates/.agents/rules/pr-sync.zh-CN.md +3 -1
- package/templates/.agents/rules/release-commands.en.md +1 -1
- package/templates/.agents/rules/release-commands.github.en.md +12 -2
- package/templates/.agents/rules/release-commands.github.zh-CN.md +12 -2
- package/templates/.agents/rules/release-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +13 -0
- package/templates/.agents/rules/task-management.zh-CN.md +13 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -7
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -7
- package/templates/.agents/skills/block-task/SKILL.en.md +7 -5
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +7 -5
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -4
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +10 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +12 -9
- package/templates/.agents/skills/commit/SKILL.en.md +23 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +29 -11
- 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 +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
- 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/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 +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +6 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +6 -4
- 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 +22 -7
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +22 -7
- 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 +17 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +17 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -5
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -5
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -16
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -16
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/import-issue/SKILL.en.md +5 -3
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +8 -5
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
- package/templates/.agents/skills/post-release/SKILL.en.md +2 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/release/SKILL.en.md +16 -10
- package/templates/.agents/skills/release/SKILL.zh-CN.md +16 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +7 -4
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/review-code/SKILL.en.md +11 -8
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -8
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +7 -4
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +186 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +186 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +37 -0
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +37 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +65 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +65 -0
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +18 -0
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +85 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +85 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +8 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/run-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/run-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +20 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +20 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +98 -107
- package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -13
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -13
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +27 -6
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +26 -5
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +3 -3
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +3 -3
- package/templates/.agents/workflows/code-review.en.yaml +1 -1
- package/templates/.agents/workflows/code-review.zh-CN.yaml +1 -1
- package/templates/.agents/workflows/feature-development.en.yaml +4 -4
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +4 -4
- package/templates/.agents/workflows/refactoring.en.yaml +3 -3
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +3 -3
- package/templates/.claude/commands/post-release.en.md +1 -0
- package/templates/.claude/commands/post-release.zh-CN.md +1 -0
- package/templates/.claude/commands/review-pr.en.md +8 -0
- package/templates/.claude/commands/review-pr.zh-CN.md +8 -0
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.opencode/commands/post-release.en.md +2 -0
- package/templates/.opencode/commands/post-release.zh-CN.md +2 -0
- package/templates/.opencode/commands/review-pr.en.md +11 -0
- package/templates/.opencode/commands/review-pr.zh-CN.md +11 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/gemini-cli.js +0 -64
- package/lib/agent-clients/adapters/gemini-cli.ts +0 -65
- package/templates/.gemini/commands/_project_/analyze-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/analyze-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/archive-tasks.en.toml +0 -10
- package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/block-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/block-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/cancel-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/cancel-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/check-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/check-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/close-codescan.en.toml +0 -8
- package/templates/.gemini/commands/_project_/close-codescan.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/close-dependabot.en.toml +0 -8
- package/templates/.gemini/commands/_project_/close-dependabot.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/code-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/commit.en.toml +0 -10
- package/templates/.gemini/commands/_project_/commit.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +0 -10
- package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/complete-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/complete-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/create-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/create-pr.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/create-release-note.en.toml +0 -8
- package/templates/.gemini/commands/_project_/create-release-note.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/create-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/create-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/import-codescan.en.toml +0 -8
- package/templates/.gemini/commands/_project_/import-codescan.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/import-dependabot.en.toml +0 -8
- package/templates/.gemini/commands/_project_/import-dependabot.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/import-issue.en.toml +0 -8
- package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/init-labels.en.toml +0 -8
- package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/init-milestones.en.toml +0 -10
- package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/plan-task.en.toml +0 -10
- package/templates/.gemini/commands/_project_/plan-task.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/post-release.en.toml +0 -6
- package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +0 -6
- package/templates/.gemini/commands/_project_/refine-title.en.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-title.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/release.en.toml +0 -8
- package/templates/.gemini/commands/_project_/release.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/restore-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/review-analysis.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/review-code.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/review-plan.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +0 -10
- package/templates/.gemini/commands/_project_/run-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/test-integration.en.toml +0 -6
- package/templates/.gemini/commands/_project_/test-integration.zh-CN.toml +0 -6
- package/templates/.gemini/commands/_project_/test.en.toml +0 -6
- package/templates/.gemini/commands/_project_/test.zh-CN.toml +0 -6
- package/templates/.gemini/commands/_project_/update-agent-infra.en.toml +0 -6
- package/templates/.gemini/commands/_project_/update-agent-infra.zh-CN.toml +0 -6
- package/templates/.gemini/commands/_project_/upgrade-dependency.en.toml +0 -8
- package/templates/.gemini/commands/_project_/upgrade-dependency.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/watch-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +0 -10
- package/templates/.gemini/settings.json +0 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
5
|
|
|
5
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
6
7
|
import { parseReviewSummary } from './review-artifacts.ts';
|
|
@@ -16,24 +17,93 @@ import {
|
|
|
16
17
|
sealDelegation
|
|
17
18
|
} from './delegation-receipts.ts';
|
|
18
19
|
import type { DelegationReceipt, DelegationRole, DelegationStage } from './delegation-receipts.ts';
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
20
|
+
import { normalizeAgentClients } from '../agent-clients/config.ts';
|
|
21
|
+
import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
|
|
22
|
+
import {
|
|
23
|
+
getAgentClientCapability,
|
|
24
|
+
getAgentClientDelegationEvidence,
|
|
25
|
+
getAgentClientModelSelection
|
|
26
|
+
} from '../agent-clients/registry.ts';
|
|
27
|
+
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
28
|
+
import type {
|
|
29
|
+
AgentClientId,
|
|
30
|
+
OrchestrationModelPolicy,
|
|
31
|
+
OrchestrationRolePolicy
|
|
32
|
+
} from '../agent-clients/types.ts';
|
|
33
|
+
import {
|
|
34
|
+
captureRepositorySnapshot,
|
|
35
|
+
captureWorkspaceSnapshot,
|
|
36
|
+
diffWorkspaceSnapshots
|
|
37
|
+
} from './workspace-snapshot.ts';
|
|
38
|
+
import type { RepositorySnapshot } from './workspace-snapshot.ts';
|
|
39
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
40
|
+
import {
|
|
41
|
+
CommitIntentError,
|
|
42
|
+
commitIntentPath,
|
|
43
|
+
createCommitIntent,
|
|
44
|
+
digest,
|
|
45
|
+
readCommitIntent,
|
|
46
|
+
removeCommitIntent,
|
|
47
|
+
removeCommitIntentByDigest,
|
|
48
|
+
serialize,
|
|
49
|
+
updateCommitIntent
|
|
50
|
+
} from './commit-intent.ts';
|
|
51
|
+
import type { CommitIntent, PushEvidence } from './commit-intent.ts';
|
|
52
|
+
import {
|
|
53
|
+
inspectCommitFinalization,
|
|
54
|
+
planCommitTaskFinalization
|
|
55
|
+
} from './commit-finalization.ts';
|
|
56
|
+
import type { CommitFinalizationInspection } from './commit-finalization.ts';
|
|
57
|
+
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
22
58
|
|
|
23
59
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
60
|
+
type LegacyOrchestrationModelPolicy = Readonly<{
|
|
61
|
+
executor: string;
|
|
62
|
+
reviewer: string;
|
|
63
|
+
}>;
|
|
64
|
+
type ModelPolicySource = Readonly<{
|
|
65
|
+
kind: 'explicit' | 'project-config';
|
|
66
|
+
client: AgentClientId;
|
|
67
|
+
resolvedAt: string;
|
|
68
|
+
}>;
|
|
69
|
+
type OrchestrationRecovery = Readonly<{
|
|
70
|
+
code: 'MODEL_POLICY_SUPPLEMENTED';
|
|
71
|
+
recoveredAt: string;
|
|
72
|
+
previousSchemaVersion: 1;
|
|
73
|
+
previousStatus: OrchestrationStatus;
|
|
74
|
+
previousPause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
75
|
+
policySource: ModelPolicySource;
|
|
76
|
+
receiptCount: 0;
|
|
77
|
+
pendingDelegation: false;
|
|
78
|
+
resultingStatus: OrchestrationStatus;
|
|
79
|
+
}>;
|
|
80
|
+
type CleanCompletionEvidence = Readonly<{
|
|
81
|
+
kind: 'reviewed-head-clean';
|
|
82
|
+
observedAt: string;
|
|
83
|
+
head: string;
|
|
84
|
+
headTree: string;
|
|
85
|
+
worktreeTree: string;
|
|
86
|
+
lastReviewedCommit: string;
|
|
87
|
+
prNumber: number;
|
|
88
|
+
prHead: string;
|
|
89
|
+
}>;
|
|
24
90
|
type OrchestrationRun = Readonly<{
|
|
25
|
-
schemaVersion: 1;
|
|
91
|
+
schemaVersion: 1 | 2;
|
|
26
92
|
taskId: string;
|
|
27
93
|
runId: string;
|
|
28
94
|
status: OrchestrationStatus;
|
|
29
95
|
nextStage: DelegationStage | null;
|
|
30
96
|
stepCount: number;
|
|
31
97
|
maxSteps: number;
|
|
98
|
+
modelPolicy?: OrchestrationModelPolicy | LegacyOrchestrationModelPolicy;
|
|
99
|
+
modelPolicySource?: ModelPolicySource;
|
|
100
|
+
recoveryHistory?: readonly OrchestrationRecovery[];
|
|
32
101
|
baseline: string;
|
|
33
102
|
pendingDelegation: DelegationReceipt | null;
|
|
34
103
|
receipts: readonly DelegationReceipt[];
|
|
35
104
|
pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
36
105
|
commitAuthorization: Readonly<{ issuedAt: string | null; consumedAt: string | null }>;
|
|
106
|
+
completionEvidence?: CleanCompletionEvidence | null;
|
|
37
107
|
createdAt: string;
|
|
38
108
|
updatedAt: string;
|
|
39
109
|
}>;
|
|
@@ -43,6 +113,8 @@ type OrchestrationNext = Readonly<{
|
|
|
43
113
|
stage: DelegationStage;
|
|
44
114
|
round: number;
|
|
45
115
|
artifact: string;
|
|
116
|
+
requestedModel: string | null;
|
|
117
|
+
requestedReasoningEffort: string | null;
|
|
46
118
|
}>;
|
|
47
119
|
type OrchestrationResult = Readonly<{
|
|
48
120
|
status: OrchestrationStatus | 'failed';
|
|
@@ -50,17 +122,84 @@ type OrchestrationResult = Readonly<{
|
|
|
50
122
|
taskId: string | null;
|
|
51
123
|
run: OrchestrationRun | null;
|
|
52
124
|
next: OrchestrationNext | null;
|
|
53
|
-
error: Readonly<{
|
|
125
|
+
error: Readonly<{
|
|
126
|
+
code: string;
|
|
127
|
+
message: string;
|
|
128
|
+
client?: AgentClientId;
|
|
129
|
+
missingFields?: readonly string[];
|
|
130
|
+
modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
|
|
131
|
+
}> | null;
|
|
132
|
+
}>;
|
|
133
|
+
type OrchestrationStageIdentity = Readonly<{
|
|
134
|
+
stage: DelegationStage;
|
|
135
|
+
round: number;
|
|
136
|
+
artifact: string;
|
|
137
|
+
role: DelegationRole;
|
|
138
|
+
}>;
|
|
139
|
+
type OrchestrationStageCompletion = Readonly<{
|
|
140
|
+
taskId: string;
|
|
141
|
+
taskDir: string;
|
|
142
|
+
updatedRun: OrchestrationRun;
|
|
143
|
+
}>;
|
|
144
|
+
type OrchestrationCompletionPlanResult = Readonly<{
|
|
145
|
+
result: OrchestrationResult;
|
|
146
|
+
plan: OrchestrationStageCompletion | null;
|
|
54
147
|
}>;
|
|
55
148
|
type OrchestrationOptions = {
|
|
56
149
|
repoRoot?: string;
|
|
57
150
|
id?: () => string;
|
|
58
151
|
now?: () => string;
|
|
59
152
|
maxSteps?: number;
|
|
60
|
-
|
|
153
|
+
client?: AgentClientId;
|
|
154
|
+
modelPolicy?: OrchestrationModelPolicy;
|
|
155
|
+
captureWorkspace?: (repoRoot: string, taskId: string | null) => string;
|
|
156
|
+
captureRepository?: (repoRoot: string) => RepositorySnapshot;
|
|
157
|
+
inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
|
|
158
|
+
status: string;
|
|
159
|
+
task?: Readonly<{ prNumber?: number | null }>;
|
|
160
|
+
prNumber?: number | null;
|
|
161
|
+
pullRequest?: Readonly<{ head?: Readonly<{ sha?: string }> }> | null;
|
|
162
|
+
error?: Readonly<{ message?: string }> | null;
|
|
163
|
+
}>;
|
|
61
164
|
diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
|
|
165
|
+
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
166
|
+
token?: () => string;
|
|
62
167
|
};
|
|
63
168
|
|
|
169
|
+
type CommitIntentResult = Readonly<{
|
|
170
|
+
status: 'ready' | 'failed';
|
|
171
|
+
changed: boolean;
|
|
172
|
+
taskId: string | null;
|
|
173
|
+
intent: Readonly<{
|
|
174
|
+
mode: CommitIntent['mode'];
|
|
175
|
+
phase: CommitIntent['phase'];
|
|
176
|
+
baselineHead: string;
|
|
177
|
+
currentHead: string;
|
|
178
|
+
committedHead: string | null;
|
|
179
|
+
pushEvidence: PushEvidence | null;
|
|
180
|
+
runId: string | null;
|
|
181
|
+
receiptId: string | null;
|
|
182
|
+
}> | null;
|
|
183
|
+
finalization?: Readonly<{
|
|
184
|
+
disposition: CommitFinalizationInspection['disposition'];
|
|
185
|
+
code: CommitFinalizationInspection['code'];
|
|
186
|
+
message: string;
|
|
187
|
+
currentHead: string;
|
|
188
|
+
committedHead: string | null;
|
|
189
|
+
needsAnchor: boolean;
|
|
190
|
+
needsLog: boolean;
|
|
191
|
+
}>;
|
|
192
|
+
token?: string;
|
|
193
|
+
error: Readonly<{ code: string; message: string }> | null;
|
|
194
|
+
}>;
|
|
195
|
+
|
|
196
|
+
function supportsLifecycleDelegation(client: AgentClientId): boolean {
|
|
197
|
+
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
198
|
+
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
199
|
+
&& getAgentClientDelegationEvidence(client).actualModel !== 'unavailable'
|
|
200
|
+
&& getAgentClientDelegationEvidence(client).actualReasoningEffort !== 'unavailable';
|
|
201
|
+
}
|
|
202
|
+
|
|
64
203
|
function orchestrationPath(taskDir: string): string {
|
|
65
204
|
return path.join(taskDir, 'orchestration.json');
|
|
66
205
|
}
|
|
@@ -68,7 +207,21 @@ function orchestrationPath(taskDir: string): string {
|
|
|
68
207
|
function readRun(taskDir: string): OrchestrationRun | null {
|
|
69
208
|
const file = orchestrationPath(taskDir);
|
|
70
209
|
if (!fs.existsSync(file)) return null;
|
|
71
|
-
|
|
210
|
+
const run = JSON.parse(fs.readFileSync(file, 'utf8')) as OrchestrationRun;
|
|
211
|
+
if (run.schemaVersion !== 2 || !isV2Policy(run.modelPolicy)) return run;
|
|
212
|
+
return {
|
|
213
|
+
...run,
|
|
214
|
+
modelPolicy: {
|
|
215
|
+
executor: {
|
|
216
|
+
model: run.modelPolicy.executor.model,
|
|
217
|
+
reasoningEffort: run.modelPolicy.executor.reasoningEffort
|
|
218
|
+
},
|
|
219
|
+
reviewer: {
|
|
220
|
+
model: run.modelPolicy.reviewer.model,
|
|
221
|
+
reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
72
225
|
}
|
|
73
226
|
|
|
74
227
|
function atomicWrite(file: string, value: unknown): void {
|
|
@@ -81,40 +234,295 @@ function saveRun(taskDir: string, run: OrchestrationRun): void {
|
|
|
81
234
|
atomicWrite(orchestrationPath(taskDir), run);
|
|
82
235
|
}
|
|
83
236
|
|
|
84
|
-
function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun
|
|
85
|
-
return Object.freeze({ ...run, ...updates, updatedAt: new Date().toISOString() });
|
|
237
|
+
function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>, now?: () => string): OrchestrationRun {
|
|
238
|
+
return Object.freeze({ ...run, ...updates, updatedAt: (now ?? (() => new Date().toISOString()))() });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function failed(
|
|
242
|
+
code: string,
|
|
243
|
+
message: string,
|
|
244
|
+
taskId: string | null = null,
|
|
245
|
+
details: Omit<NonNullable<OrchestrationResult['error']>, 'code' | 'message'> = {}
|
|
246
|
+
): OrchestrationResult {
|
|
247
|
+
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
|
|
251
|
+
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function repositoryHead(repoRoot: string): string {
|
|
255
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
256
|
+
cwd: repoRoot,
|
|
257
|
+
encoding: 'utf8',
|
|
258
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
259
|
+
}).trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
|
|
263
|
+
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
264
|
+
cwd: repoRoot,
|
|
265
|
+
stdio: 'ignore'
|
|
266
|
+
}).status === 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
|
|
270
|
+
return {
|
|
271
|
+
mode: intent.mode,
|
|
272
|
+
phase: intent.phase,
|
|
273
|
+
baselineHead: intent.baselineHead,
|
|
274
|
+
currentHead,
|
|
275
|
+
committedHead: intent.committedHead,
|
|
276
|
+
pushEvidence: intent.pushEvidence,
|
|
277
|
+
runId: intent.orchestration?.runId ?? null,
|
|
278
|
+
receiptId: intent.orchestration?.receiptId ?? null
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
|
|
283
|
+
return {
|
|
284
|
+
disposition: inspection.disposition,
|
|
285
|
+
code: inspection.code,
|
|
286
|
+
message: inspection.message,
|
|
287
|
+
currentHead: inspection.currentHead,
|
|
288
|
+
committedHead: inspection.committedHead,
|
|
289
|
+
needsAnchor: inspection.needsAnchor,
|
|
290
|
+
needsLog: inspection.needsLog
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
|
|
295
|
+
return run !== null
|
|
296
|
+
&& [1, 2].includes(run.schemaVersion)
|
|
297
|
+
&& run.taskId === taskId
|
|
298
|
+
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
299
|
+
&& Array.isArray(run.receipts)
|
|
300
|
+
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
301
|
+
&& typeof run.commitAuthorization === 'object'
|
|
302
|
+
&& run.commitAuthorization !== null
|
|
303
|
+
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
304
|
+
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
|
|
308
|
+
if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
|
|
309
|
+
if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
|
|
310
|
+
return commitIntentFailed(
|
|
311
|
+
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
312
|
+
error instanceof Error ? error.message : String(error),
|
|
313
|
+
taskId
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function validModel(value: unknown): value is string {
|
|
318
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function validateModelPolicy(policy: OrchestrationModelPolicy | undefined): Readonly<{ code: string; message: string }> | null {
|
|
322
|
+
if (
|
|
323
|
+
!policy
|
|
324
|
+
|| !validModel(policy.executor?.model)
|
|
325
|
+
|| !validModel(policy.executor?.reasoningEffort)
|
|
326
|
+
|| !validModel(policy.reviewer?.model)
|
|
327
|
+
|| !validModel(policy.reviewer?.reasoningEffort)
|
|
328
|
+
) {
|
|
329
|
+
return { code: 'ORCHESTRATION_MODEL_POLICY_REQUIRED', message: 'executor and reviewer model and reasoning effort are required' };
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationModelPolicy): boolean {
|
|
335
|
+
return left.executor.model === right.executor.model
|
|
336
|
+
&& left.executor.reasoningEffort === right.executor.reasoningEffort
|
|
337
|
+
&& left.reviewer.model === right.reviewer.model
|
|
338
|
+
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
|
|
342
|
+
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
|
|
346
|
+
return isV2Policy(run.modelPolicy) ? run.modelPolicy[role] : null;
|
|
86
347
|
}
|
|
87
348
|
|
|
88
|
-
function
|
|
89
|
-
|
|
349
|
+
function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
|
|
350
|
+
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
351
|
+
if (!fs.existsSync(configPath)) return undefined;
|
|
352
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
|
|
353
|
+
return normalizeAgentClients(raw).state[client].orchestration;
|
|
90
354
|
}
|
|
91
355
|
|
|
92
356
|
function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
93
357
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
94
358
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
95
|
-
|
|
359
|
+
if (!isAgentClientId(options.client)) {
|
|
360
|
+
return failed('ORCHESTRATION_PAYLOAD_INVALID', 'begin-or-resume requires a known client', resolved.taskId);
|
|
361
|
+
}
|
|
362
|
+
let existing: OrchestrationRun | null;
|
|
363
|
+
try {
|
|
364
|
+
existing = readRun(resolved.taskDir);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
367
|
+
}
|
|
96
368
|
if (existing) {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
369
|
+
if (
|
|
370
|
+
![1, 2].includes(existing.schemaVersion)
|
|
371
|
+
|| !Array.isArray(existing.receipts)
|
|
372
|
+
|| !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')
|
|
373
|
+
) {
|
|
374
|
+
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
375
|
+
}
|
|
376
|
+
if (existing.status === 'completed' && existing.schemaVersion === 1) {
|
|
377
|
+
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
378
|
+
}
|
|
379
|
+
if (existing.schemaVersion === 2) {
|
|
380
|
+
const policyError = validateModelPolicy(isV2Policy(existing.modelPolicy) ? existing.modelPolicy : undefined);
|
|
381
|
+
if (
|
|
382
|
+
policyError
|
|
383
|
+
|| !existing.modelPolicySource
|
|
384
|
+
|| !isAgentClientId(existing.modelPolicySource.client)
|
|
385
|
+
|| !Array.isArray(existing.recoveryHistory)
|
|
386
|
+
) {
|
|
387
|
+
return failed('ORCHESTRATION_STATE_INVALID', 'schemaVersion 2 orchestration state is incomplete', resolved.taskId);
|
|
388
|
+
}
|
|
389
|
+
if (existing.status === 'completed') {
|
|
390
|
+
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
391
|
+
}
|
|
392
|
+
if (existing.modelPolicySource.client !== options.client) {
|
|
393
|
+
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
394
|
+
}
|
|
395
|
+
if (options.modelPolicy) {
|
|
396
|
+
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
397
|
+
if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
398
|
+
if (!sameModelPolicy(existing.modelPolicy as OrchestrationModelPolicy, options.modelPolicy)) {
|
|
399
|
+
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
403
|
+
const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
|
|
404
|
+
saveRun(resolved.taskDir, resumed);
|
|
405
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
406
|
+
}
|
|
407
|
+
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let policy: OrchestrationModelPolicy | undefined = options.modelPolicy;
|
|
411
|
+
let sourceKind: ModelPolicySource['kind'] = 'explicit';
|
|
412
|
+
if (!policy) {
|
|
413
|
+
try {
|
|
414
|
+
policy = resolveProjectPolicy(resolved.repoRoot, options.client);
|
|
415
|
+
sourceKind = 'project-config';
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const policyError = validateModelPolicy(policy);
|
|
421
|
+
if (policyError) {
|
|
422
|
+
return failed(policyError.code, policyError.message, resolved.taskId, {
|
|
423
|
+
client: options.client,
|
|
424
|
+
missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
|
|
425
|
+
modelSelectionContext: getAgentClientModelSelection(options.client)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (existing.pendingDelegation !== null) {
|
|
429
|
+
const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_DELEGATION_BUSY';
|
|
430
|
+
if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
431
|
+
const paused = withUpdatedRun(existing, {
|
|
432
|
+
status: 'paused',
|
|
433
|
+
pause: { code: 'ORCHESTRATION_DELEGATION_BUSY', message: 'legacy run has a pending delegation', recoverable: false }
|
|
434
|
+
});
|
|
435
|
+
saveRun(resolved.taskDir, paused);
|
|
436
|
+
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
437
|
+
}
|
|
438
|
+
if (existing.receipts.length > 0) {
|
|
439
|
+
const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED';
|
|
440
|
+
if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
441
|
+
const paused = withUpdatedRun(existing, {
|
|
442
|
+
status: 'paused',
|
|
443
|
+
pause: {
|
|
444
|
+
code: 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED',
|
|
445
|
+
message: 'legacy receipts do not contain actual reasoning-effort evidence',
|
|
446
|
+
recoverable: false
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
saveRun(resolved.taskDir, paused);
|
|
450
|
+
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
451
|
+
}
|
|
452
|
+
if (
|
|
453
|
+
existing.modelPolicy
|
|
454
|
+
&& typeof existing.modelPolicy.executor === 'string'
|
|
455
|
+
&& (
|
|
456
|
+
existing.modelPolicy.executor !== policy!.executor.model
|
|
457
|
+
|| existing.modelPolicy.reviewer !== policy!.reviewer.model
|
|
458
|
+
)
|
|
459
|
+
) {
|
|
460
|
+
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
461
|
+
}
|
|
462
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
463
|
+
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
464
|
+
const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
|
|
465
|
+
const resultingStatus = clearsModelPause ? 'running' : existing.status;
|
|
466
|
+
const recovery: OrchestrationRecovery = {
|
|
467
|
+
code: 'MODEL_POLICY_SUPPLEMENTED',
|
|
468
|
+
recoveredAt: now,
|
|
469
|
+
previousSchemaVersion: 1,
|
|
470
|
+
previousStatus: existing.status,
|
|
471
|
+
previousPause: existing.pause,
|
|
472
|
+
policySource: source,
|
|
473
|
+
receiptCount: 0,
|
|
474
|
+
pendingDelegation: false,
|
|
475
|
+
resultingStatus
|
|
476
|
+
};
|
|
477
|
+
const recovered: OrchestrationRun = Object.freeze({
|
|
478
|
+
...existing,
|
|
479
|
+
schemaVersion: 2,
|
|
480
|
+
status: resultingStatus,
|
|
481
|
+
modelPolicy: policy!,
|
|
482
|
+
modelPolicySource: source,
|
|
483
|
+
recoveryHistory: Object.freeze([recovery]),
|
|
484
|
+
pause: clearsModelPause ? null : existing.pause,
|
|
485
|
+
updatedAt: now
|
|
486
|
+
});
|
|
487
|
+
saveRun(resolved.taskDir, recovered);
|
|
488
|
+
return { status: resultingStatus, changed: true, taskId: resolved.taskId, run: recovered, next: null, error: null };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let policy = options.modelPolicy;
|
|
492
|
+
let sourceKind: ModelPolicySource['kind'] = 'explicit';
|
|
493
|
+
if (!policy) {
|
|
494
|
+
try {
|
|
495
|
+
policy = resolveProjectPolicy(resolved.repoRoot, options.client);
|
|
496
|
+
sourceKind = 'project-config';
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
101
499
|
}
|
|
102
|
-
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
103
500
|
}
|
|
501
|
+
const policyError = validateModelPolicy(policy);
|
|
502
|
+
if (policyError) return failed(policyError.code, policyError.message, resolved.taskId, {
|
|
503
|
+
client: options.client,
|
|
504
|
+
missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
|
|
505
|
+
modelSelectionContext: getAgentClientModelSelection(options.client)
|
|
506
|
+
});
|
|
104
507
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
508
|
+
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
105
509
|
const run: OrchestrationRun = {
|
|
106
|
-
schemaVersion:
|
|
510
|
+
schemaVersion: 2,
|
|
107
511
|
taskId: resolved.taskId,
|
|
108
512
|
runId: (options.id ?? randomUUID)(),
|
|
109
513
|
status: 'running',
|
|
110
514
|
nextStage: null,
|
|
111
515
|
stepCount: 0,
|
|
112
516
|
maxSteps: options.maxSteps ?? 24,
|
|
517
|
+
modelPolicy: policy!,
|
|
518
|
+
modelPolicySource: source,
|
|
519
|
+
recoveryHistory: [],
|
|
113
520
|
baseline: '',
|
|
114
521
|
pendingDelegation: null,
|
|
115
522
|
receipts: [],
|
|
116
523
|
pause: null,
|
|
117
524
|
commitAuthorization: { issuedAt: null, consumedAt: null },
|
|
525
|
+
completionEvidence: null,
|
|
118
526
|
createdAt: now,
|
|
119
527
|
updatedAt: now
|
|
120
528
|
};
|
|
@@ -150,7 +558,7 @@ function latestReviewApproved(taskDir: string, family: 'review-analysis' | 'revi
|
|
|
150
558
|
return parsed.ok && parsed.summary.verdict === 'Approved' && parsed.summary.counts !== null;
|
|
151
559
|
}
|
|
152
560
|
|
|
153
|
-
function routeFromFacts(taskDir: string): OrchestrationNext | null {
|
|
561
|
+
function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | null {
|
|
154
562
|
const analysisRound = highestRound(taskDir, 'analysis');
|
|
155
563
|
const analysisReviewRound = highestRound(taskDir, 'review-analysis');
|
|
156
564
|
const planRound = highestRound(taskDir, 'plan');
|
|
@@ -205,13 +613,21 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
205
613
|
} catch (error) {
|
|
206
614
|
return failed('ORCHESTRATION_TASK_READ_FAILED', String(error), resolved.taskId);
|
|
207
615
|
}
|
|
616
|
+
let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
208
617
|
try {
|
|
209
|
-
parseTypedTaskFrontmatter(content);
|
|
618
|
+
metadata = parseTypedTaskFrontmatter(content);
|
|
210
619
|
} catch (error) {
|
|
211
620
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
212
621
|
}
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
622
|
+
const routed = routeFromFacts(resolved.taskDir);
|
|
623
|
+
if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
624
|
+
const run = readRun(resolved.taskDir);
|
|
625
|
+
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
626
|
+
const next: OrchestrationNext = {
|
|
627
|
+
...routed,
|
|
628
|
+
requestedModel: policy?.model ?? null,
|
|
629
|
+
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
630
|
+
};
|
|
215
631
|
if (next.stage === 'commit') {
|
|
216
632
|
const reviewRound = highestRound(resolved.taskDir, 'review-code');
|
|
217
633
|
const review = parseReviewSummary(fs.readFileSync(
|
|
@@ -221,17 +637,466 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
221
637
|
if (!review.ok || review.summary.manualValidation === null) {
|
|
222
638
|
return failed('ORCHESTRATION_REVIEW_INVALID', 'latest code review has no numeric manual-validation count', resolved.taskId);
|
|
223
639
|
}
|
|
224
|
-
if (review.summary.manualValidation > 0) {
|
|
225
|
-
return failed('ORCHESTRATION_MANUAL_VALIDATION_PENDING', 'manual validation must be completed before commit', resolved.taskId);
|
|
226
|
-
}
|
|
227
640
|
const rows = parseLedger(content);
|
|
228
641
|
const ledgerError = validateLedgerRows(rows);
|
|
229
642
|
if (ledgerError) return failed('ORCHESTRATION_LEDGER_INVALID', ledgerError.message, resolved.taskId);
|
|
230
643
|
if (!summarizeLedgerStage(rows, 'code').canAdvance) {
|
|
231
644
|
return failed('ORCHESTRATION_LEDGER_BLOCKED', 'code review ledger has unresolved findings or human decisions', resolved.taskId);
|
|
232
645
|
}
|
|
646
|
+
if (run?.status === 'completed') {
|
|
647
|
+
return { status: 'completed', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let config: unknown;
|
|
651
|
+
try {
|
|
652
|
+
config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
653
|
+
} catch (error) {
|
|
654
|
+
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
655
|
+
}
|
|
656
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
657
|
+
return failed('ORCHESTRATION_CONFIG_INVALID', 'project configuration must be an object', resolved.taskId);
|
|
658
|
+
}
|
|
659
|
+
const projectConfig = config as Record<string, unknown>;
|
|
660
|
+
if (!Object.hasOwn(projectConfig, 'prFlow') || projectConfig.prFlow === 'disabled') {
|
|
661
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
662
|
+
}
|
|
663
|
+
if (projectConfig.prFlow !== 'required') {
|
|
664
|
+
return failed('ORCHESTRATION_CONFIG_INVALID', "project configuration 'prFlow' must be 'required' or 'disabled'", resolved.taskId);
|
|
665
|
+
}
|
|
666
|
+
const prNumber = Number(metadata.pr_number);
|
|
667
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
668
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
669
|
+
}
|
|
670
|
+
if (!run) {
|
|
671
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
672
|
+
}
|
|
673
|
+
if (run.status !== 'running' || run.pendingDelegation !== null) {
|
|
674
|
+
return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'clean completion requires an idle running orchestration', resolved.taskId);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const capture = options.captureRepository ?? captureRepositorySnapshot;
|
|
678
|
+
let before: RepositorySnapshot;
|
|
679
|
+
try {
|
|
680
|
+
before = capture(resolved.repoRoot);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
683
|
+
}
|
|
684
|
+
if (before.headTree !== before.worktreeTree) {
|
|
685
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
686
|
+
}
|
|
687
|
+
const lastReviewedCommit = String(metadata.last_reviewed_commit ?? '');
|
|
688
|
+
if (before.head !== lastReviewedCommit) {
|
|
689
|
+
return failed('ORCHESTRATION_REVIEWED_HEAD_MISMATCH', 'local HEAD does not match last_reviewed_commit', resolved.taskId);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const inspect: NonNullable<OrchestrationOptions['inspectPullRequest']> =
|
|
693
|
+
options.inspectPullRequest ?? inspectPlatformPullRequest;
|
|
694
|
+
let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
|
|
695
|
+
try {
|
|
696
|
+
inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
|
|
697
|
+
} catch (error) {
|
|
698
|
+
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
699
|
+
}
|
|
700
|
+
if (inspection.status === 'blocked') {
|
|
701
|
+
return failed('ORCHESTRATION_PR_INSPECTION_BLOCKED', inspection.error?.message ?? 'pull request inspection is blocked', resolved.taskId);
|
|
702
|
+
}
|
|
703
|
+
if (inspection.status === 'failed') {
|
|
704
|
+
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', inspection.error?.message ?? 'pull request inspection failed', resolved.taskId);
|
|
705
|
+
}
|
|
706
|
+
const inspectedPrNumber = inspection.task?.prNumber ?? inspection.prNumber;
|
|
707
|
+
const prHead = inspection.pullRequest?.head?.sha;
|
|
708
|
+
if (typeof prHead !== 'string') {
|
|
709
|
+
return failed('ORCHESTRATION_PR_INSPECTION_INVALID', 'pull request inspection returned incomplete identity evidence', resolved.taskId);
|
|
710
|
+
}
|
|
711
|
+
if (inspectedPrNumber !== prNumber || prHead !== before.head) {
|
|
712
|
+
return failed('ORCHESTRATION_PR_HEAD_MISMATCH', 'pull request identity does not match the reviewed local HEAD', resolved.taskId);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let after: RepositorySnapshot;
|
|
716
|
+
try {
|
|
717
|
+
after = capture(resolved.repoRoot);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
720
|
+
}
|
|
721
|
+
if (after.headTree !== after.worktreeTree) {
|
|
722
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
723
|
+
}
|
|
724
|
+
if (after.head !== before.head || after.headTree !== before.headTree) {
|
|
725
|
+
return failed('ORCHESTRATION_REPOSITORY_CHANGED', 'repository changed while clean completion evidence was collected', resolved.taskId);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const observedAt = (options.now ?? (() => new Date().toISOString()))();
|
|
729
|
+
const completionEvidence: CleanCompletionEvidence = {
|
|
730
|
+
kind: 'reviewed-head-clean',
|
|
731
|
+
observedAt,
|
|
732
|
+
head: after.head,
|
|
733
|
+
headTree: after.headTree,
|
|
734
|
+
worktreeTree: after.worktreeTree,
|
|
735
|
+
lastReviewedCommit,
|
|
736
|
+
prNumber,
|
|
737
|
+
prHead
|
|
738
|
+
};
|
|
739
|
+
const completed = withUpdatedRun(run, {
|
|
740
|
+
status: 'completed',
|
|
741
|
+
nextStage: null,
|
|
742
|
+
pause: null,
|
|
743
|
+
completionEvidence
|
|
744
|
+
});
|
|
745
|
+
saveRun(resolved.taskDir, completed);
|
|
746
|
+
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
233
747
|
}
|
|
234
|
-
return { status: 'running', changed: false, taskId: resolved.taskId, run
|
|
748
|
+
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function beginCommitIntent(
|
|
752
|
+
taskRef: string,
|
|
753
|
+
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string }>,
|
|
754
|
+
options: OrchestrationOptions = {}
|
|
755
|
+
): CommitIntentResult {
|
|
756
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
757
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
758
|
+
try {
|
|
759
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
760
|
+
const currentHead = repositoryHead(resolved.repoRoot);
|
|
761
|
+
if (currentHead !== input.baselineHead) {
|
|
762
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
763
|
+
}
|
|
764
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
765
|
+
try {
|
|
766
|
+
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
769
|
+
}
|
|
770
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const runFile = orchestrationPath(resolved.taskDir);
|
|
774
|
+
let run: OrchestrationRun | null = null;
|
|
775
|
+
let sourceRunBytes: string | null = null;
|
|
776
|
+
if (fs.existsSync(runFile)) {
|
|
777
|
+
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
778
|
+
try {
|
|
779
|
+
run = readRun(resolved.taskDir);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
782
|
+
}
|
|
783
|
+
if (!validCommitRun(run, resolved.taskId)) {
|
|
784
|
+
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
789
|
+
const failAndPause = (code: string, message: string): CommitIntentResult => {
|
|
790
|
+
if (run?.status === 'running') {
|
|
791
|
+
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
792
|
+
status: 'paused', pause: { code, message, recoverable: true }
|
|
793
|
+
}, () => now));
|
|
794
|
+
}
|
|
795
|
+
return commitIntentFailed(code, message, resolved.taskId);
|
|
796
|
+
};
|
|
797
|
+
let orchestration: CommitIntent['orchestration'] = null;
|
|
798
|
+
if (!input.orchestrated) {
|
|
799
|
+
if (run?.pendingDelegation) {
|
|
800
|
+
return commitIntentFailed(
|
|
801
|
+
'ORCHESTRATION_STANDALONE_BUSY',
|
|
802
|
+
'standalone commit is blocked by a pending orchestration delegation',
|
|
803
|
+
resolved.taskId
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
if (!run || run.status !== 'running') {
|
|
808
|
+
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
809
|
+
}
|
|
810
|
+
const receipt = run.pendingDelegation;
|
|
811
|
+
if (
|
|
812
|
+
!receipt
|
|
813
|
+
|| receipt.status !== 'activated'
|
|
814
|
+
|| receipt.stage !== 'commit'
|
|
815
|
+
|| receipt.round !== 1
|
|
816
|
+
|| receipt.artifact !== 'commit'
|
|
817
|
+
|| receipt.role !== 'executor'
|
|
818
|
+
) {
|
|
819
|
+
return failAndPause(
|
|
820
|
+
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
821
|
+
'orchestrated commit requires one matching activated commit delegation'
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
825
|
+
return failAndPause(
|
|
826
|
+
'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
|
|
827
|
+
'orchestrated commit requires an unconsumed one-use authorization'
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
831
|
+
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
832
|
+
}, { ...options, now: () => now });
|
|
833
|
+
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
834
|
+
return failAndPause(
|
|
835
|
+
planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
836
|
+
planned.result.error?.message ?? 'commit delegation could not be completed'
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
840
|
+
orchestration = {
|
|
841
|
+
runId: run.runId,
|
|
842
|
+
receiptId: receipt.id,
|
|
843
|
+
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
844
|
+
sourceRunDigest: digest(sourceRunBytes!),
|
|
845
|
+
plannedRunDigest: digest(plannedRunBytes),
|
|
846
|
+
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
847
|
+
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const created = createCommitIntent(resolved.taskDir, {
|
|
852
|
+
taskId: resolved.taskId,
|
|
853
|
+
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
854
|
+
phase: 'prepared',
|
|
855
|
+
baselineHead: input.baselineHead,
|
|
856
|
+
committedHead: null,
|
|
857
|
+
pushEvidence: null,
|
|
858
|
+
orchestration,
|
|
859
|
+
createdAt: now,
|
|
860
|
+
updatedAt: now
|
|
861
|
+
}, { token: options.token });
|
|
862
|
+
return {
|
|
863
|
+
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
864
|
+
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function checkpointCommitIntent(
|
|
873
|
+
taskRef: string,
|
|
874
|
+
input: Readonly<{
|
|
875
|
+
token: string;
|
|
876
|
+
kind: 'committed' | 'pushed';
|
|
877
|
+
head: string;
|
|
878
|
+
remote?: string;
|
|
879
|
+
ref?: string;
|
|
880
|
+
}>,
|
|
881
|
+
options: OrchestrationOptions = {}
|
|
882
|
+
): CommitIntentResult {
|
|
883
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
884
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
885
|
+
try {
|
|
886
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
887
|
+
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
888
|
+
const currentHead = repositoryHead(resolved.repoRoot);
|
|
889
|
+
if (currentHead !== input.head) {
|
|
890
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
891
|
+
}
|
|
892
|
+
if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
|
|
893
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
894
|
+
}
|
|
895
|
+
if (input.kind === 'pushed') {
|
|
896
|
+
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
897
|
+
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
898
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
902
|
+
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
903
|
+
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
904
|
+
: {
|
|
905
|
+
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
906
|
+
pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
|
|
907
|
+
});
|
|
908
|
+
return {
|
|
909
|
+
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
910
|
+
intent: commitIntentView(updated, currentHead), error: null
|
|
911
|
+
};
|
|
912
|
+
});
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function completeCommitOrchestration(
|
|
919
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
920
|
+
intent: CommitIntent,
|
|
921
|
+
agent: string
|
|
922
|
+
): Readonly<{ code: string; message: string }> | null {
|
|
923
|
+
if (intent.orchestration === null) return null;
|
|
924
|
+
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
925
|
+
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
926
|
+
}
|
|
927
|
+
const runFile = orchestrationPath(resolved.taskDir);
|
|
928
|
+
if (!fs.existsSync(runFile)) {
|
|
929
|
+
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
930
|
+
}
|
|
931
|
+
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
932
|
+
const currentDigest = digest(currentBytes);
|
|
933
|
+
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
934
|
+
const run = readRun(resolved.taskDir);
|
|
935
|
+
if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
936
|
+
const plannedRun: OrchestrationRun = Object.freeze({
|
|
937
|
+
...run,
|
|
938
|
+
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
939
|
+
updatedAt: intent.orchestration.completionUpdatedAt
|
|
940
|
+
});
|
|
941
|
+
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
942
|
+
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
943
|
+
}
|
|
944
|
+
atomicWrite(runFile, plannedRun);
|
|
945
|
+
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
946
|
+
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
947
|
+
}
|
|
948
|
+
} else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
949
|
+
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function finalizeCommitIntentUnlocked(
|
|
955
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
956
|
+
inspection: CommitFinalizationInspection,
|
|
957
|
+
agent: string,
|
|
958
|
+
remove: () => void
|
|
959
|
+
): CommitIntentResult {
|
|
960
|
+
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
961
|
+
return commitIntentFailed(
|
|
962
|
+
inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
|
|
963
|
+
inspection.message,
|
|
964
|
+
resolved.taskId
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (
|
|
968
|
+
inspection.intent.orchestration !== null
|
|
969
|
+
&& inspection.intent.orchestration.plannedReceipt.agent !== agent
|
|
970
|
+
) {
|
|
971
|
+
return commitIntentFailed(
|
|
972
|
+
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
973
|
+
'completion agent does not match the planned receipt',
|
|
974
|
+
resolved.taskId
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const metadata = captureTaskWriteMetadata();
|
|
978
|
+
const taskPlan = planCommitTaskFinalization(
|
|
979
|
+
resolved.taskDir,
|
|
980
|
+
inspection,
|
|
981
|
+
agent,
|
|
982
|
+
metadata.timestamp
|
|
983
|
+
);
|
|
984
|
+
const written = writeTask({
|
|
985
|
+
taskRef: resolved.taskId,
|
|
986
|
+
expectedState: 'active',
|
|
987
|
+
mutations: taskPlan.mutations
|
|
988
|
+
}, {
|
|
989
|
+
repoRoot: resolved.repoRoot,
|
|
990
|
+
metadataProvider: () => metadata
|
|
991
|
+
});
|
|
992
|
+
if (written.status === 'failed') {
|
|
993
|
+
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
994
|
+
}
|
|
995
|
+
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
996
|
+
if (orchestrationError) {
|
|
997
|
+
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
remove();
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
return commitIntentFailed(
|
|
1003
|
+
'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
|
|
1004
|
+
error instanceof Error ? error.message : String(error),
|
|
1005
|
+
resolved.taskId
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function completeCommitIntent(
|
|
1012
|
+
taskRef: string,
|
|
1013
|
+
input: Readonly<{ token: string; agent: string }>,
|
|
1014
|
+
options: OrchestrationOptions = {}
|
|
1015
|
+
): CommitIntentResult {
|
|
1016
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1017
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1018
|
+
try {
|
|
1019
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1020
|
+
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1021
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
|
|
1022
|
+
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1023
|
+
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function recoverCommitIntent(
|
|
1032
|
+
taskRef: string,
|
|
1033
|
+
input: Readonly<{ agent: string }>,
|
|
1034
|
+
options: OrchestrationOptions = {}
|
|
1035
|
+
): CommitIntentResult {
|
|
1036
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1037
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1038
|
+
try {
|
|
1039
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1040
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
|
|
1041
|
+
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1042
|
+
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1043
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1044
|
+
}
|
|
1045
|
+
if (!inspection.intentDigest) {
|
|
1046
|
+
return commitIntentFailed(
|
|
1047
|
+
inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
1048
|
+
inspection.message,
|
|
1049
|
+
resolved.taskId
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1053
|
+
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function abortCommitIntent(
|
|
1062
|
+
taskRef: string,
|
|
1063
|
+
input: Readonly<{ token: string; expectedHead: string }>,
|
|
1064
|
+
options: OrchestrationOptions = {}
|
|
1065
|
+
): CommitIntentResult {
|
|
1066
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1067
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1068
|
+
try {
|
|
1069
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1070
|
+
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1071
|
+
const currentHead = repositoryHead(resolved.repoRoot);
|
|
1072
|
+
if (
|
|
1073
|
+
intent.phase !== 'prepared'
|
|
1074
|
+
|| intent.pushEvidence !== null
|
|
1075
|
+
|| input.expectedHead !== intent.baselineHead
|
|
1076
|
+
|| currentHead !== intent.baselineHead
|
|
1077
|
+
) {
|
|
1078
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1079
|
+
}
|
|
1080
|
+
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1081
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1082
|
+
});
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
|
|
1089
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1090
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1091
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
|
|
1092
|
+
return {
|
|
1093
|
+
status: 'ready',
|
|
1094
|
+
changed: false,
|
|
1095
|
+
taskId: resolved.taskId,
|
|
1096
|
+
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1097
|
+
finalization: commitFinalizationView(inspection),
|
|
1098
|
+
error: null
|
|
1099
|
+
};
|
|
235
1100
|
}
|
|
236
1101
|
|
|
237
1102
|
function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -242,24 +1107,33 @@ function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}
|
|
|
242
1107
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
243
1108
|
}
|
|
244
1109
|
|
|
245
|
-
function
|
|
1110
|
+
function prepareOrchestrationDelegationUnlocked(
|
|
246
1111
|
taskRef: string,
|
|
247
|
-
input: Readonly<{ client: AgentClientId; requestedModel?: string }>,
|
|
1112
|
+
input: Readonly<{ client: AgentClientId; requestedModel?: string; requestedReasoningEffort?: string }>,
|
|
248
1113
|
options: OrchestrationOptions = {}
|
|
249
1114
|
): OrchestrationResult {
|
|
250
1115
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
251
1116
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1117
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1118
|
+
try {
|
|
1119
|
+
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
return failed(
|
|
1122
|
+
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
1123
|
+
error instanceof Error ? error.message : String(error),
|
|
1124
|
+
resolved.taskId
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1128
|
+
}
|
|
1129
|
+
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
256
1130
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
257
1131
|
}
|
|
258
1132
|
const run = readRun(resolved.taskDir);
|
|
259
1133
|
if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
1134
|
+
if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
260
1135
|
if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
261
|
-
const repositoryPending = (
|
|
262
|
-
.flatMap((client) => matchingDelegations(client, () => true, options));
|
|
1136
|
+
const repositoryPending = matchingDelegations(() => true, options);
|
|
263
1137
|
if (repositoryPending.length > 0) {
|
|
264
1138
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
|
|
265
1139
|
}
|
|
@@ -267,9 +1141,22 @@ function prepareOrchestrationDelegation(
|
|
|
267
1141
|
const routed = routeOrchestration(taskRef, options);
|
|
268
1142
|
if (!routed.next) return routed;
|
|
269
1143
|
const next = routed.next;
|
|
1144
|
+
if (!validModel(input.requestedModel)) {
|
|
1145
|
+
return failed('ORCHESTRATION_REQUESTED_MODEL_REQUIRED', 'prepare requires the exact requested model identity', resolved.taskId);
|
|
1146
|
+
}
|
|
1147
|
+
if (!validModel(input.requestedReasoningEffort)) {
|
|
1148
|
+
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_REQUIRED', 'prepare requires the exact requested reasoning effort', resolved.taskId);
|
|
1149
|
+
}
|
|
1150
|
+
const expectedPolicy = run.modelPolicy[next.role];
|
|
1151
|
+
if (input.requestedModel !== expectedPolicy.model) {
|
|
1152
|
+
return failed('ORCHESTRATION_REQUESTED_MODEL_MISMATCH', `requested model does not match the persisted ${next.role} model`, resolved.taskId);
|
|
1153
|
+
}
|
|
1154
|
+
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1155
|
+
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1156
|
+
}
|
|
270
1157
|
let beforeFingerprint: string;
|
|
271
1158
|
try {
|
|
272
|
-
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot);
|
|
1159
|
+
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
|
|
273
1160
|
} catch (error) {
|
|
274
1161
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
275
1162
|
}
|
|
@@ -281,7 +1168,9 @@ function prepareOrchestrationDelegation(
|
|
|
281
1168
|
round: next.round,
|
|
282
1169
|
artifact: next.artifact,
|
|
283
1170
|
client: input.client,
|
|
284
|
-
requestedModel: input.requestedModel
|
|
1171
|
+
requestedModel: input.requestedModel,
|
|
1172
|
+
requestedReasoningEffort: input.requestedReasoningEffort,
|
|
1173
|
+
workspaceSnapshotScope: 'task',
|
|
285
1174
|
beforeFingerprint
|
|
286
1175
|
}, { id: options.id, now: options.now });
|
|
287
1176
|
const updated = withUpdatedRun(run, {
|
|
@@ -295,8 +1184,32 @@ function prepareOrchestrationDelegation(
|
|
|
295
1184
|
return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next, error: null };
|
|
296
1185
|
}
|
|
297
1186
|
|
|
1187
|
+
function prepareOrchestrationDelegation(
|
|
1188
|
+
taskRef: string,
|
|
1189
|
+
input: Readonly<{ client: AgentClientId; requestedModel?: string; requestedReasoningEffort?: string }>,
|
|
1190
|
+
options: OrchestrationOptions = {}
|
|
1191
|
+
): OrchestrationResult {
|
|
1192
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1193
|
+
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1194
|
+
try {
|
|
1195
|
+
return withTaskExecutionLock(
|
|
1196
|
+
resolved.repoRoot,
|
|
1197
|
+
'__repository__',
|
|
1198
|
+
'task-orchestration.prepare.repository',
|
|
1199
|
+
() => withTaskExecutionLock(
|
|
1200
|
+
resolved.repoRoot,
|
|
1201
|
+
resolved.taskId,
|
|
1202
|
+
'task-orchestration.prepare.task',
|
|
1203
|
+
() => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
|
|
1204
|
+
)
|
|
1205
|
+
);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (error instanceof TaskExecutionLockError) return failed(error.code, error.message, resolved.taskId);
|
|
1208
|
+
throw error;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
298
1212
|
function matchingDelegations(
|
|
299
|
-
client: AgentClientId,
|
|
300
1213
|
predicate: (receipt: DelegationReceipt) => boolean,
|
|
301
1214
|
options: OrchestrationOptions
|
|
302
1215
|
): Array<{ taskId: string; run: OrchestrationRun }> {
|
|
@@ -309,7 +1222,7 @@ function matchingDelegations(
|
|
|
309
1222
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
310
1223
|
if (!run?.pendingDelegation) return [];
|
|
311
1224
|
const receipt = run.pendingDelegation;
|
|
312
|
-
return
|
|
1225
|
+
return predicate(receipt)
|
|
313
1226
|
? [{ taskId: entry.name, run }]
|
|
314
1227
|
: [];
|
|
315
1228
|
});
|
|
@@ -320,7 +1233,10 @@ function uniqueMatchingDelegation(
|
|
|
320
1233
|
predicate: (receipt: DelegationReceipt) => boolean,
|
|
321
1234
|
options: OrchestrationOptions
|
|
322
1235
|
): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
|
|
323
|
-
const matches = matchingDelegations(
|
|
1236
|
+
const matches = matchingDelegations(
|
|
1237
|
+
(receipt) => receipt.client === client && predicate(receipt),
|
|
1238
|
+
options
|
|
1239
|
+
);
|
|
324
1240
|
if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
|
|
325
1241
|
if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
|
|
326
1242
|
return matches[0]!;
|
|
@@ -364,7 +1280,8 @@ function sealMatchingOrchestrationDelegation(
|
|
|
364
1280
|
}
|
|
365
1281
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
366
1282
|
try {
|
|
367
|
-
const
|
|
1283
|
+
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
1284
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot, snapshotTaskId);
|
|
368
1285
|
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(repoRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
369
1286
|
return sealOrchestrationDelegation(matched.taskId, {
|
|
370
1287
|
childId: event.childId,
|
|
@@ -419,36 +1336,20 @@ function completeOrchestrationStage(
|
|
|
419
1336
|
return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
420
1337
|
}
|
|
421
1338
|
|
|
422
|
-
function
|
|
1339
|
+
function inspectOrchestrationStage(
|
|
423
1340
|
taskRef: string,
|
|
424
|
-
|
|
1341
|
+
event: OrchestrationStageIdentity,
|
|
425
1342
|
options: OrchestrationOptions = {}
|
|
426
1343
|
): OrchestrationResult {
|
|
427
1344
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
428
1345
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return failed('
|
|
1346
|
+
let run: OrchestrationRun | null;
|
|
1347
|
+
try {
|
|
1348
|
+
run = readRun(resolved.taskDir);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
434
1351
|
}
|
|
435
|
-
return
|
|
436
|
-
stage: receipt.stage,
|
|
437
|
-
round: receipt.round,
|
|
438
|
-
artifact: receipt.artifact,
|
|
439
|
-
agent
|
|
440
|
-
}, options);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function validateOrchestrationStage(
|
|
444
|
-
taskRef: string,
|
|
445
|
-
event: Readonly<{ stage: DelegationStage; round: number; artifact: string; role: DelegationRole }>,
|
|
446
|
-
options: OrchestrationOptions = {}
|
|
447
|
-
): OrchestrationResult {
|
|
448
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
449
|
-
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
450
|
-
const run = readRun(resolved.taskDir);
|
|
451
|
-
if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
1352
|
+
if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
452
1353
|
const receipt = run.pendingDelegation;
|
|
453
1354
|
if (
|
|
454
1355
|
!receipt
|
|
@@ -458,17 +1359,46 @@ function validateOrchestrationStage(
|
|
|
458
1359
|
|| receipt.artifact !== event.artifact
|
|
459
1360
|
|| receipt.role !== event.role
|
|
460
1361
|
) {
|
|
461
|
-
return
|
|
462
|
-
taskRef,
|
|
1362
|
+
return failed(
|
|
463
1363
|
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
464
1364
|
'active run does not have one matching activated delegation',
|
|
465
|
-
|
|
466
|
-
options
|
|
1365
|
+
resolved.taskId
|
|
467
1366
|
);
|
|
468
1367
|
}
|
|
469
1368
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
470
1369
|
}
|
|
471
1370
|
|
|
1371
|
+
function planOrchestrationStageCompletion(
|
|
1372
|
+
taskRef: string,
|
|
1373
|
+
event: Readonly<{ stage: DelegationStage; round: number; artifact: string; role: DelegationRole; agent: string }>,
|
|
1374
|
+
options: OrchestrationOptions = {}
|
|
1375
|
+
): OrchestrationCompletionPlanResult {
|
|
1376
|
+
const inspected = inspectOrchestrationStage(taskRef, event, options);
|
|
1377
|
+
if (inspected.status === 'failed' || !inspected.run?.pendingDelegation) {
|
|
1378
|
+
return { result: inspected, plan: null };
|
|
1379
|
+
}
|
|
1380
|
+
const receipt = inspected.run.pendingDelegation;
|
|
1381
|
+
const completed = completeDelegationStage(receipt, event);
|
|
1382
|
+
if (!completed.ok) {
|
|
1383
|
+
return { result: failed(completed.code, completed.message, inspected.taskId), plan: null };
|
|
1384
|
+
}
|
|
1385
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1386
|
+
if (!resolved.ok) return { result: failed(resolved.code, resolved.message, resolved.taskId), plan: null };
|
|
1387
|
+
const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt }, options.now);
|
|
1388
|
+
return {
|
|
1389
|
+
result: { status: 'running', changed: true, taskId: resolved.taskId, run: updatedRun, next: null, error: null },
|
|
1390
|
+
plan: {
|
|
1391
|
+
taskId: resolved.taskId,
|
|
1392
|
+
taskDir: resolved.taskDir,
|
|
1393
|
+
updatedRun
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function commitOrchestrationStageCompletion(plan: OrchestrationStageCompletion): void {
|
|
1399
|
+
saveRun(plan.taskDir, plan.updatedRun);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
472
1402
|
function sealOrchestrationDelegation(
|
|
473
1403
|
taskRef: string,
|
|
474
1404
|
event: Parameters<typeof sealDelegation>[1],
|
|
@@ -527,17 +1457,34 @@ export {
|
|
|
527
1457
|
activateMatchingOrchestrationDelegation,
|
|
528
1458
|
activateOrchestrationDelegation,
|
|
529
1459
|
advanceOrchestration,
|
|
1460
|
+
abortCommitIntent,
|
|
1461
|
+
beginCommitIntent,
|
|
530
1462
|
beginOrResumeOrchestration,
|
|
531
|
-
|
|
1463
|
+
checkpointCommitIntent,
|
|
1464
|
+
commitOrchestrationStageCompletion,
|
|
1465
|
+
completeCommitIntent,
|
|
532
1466
|
completeOrchestrationStage,
|
|
1467
|
+
inspectOrchestrationStage,
|
|
533
1468
|
orchestrationPath,
|
|
534
1469
|
pauseOrchestration,
|
|
1470
|
+
planOrchestrationStageCompletion,
|
|
535
1471
|
prepareOrchestrationDelegation,
|
|
536
1472
|
readRun,
|
|
1473
|
+
recoverCommitIntent,
|
|
537
1474
|
routeOrchestration,
|
|
538
1475
|
sealMatchingOrchestrationDelegation,
|
|
539
1476
|
sealOrchestrationDelegation,
|
|
540
|
-
|
|
541
|
-
|
|
1477
|
+
statusCommitIntent,
|
|
1478
|
+
statusOrchestration
|
|
1479
|
+
};
|
|
1480
|
+
export type {
|
|
1481
|
+
CommitIntentResult,
|
|
1482
|
+
OrchestrationCompletionPlanResult,
|
|
1483
|
+
OrchestrationModelPolicy,
|
|
1484
|
+
OrchestrationNext,
|
|
1485
|
+
OrchestrationResult,
|
|
1486
|
+
OrchestrationRun,
|
|
1487
|
+
OrchestrationStageCompletion,
|
|
1488
|
+
OrchestrationStageIdentity,
|
|
1489
|
+
OrchestrationStatus
|
|
542
1490
|
};
|
|
543
|
-
export type { OrchestrationNext, OrchestrationResult, OrchestrationRun, OrchestrationStatus };
|