@fitlab-ai/agent-infra 0.9.2 → 0.9.3
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 +10 -9
- package/README.zh-CN.md +10 -9
- package/dist/bin/internal-cli.js +15 -0
- package/dist/lib/agent-clients/adapter.js +45 -2
- package/dist/lib/agent-clients/adapters/antigravity-cli.js +64 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +10 -1
- 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.js +9 -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 +7 -1
- 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/task-activity.js +73 -0
- package/dist/lib/internal/task-event.js +17 -1
- package/dist/lib/internal/task-lifecycle.js +7 -0
- package/dist/lib/internal/task-orchestration.js +51 -6
- 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/issue-comments.js +3 -2
- package/dist/lib/platform/merged-pr-equivalence.js +115 -5
- package/dist/lib/platform/pr-checks.js +64 -41
- package/dist/lib/platform/pr-review.js +166 -0
- package/dist/lib/platform/pull-requests.js +324 -3
- package/dist/lib/platform/release-notes.js +85 -3
- package/dist/lib/pr-review/evidence-grading.js +229 -0
- package/dist/lib/run/tui.js +3 -5
- package/dist/lib/sandbox/commands/create.js +0 -20
- package/dist/lib/sandbox/commands/show.js +1 -1
- package/dist/lib/task/activity-intent.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +2 -1
- package/dist/lib/task/commands/log.js +15 -14
- package/dist/lib/task/delegation-receipts.js +23 -2
- package/dist/lib/task/events.js +67 -28
- package/dist/lib/task/finalization-artifacts.js +35 -0
- package/dist/lib/task/lifecycle-execution.js +46 -0
- package/dist/lib/task/orchestration.js +408 -44
- 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 +75 -2
- package/lib/agent-clients/adapters/antigravity-cli.ts +65 -0
- package/lib/agent-clients/adapters/claude-code.ts +10 -1
- package/lib/agent-clients/adapters/codex.ts +9 -0
- package/lib/agent-clients/adapters/index.ts +2 -2
- package/lib/agent-clients/adapters/opencode.ts +9 -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 +10 -0
- 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/task-activity.ts +55 -0
- package/lib/internal/task-event.ts +9 -1
- package/lib/internal/task-lifecycle.ts +4 -0
- package/lib/internal/task-orchestration.ts +45 -6
- 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/issue-comments.ts +3 -2
- package/lib/platform/merged-pr-equivalence.ts +127 -5
- package/lib/platform/pr-checks.ts +72 -45
- package/lib/platform/pr-review.ts +201 -0
- package/lib/platform/pull-requests.ts +395 -2
- package/lib/platform/release-notes.ts +100 -4
- package/lib/pr-review/evidence-grading.ts +318 -0
- package/lib/run/tui.ts +3 -4
- package/lib/sandbox/commands/create.ts +0 -20
- package/lib/sandbox/commands/show.ts +1 -1
- package/lib/server/server.schema.json +2 -2
- package/lib/task/activity-intent.ts +128 -0
- package/lib/task/artifact-lifecycle.ts +2 -1
- package/lib/task/commands/log.ts +15 -14
- package/lib/task/delegation-receipts.ts +32 -3
- package/lib/task/events.ts +75 -30
- package/lib/task/finalization-artifacts.ts +58 -0
- package/lib/task/lifecycle-execution.ts +106 -0
- package/lib/task/orchestration.ts +543 -64
- 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/create-issue.github.en.md +4 -2
- package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -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 +2 -2
- 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/task-management.en.md +11 -0
- package/templates/.agents/rules/task-management.zh-CN.md +11 -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 +9 -6
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -8
- package/templates/.agents/skills/commit/SKILL.en.md +3 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -1
- 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/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/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 +21 -6
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +21 -6
- 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 +3 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -1
- 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/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 +9 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +9 -6
- 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 +158 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +158 -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 +35 -61
- 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 +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- 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/.gemini/commands/_project_/review-pr.en.toml +10 -0
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +10 -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/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
|
@@ -8,7 +8,8 @@ import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.
|
|
|
8
8
|
import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
|
|
9
9
|
import { finalizeReviewSummaryContent } from './review-artifacts.ts';
|
|
10
10
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
11
|
-
import {
|
|
11
|
+
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
12
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
12
13
|
import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
|
|
13
14
|
|
|
14
15
|
type ReviewFinalizationErrorCode =
|
|
@@ -31,6 +32,7 @@ type ReviewFinalizationRequest = {
|
|
|
31
32
|
taskRef: string;
|
|
32
33
|
stage: string;
|
|
33
34
|
artifact: string;
|
|
35
|
+
orchestrated?: boolean;
|
|
34
36
|
dryRun?: boolean;
|
|
35
37
|
};
|
|
36
38
|
type ReviewFinalizationResult = {
|
|
@@ -113,7 +115,7 @@ function cleanupTemp(fileSystem: ReviewFileSystem, tempPath: string): void {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function
|
|
118
|
+
function finalizeReviewSummaryUnlocked(
|
|
117
119
|
request: ReviewFinalizationRequest,
|
|
118
120
|
options: ReviewFinalizationOptions = {}
|
|
119
121
|
): ReviewFinalizationResult {
|
|
@@ -167,17 +169,21 @@ function finalizeReviewSummary(
|
|
|
167
169
|
resolved.taskId
|
|
168
170
|
);
|
|
169
171
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const execution = validateLifecycleExecution(request.taskRef, {
|
|
173
|
+
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
174
|
+
identity: {
|
|
175
|
+
stage: spec.family,
|
|
176
|
+
round: parsedArtifact.round,
|
|
177
|
+
artifact: request.artifact,
|
|
178
|
+
role: 'reviewer'
|
|
179
|
+
},
|
|
180
|
+
dryRun: request.dryRun
|
|
175
181
|
}, { repoRoot: options.repoRoot });
|
|
176
|
-
if (
|
|
182
|
+
if (!execution.ok) {
|
|
177
183
|
return failed(
|
|
178
184
|
request,
|
|
179
185
|
'REVIEW_PROVENANCE_INVALID',
|
|
180
|
-
|
|
186
|
+
`${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`,
|
|
181
187
|
resolved.taskId
|
|
182
188
|
);
|
|
183
189
|
}
|
|
@@ -265,6 +271,31 @@ function finalizeReviewSummary(
|
|
|
265
271
|
};
|
|
266
272
|
}
|
|
267
273
|
|
|
274
|
+
function finalizeReviewSummary(
|
|
275
|
+
request: ReviewFinalizationRequest,
|
|
276
|
+
options: ReviewFinalizationOptions = {}
|
|
277
|
+
): ReviewFinalizationResult {
|
|
278
|
+
if (request.dryRun) return finalizeReviewSummaryUnlocked(request, options);
|
|
279
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
280
|
+
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, options);
|
|
281
|
+
try {
|
|
282
|
+
return withTaskExecutionLock(
|
|
283
|
+
resolved.repoRoot,
|
|
284
|
+
resolved.taskId,
|
|
285
|
+
'task-review.finalize-summary',
|
|
286
|
+
() => finalizeReviewSummaryUnlocked(request, options)
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
290
|
+
return failed(
|
|
291
|
+
request,
|
|
292
|
+
'REVIEW_PROVENANCE_INVALID',
|
|
293
|
+
`${error.code}: ${error.message}`,
|
|
294
|
+
resolved.taskId
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
268
299
|
export { finalizeReviewSummary };
|
|
269
300
|
export type {
|
|
270
301
|
ReviewFinalizationError,
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import { getProcessStartTime, processIdentityMatches } from '../server/process-state.ts';
|
|
7
|
+
|
|
8
|
+
type TaskExecutionLockErrorCode =
|
|
9
|
+
| 'ORCHESTRATION_LOCK_BUSY'
|
|
10
|
+
| 'ORCHESTRATION_LOCK_UNSUPPORTED'
|
|
11
|
+
| 'ORCHESTRATION_LOCK_FAILED';
|
|
12
|
+
|
|
13
|
+
type TaskExecutionLockOwner = Readonly<{
|
|
14
|
+
version: 1;
|
|
15
|
+
pid: number;
|
|
16
|
+
startTime: string;
|
|
17
|
+
token: string;
|
|
18
|
+
owner: string;
|
|
19
|
+
canonicalRepoRoot: string;
|
|
20
|
+
taskId: string;
|
|
21
|
+
acquiredAt: string;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
type LinkOperation = 'acquire' | 'quarantine';
|
|
25
|
+
type LinkDisposition = 'exists' | 'missing' | TaskExecutionLockError;
|
|
26
|
+
|
|
27
|
+
type TaskExecutionLockOptions = Readonly<{
|
|
28
|
+
lockRoot?: string;
|
|
29
|
+
token?: () => string;
|
|
30
|
+
now?: () => string;
|
|
31
|
+
getStartTime?: (pid: number) => string | null;
|
|
32
|
+
identityMatches?: (record: { version: 1; pid: number; startTime: string }) => boolean;
|
|
33
|
+
linkSync?: (existingPath: string, newPath: string) => void;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
class TaskExecutionLockError extends Error {
|
|
37
|
+
readonly code: TaskExecutionLockErrorCode;
|
|
38
|
+
readonly detail: Readonly<{ operation: string; errno: string | null; key: string }>;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
code: TaskExecutionLockErrorCode,
|
|
42
|
+
message: string,
|
|
43
|
+
detail: Readonly<{ operation: string; errno: string | null; key: string }>
|
|
44
|
+
) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'TaskExecutionLockError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.detail = detail;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function errno(error: unknown): string | null {
|
|
53
|
+
const value = (error as NodeJS.ErrnoException | null)?.code;
|
|
54
|
+
return typeof value === 'string' ? value : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapLinkError(operation: LinkOperation, error: unknown, key: string): LinkDisposition {
|
|
58
|
+
const code = errno(error);
|
|
59
|
+
if (code === 'EEXIST') return 'exists';
|
|
60
|
+
if (operation === 'quarantine' && code === 'ENOENT') return 'missing';
|
|
61
|
+
const detail = { operation, errno: code, key };
|
|
62
|
+
if (['EPERM', 'EACCES', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV'].includes(code ?? '')) {
|
|
63
|
+
return new TaskExecutionLockError(
|
|
64
|
+
'ORCHESTRATION_LOCK_UNSUPPORTED',
|
|
65
|
+
`lifecycle lock hard-link operation '${operation}' is unsupported`,
|
|
66
|
+
detail
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return new TaskExecutionLockError(
|
|
70
|
+
'ORCHESTRATION_LOCK_FAILED',
|
|
71
|
+
`lifecycle lock hard-link operation '${operation}' failed`,
|
|
72
|
+
detail
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function lockKey(repoRoot: string, taskId: string): Readonly<{ canonicalRepoRoot: string; key: string }> {
|
|
77
|
+
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
78
|
+
const key = createHash('sha256').update(`${canonicalRepoRoot}\0${taskId}`).digest('hex');
|
|
79
|
+
return { canonicalRepoRoot, key };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseOwner(raw: string, key: string): TaskExecutionLockOwner {
|
|
83
|
+
try {
|
|
84
|
+
const value = JSON.parse(raw) as Partial<TaskExecutionLockOwner> | null;
|
|
85
|
+
if (
|
|
86
|
+
value !== null
|
|
87
|
+
&& value.version === 1
|
|
88
|
+
&& Number.isSafeInteger(value.pid)
|
|
89
|
+
&& (value.pid ?? 0) > 0
|
|
90
|
+
&& typeof value.startTime === 'string'
|
|
91
|
+
&& value.startTime.length > 0
|
|
92
|
+
&& typeof value.token === 'string'
|
|
93
|
+
&& value.token.length > 0
|
|
94
|
+
&& typeof value.owner === 'string'
|
|
95
|
+
&& typeof value.canonicalRepoRoot === 'string'
|
|
96
|
+
&& typeof value.taskId === 'string'
|
|
97
|
+
&& typeof value.acquiredAt === 'string'
|
|
98
|
+
) {
|
|
99
|
+
return value as TaskExecutionLockOwner;
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Fall through to the stable lock failure below.
|
|
103
|
+
}
|
|
104
|
+
throw new TaskExecutionLockError(
|
|
105
|
+
'ORCHESTRATION_LOCK_FAILED',
|
|
106
|
+
'lifecycle lock owner record is invalid',
|
|
107
|
+
{ operation: 'read', errno: null, key }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unlinkIfPresent(file: string): void {
|
|
112
|
+
try {
|
|
113
|
+
fs.unlinkSync(file);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (errno(error) !== 'ENOENT') throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sameInode(left: fs.Stats, right: fs.Stats): boolean {
|
|
120
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function reclaimStaleLock(
|
|
124
|
+
fixed: string,
|
|
125
|
+
quarantine: string,
|
|
126
|
+
snapshot: string,
|
|
127
|
+
key: string,
|
|
128
|
+
link: (existingPath: string, newPath: string) => void
|
|
129
|
+
): void {
|
|
130
|
+
let linked = false;
|
|
131
|
+
try {
|
|
132
|
+
link(fixed, quarantine);
|
|
133
|
+
linked = true;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const disposition = mapLinkError('quarantine', error, key);
|
|
136
|
+
if (disposition === 'missing') return;
|
|
137
|
+
if (disposition === 'exists') {
|
|
138
|
+
try {
|
|
139
|
+
const quarantineSnapshot = fs.readFileSync(quarantine, 'utf8');
|
|
140
|
+
const fixedStat = fs.statSync(fixed);
|
|
141
|
+
const quarantineStat = fs.statSync(quarantine);
|
|
142
|
+
if (quarantineSnapshot === snapshot && sameInode(fixedStat, quarantineStat)) {
|
|
143
|
+
unlinkIfPresent(quarantine);
|
|
144
|
+
}
|
|
145
|
+
} catch (readError) {
|
|
146
|
+
if (errno(readError) !== 'ENOENT') throw readError;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (disposition instanceof TaskExecutionLockError) throw disposition;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const fixedSnapshot = fs.readFileSync(fixed, 'utf8');
|
|
154
|
+
const fixedStat = fs.statSync(fixed);
|
|
155
|
+
const quarantineStat = fs.statSync(quarantine);
|
|
156
|
+
if (fixedSnapshot === snapshot && sameInode(fixedStat, quarantineStat)) unlinkIfPresent(fixed);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (errno(error) !== 'ENOENT') throw error;
|
|
159
|
+
} finally {
|
|
160
|
+
if (linked) unlinkIfPresent(quarantine);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cleanStaleCandidates(
|
|
165
|
+
lockRoot: string,
|
|
166
|
+
key: string,
|
|
167
|
+
identityMatches: (record: { version: 1; pid: number; startTime: string }) => boolean
|
|
168
|
+
): void {
|
|
169
|
+
const prefix = `${key}.candidate.`;
|
|
170
|
+
for (const name of fs.readdirSync(lockRoot)) {
|
|
171
|
+
if (!name.startsWith(prefix)) continue;
|
|
172
|
+
const candidate = path.join(lockRoot, name);
|
|
173
|
+
try {
|
|
174
|
+
const owner = parseOwner(fs.readFileSync(candidate, 'utf8'), key);
|
|
175
|
+
if (!identityMatches(owner)) unlinkIfPresent(candidate);
|
|
176
|
+
} catch {
|
|
177
|
+
// A malformed candidate never owns the fixed lock and is left for manual diagnosis.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function withTaskExecutionLock<T>(
|
|
183
|
+
repoRoot: string,
|
|
184
|
+
taskId: string,
|
|
185
|
+
ownerName: string,
|
|
186
|
+
callback: () => T,
|
|
187
|
+
options: TaskExecutionLockOptions = {}
|
|
188
|
+
): T {
|
|
189
|
+
let identity: Readonly<{ canonicalRepoRoot: string; key: string }>;
|
|
190
|
+
try {
|
|
191
|
+
identity = lockKey(repoRoot, taskId);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new TaskExecutionLockError(
|
|
194
|
+
'ORCHESTRATION_LOCK_FAILED',
|
|
195
|
+
'failed to canonicalize the repository for lifecycle locking',
|
|
196
|
+
{ operation: 'canonicalize', errno: errno(error), key: 'unresolved' }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const { canonicalRepoRoot, key } = identity;
|
|
200
|
+
const lockRoot = options.lockRoot ?? path.join(os.tmpdir(), 'agent-infra-lifecycle-locks');
|
|
201
|
+
const token = (options.token ?? randomUUID)();
|
|
202
|
+
const candidate = path.join(lockRoot, `${key}.candidate.${process.pid}.${token}`);
|
|
203
|
+
const fixed = path.join(lockRoot, `${key}.lock`);
|
|
204
|
+
const link = options.linkSync ?? fs.linkSync;
|
|
205
|
+
const startTime = (options.getStartTime ?? getProcessStartTime)(process.pid);
|
|
206
|
+
if (startTime === null) {
|
|
207
|
+
throw new TaskExecutionLockError(
|
|
208
|
+
'ORCHESTRATION_LOCK_FAILED',
|
|
209
|
+
'current process start time is unavailable',
|
|
210
|
+
{ operation: 'identity', errno: null, key }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const owner: TaskExecutionLockOwner = {
|
|
214
|
+
version: 1,
|
|
215
|
+
pid: process.pid,
|
|
216
|
+
startTime,
|
|
217
|
+
token,
|
|
218
|
+
owner: ownerName,
|
|
219
|
+
canonicalRepoRoot,
|
|
220
|
+
taskId,
|
|
221
|
+
acquiredAt: (options.now ?? (() => new Date().toISOString()))()
|
|
222
|
+
};
|
|
223
|
+
const serialized = `${JSON.stringify(owner)}\n`;
|
|
224
|
+
let ownsFixed = false;
|
|
225
|
+
try {
|
|
226
|
+
fs.mkdirSync(lockRoot, { recursive: true });
|
|
227
|
+
cleanStaleCandidates(lockRoot, key, options.identityMatches ?? processIdentityMatches);
|
|
228
|
+
const descriptor = fs.openSync(candidate, 'wx', 0o600);
|
|
229
|
+
try {
|
|
230
|
+
fs.writeFileSync(descriptor, serialized);
|
|
231
|
+
fs.fsyncSync(descriptor);
|
|
232
|
+
} finally {
|
|
233
|
+
fs.closeSync(descriptor);
|
|
234
|
+
}
|
|
235
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
236
|
+
try {
|
|
237
|
+
link(candidate, fixed);
|
|
238
|
+
ownsFixed = true;
|
|
239
|
+
break;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const disposition = mapLinkError('acquire', error, key);
|
|
242
|
+
if (disposition instanceof TaskExecutionLockError) throw disposition;
|
|
243
|
+
let snapshot: string;
|
|
244
|
+
let current: TaskExecutionLockOwner;
|
|
245
|
+
try {
|
|
246
|
+
snapshot = fs.readFileSync(fixed, 'utf8');
|
|
247
|
+
current = parseOwner(snapshot, key);
|
|
248
|
+
} catch (readError) {
|
|
249
|
+
if (errno(readError) === 'ENOENT') continue;
|
|
250
|
+
throw readError;
|
|
251
|
+
}
|
|
252
|
+
if ((options.identityMatches ?? processIdentityMatches)(current)) {
|
|
253
|
+
throw new TaskExecutionLockError(
|
|
254
|
+
'ORCHESTRATION_LOCK_BUSY',
|
|
255
|
+
'another lifecycle operation holds the task lock',
|
|
256
|
+
{ operation: 'acquire', errno: 'EEXIST', key }
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
reclaimStaleLock(fixed, `${fixed}.quarantine.${current.token}`, snapshot, key, link);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!ownsFixed) {
|
|
263
|
+
throw new TaskExecutionLockError(
|
|
264
|
+
'ORCHESTRATION_LOCK_BUSY',
|
|
265
|
+
'lifecycle task lock could not be acquired after stale recovery',
|
|
266
|
+
{ operation: 'acquire', errno: 'EEXIST', key }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
unlinkIfPresent(candidate);
|
|
270
|
+
return callback();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error instanceof TaskExecutionLockError) throw error;
|
|
273
|
+
throw new TaskExecutionLockError(
|
|
274
|
+
'ORCHESTRATION_LOCK_FAILED',
|
|
275
|
+
'lifecycle task lock operation failed',
|
|
276
|
+
{ operation: ownsFixed ? 'callback' : 'acquire', errno: errno(error), key }
|
|
277
|
+
);
|
|
278
|
+
} finally {
|
|
279
|
+
try {
|
|
280
|
+
unlinkIfPresent(candidate);
|
|
281
|
+
} catch {
|
|
282
|
+
// Candidate cleanup is best-effort and must not replace the primary result.
|
|
283
|
+
}
|
|
284
|
+
if (ownsFixed) {
|
|
285
|
+
try {
|
|
286
|
+
const current = parseOwner(fs.readFileSync(fixed, 'utf8'), key);
|
|
287
|
+
if (current.token === token) unlinkIfPresent(fixed);
|
|
288
|
+
} catch {
|
|
289
|
+
// A missing or replaced fixed lock is not owned by this invocation.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export { TaskExecutionLockError, lockKey, mapLinkError, withTaskExecutionLock };
|
|
296
|
+
export type { LinkDisposition, LinkOperation, TaskExecutionLockErrorCode, TaskExecutionLockOptions, TaskExecutionLockOwner };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const LOCAL_VERIFICATION_CHECKS = [
|
|
2
2
|
'task-meta', 'artifact', 'implementation-input', 'activity-log', 'completion-checklist',
|
|
3
|
-
'orchestration-state'
|
|
3
|
+
'orchestration-state', 'orchestration-evidence'
|
|
4
4
|
] as const;
|
|
5
5
|
|
|
6
6
|
function isLocalVerificationCheck(value: string): value is typeof LOCAL_VERIFICATION_CHECKS[number] {
|
|
@@ -114,6 +114,8 @@ function runCheck(type: any, context: any, shared: any): any {
|
|
|
114
114
|
return checkCompletionChecklist(context);
|
|
115
115
|
case "review-ledger":
|
|
116
116
|
return checkReviewLedger(context);
|
|
117
|
+
case "manual-validation":
|
|
118
|
+
return checkManualValidation(context);
|
|
117
119
|
case "review-summary":
|
|
118
120
|
return checkReviewSummary(context);
|
|
119
121
|
case "review-fact":
|
|
@@ -122,6 +124,8 @@ function runCheck(type: any, context: any, shared: any): any {
|
|
|
122
124
|
return checkPostReviewCommit(context);
|
|
123
125
|
case "orchestration-state":
|
|
124
126
|
return checkOrchestrationState(context);
|
|
127
|
+
case "orchestration-evidence":
|
|
128
|
+
return checkOrchestrationEvidence(context);
|
|
125
129
|
default: {
|
|
126
130
|
const adapter = PLATFORM_ADAPTERS[type];
|
|
127
131
|
if (!adapter) {
|
|
@@ -137,6 +141,25 @@ function runCheck(type: any, context: any, shared: any): any {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
function isFailClosedLegacyPause(run: any): boolean {
|
|
145
|
+
if (
|
|
146
|
+
run.schemaVersion !== 1
|
|
147
|
+
|| run.status !== 'paused'
|
|
148
|
+
|| run.pause?.recoverable !== false
|
|
149
|
+
|| !exactText(run.pause?.message)
|
|
150
|
+
|| !Array.isArray(run.receipts)
|
|
151
|
+
) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (run.pause.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED') {
|
|
155
|
+
return run.receipts.length > 0 && run.pendingDelegation === null;
|
|
156
|
+
}
|
|
157
|
+
if (run.pause.code === 'ORCHESTRATION_DELEGATION_BUSY') {
|
|
158
|
+
return run.pendingDelegation != null && run.pendingDelegation.status !== 'sealed';
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
140
163
|
function checkOrchestrationState({ taskDir }: any): any {
|
|
141
164
|
const file = path.join(taskDir, 'orchestration.json');
|
|
142
165
|
const stat = safeStat(file);
|
|
@@ -147,22 +170,177 @@ function checkOrchestrationState({ taskDir }: any): any {
|
|
|
147
170
|
} catch (error) {
|
|
148
171
|
return failResult('orchestration-state', `Invalid orchestration.json: ${String(error)}`);
|
|
149
172
|
}
|
|
150
|
-
if (
|
|
173
|
+
if (isFailClosedLegacyPause(run)) {
|
|
174
|
+
return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
|
|
175
|
+
}
|
|
176
|
+
if (run.schemaVersion !== 2 || !['paused', 'completed'].includes(run.status)) {
|
|
151
177
|
return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
|
|
152
178
|
}
|
|
153
179
|
if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
|
|
154
180
|
return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
|
|
155
181
|
}
|
|
156
182
|
if (run.status === 'completed') {
|
|
157
|
-
if (run.
|
|
183
|
+
if (run.completionEvidence != null) {
|
|
184
|
+
const evidenceError = validateCleanCompletionEvidence(run);
|
|
185
|
+
if (evidenceError) return failResult('orchestration-state', evidenceError);
|
|
186
|
+
} else if (run.pendingDelegation !== null || !run.commitAuthorization?.consumedAt) {
|
|
158
187
|
return failResult('orchestration-state', 'Completed run must consume commit authorization and clear pending delegation');
|
|
159
188
|
}
|
|
189
|
+
} else if (run.completionEvidence != null) {
|
|
190
|
+
return failResult('orchestration-state', 'Clean completion evidence requires a completed run');
|
|
160
191
|
} else if (run.pendingDelegation?.status === 'sealed') {
|
|
161
192
|
return failResult('orchestration-state', 'Paused run has a sealed delegation that could still advance');
|
|
162
193
|
}
|
|
163
194
|
return passResult('orchestration-state', `Orchestration run is safely ${run.status}`);
|
|
164
195
|
}
|
|
165
196
|
|
|
197
|
+
function exactText(value: unknown): value is string {
|
|
198
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateCleanCompletionEvidence(run: any): string | null {
|
|
202
|
+
const evidence = run.completionEvidence;
|
|
203
|
+
const shaPattern = /^[0-9a-f]{40}$/i;
|
|
204
|
+
if (
|
|
205
|
+
evidence?.kind !== 'reviewed-head-clean'
|
|
206
|
+
|| !exactText(evidence.observedAt)
|
|
207
|
+
|| Number.isNaN(Date.parse(evidence.observedAt))
|
|
208
|
+
|| !shaPattern.test(evidence.head)
|
|
209
|
+
|| !shaPattern.test(evidence.headTree)
|
|
210
|
+
|| !shaPattern.test(evidence.worktreeTree)
|
|
211
|
+
|| !shaPattern.test(evidence.lastReviewedCommit)
|
|
212
|
+
|| !Number.isInteger(evidence.prNumber)
|
|
213
|
+
|| evidence.prNumber <= 0
|
|
214
|
+
|| !shaPattern.test(evidence.prHead)
|
|
215
|
+
) {
|
|
216
|
+
return 'Clean completion evidence has an invalid structure';
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
evidence.head !== evidence.lastReviewedCommit
|
|
220
|
+
|| evidence.head !== evidence.prHead
|
|
221
|
+
|| evidence.headTree !== evidence.worktreeTree
|
|
222
|
+
) {
|
|
223
|
+
return 'Clean completion evidence does not bind matching commit and tree identities';
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
run.status !== 'completed'
|
|
227
|
+
|| run.pendingDelegation !== null
|
|
228
|
+
|| run.commitAuthorization?.issuedAt !== null
|
|
229
|
+
|| run.commitAuthorization?.consumedAt !== null
|
|
230
|
+
|| !Array.isArray(run.receipts)
|
|
231
|
+
|| run.receipts.some((receipt: any) => receipt.stage === 'commit')
|
|
232
|
+
) {
|
|
233
|
+
return 'Clean completion evidence conflicts with commit delegation state';
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
239
|
+
const file = path.join(taskDir, 'orchestration.json');
|
|
240
|
+
const stat = safeStat(file);
|
|
241
|
+
if (!stat?.isFile()) return failResult('orchestration-evidence', 'orchestration.json is missing');
|
|
242
|
+
let run: any;
|
|
243
|
+
try {
|
|
244
|
+
run = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return failResult('orchestration-evidence', `Invalid orchestration.json: ${String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
if (isFailClosedLegacyPause(run)) {
|
|
249
|
+
return passResult('orchestration-evidence', 'Legacy orchestration evidence is preserved in a fail-closed pause');
|
|
250
|
+
}
|
|
251
|
+
const policy = run.modelPolicy;
|
|
252
|
+
if (
|
|
253
|
+
!policy
|
|
254
|
+
|| !exactText(policy.executor?.model)
|
|
255
|
+
|| !exactText(policy.executor?.reasoningEffort)
|
|
256
|
+
|| !exactText(policy.reviewer?.model)
|
|
257
|
+
|| !exactText(policy.reviewer?.reasoningEffort)
|
|
258
|
+
) {
|
|
259
|
+
return failResult('orchestration-evidence', 'Run model policy requires exact executor and reviewer model and effort');
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
!run.modelPolicySource
|
|
263
|
+
|| !['explicit', 'project-config'].includes(run.modelPolicySource.kind)
|
|
264
|
+
|| !exactText(run.modelPolicySource.client)
|
|
265
|
+
|| !exactText(run.modelPolicySource.resolvedAt)
|
|
266
|
+
|| !Array.isArray(run.recoveryHistory)
|
|
267
|
+
) {
|
|
268
|
+
return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
|
|
269
|
+
}
|
|
270
|
+
for (const recovery of run.recoveryHistory) {
|
|
271
|
+
if (
|
|
272
|
+
recovery.code !== 'MODEL_POLICY_SUPPLEMENTED'
|
|
273
|
+
|| recovery.previousSchemaVersion !== 1
|
|
274
|
+
|| recovery.receiptCount !== 0
|
|
275
|
+
|| recovery.pendingDelegation !== false
|
|
276
|
+
|| !exactText(recovery.recoveredAt)
|
|
277
|
+
|| !recovery.policySource
|
|
278
|
+
) {
|
|
279
|
+
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!Array.isArray(run.receipts)) {
|
|
283
|
+
return failResult('orchestration-evidence', 'Run receipts must be an array');
|
|
284
|
+
}
|
|
285
|
+
if (run.completionEvidence != null) {
|
|
286
|
+
const evidenceError = validateCleanCompletionEvidence(run);
|
|
287
|
+
if (evidenceError) return failResult('orchestration-evidence', evidenceError);
|
|
288
|
+
} else if (run.status === 'completed' && (
|
|
289
|
+
run.receipts.length === 0
|
|
290
|
+
|| run.receipts.at(-1)?.stage !== 'commit'
|
|
291
|
+
|| run.receipts.at(-1)?.status !== 'consumed'
|
|
292
|
+
)) {
|
|
293
|
+
return failResult('orchestration-evidence', 'Completed run requires a final consumed commit receipt');
|
|
294
|
+
}
|
|
295
|
+
if (run.pendingDelegation && !['prepared', 'activated', 'stage-completed'].includes(run.pendingDelegation.status)) {
|
|
296
|
+
return failResult('orchestration-evidence', `Pending receipt has unsafe status '${run.pendingDelegation.status}'`);
|
|
297
|
+
}
|
|
298
|
+
const receipts = [...run.receipts, ...(run.pendingDelegation ? [run.pendingDelegation] : [])];
|
|
299
|
+
for (const receipt of receipts) {
|
|
300
|
+
const expectedPolicy = policy[receipt.role];
|
|
301
|
+
if (!expectedPolicy || receipt.requestedModel !== expectedPolicy.model) {
|
|
302
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' requested model does not match its role policy`);
|
|
303
|
+
}
|
|
304
|
+
if (receipt.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
305
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' requested reasoning effort does not match its role policy`);
|
|
306
|
+
}
|
|
307
|
+
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
|
|
308
|
+
if (activated) {
|
|
309
|
+
if (!exactText(receipt.actualModel)) {
|
|
310
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
|
|
311
|
+
}
|
|
312
|
+
if (!exactText(receipt.actualReasoningEffort)) {
|
|
313
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
|
|
314
|
+
}
|
|
315
|
+
if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId || receipt.spawnMode !== 'fresh') {
|
|
316
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
|
|
317
|
+
}
|
|
318
|
+
if (receipt.actualModel !== receipt.requestedModel && !exactText(receipt.modelFallbackReason)) {
|
|
319
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
|
|
320
|
+
}
|
|
321
|
+
if (receipt.actualModel === receipt.requestedModel && receipt.modelFallbackReason !== null) {
|
|
322
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
|
|
323
|
+
}
|
|
324
|
+
if (
|
|
325
|
+
receipt.actualReasoningEffort !== receipt.requestedReasoningEffort
|
|
326
|
+
&& !exactText(receipt.reasoningEffortFallbackReason)
|
|
327
|
+
) {
|
|
328
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
|
|
329
|
+
}
|
|
330
|
+
if (
|
|
331
|
+
receipt.actualReasoningEffort === receipt.requestedReasoningEffort
|
|
332
|
+
&& receipt.reasoningEffortFallbackReason !== null
|
|
333
|
+
) {
|
|
334
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (run.receipts.some((receipt: any) => receipt.status !== 'consumed')) {
|
|
339
|
+
return failResult('orchestration-evidence', 'Historical receipts must be consumed');
|
|
340
|
+
}
|
|
341
|
+
return passResult('orchestration-evidence', 'Persisted orchestration model and delegation evidence is internally consistent');
|
|
342
|
+
}
|
|
343
|
+
|
|
166
344
|
// === Check Functions ===
|
|
167
345
|
|
|
168
346
|
function checkTaskMeta({ taskDir, config }: any): any {
|
|
@@ -740,6 +918,75 @@ function checkReviewLedger({ taskDir, config }: any): any {
|
|
|
740
918
|
return passResult("review-ledger", `Disagreement ledger clean (${inScopeCount} in-scope entries terminal${scopeLabel})`);
|
|
741
919
|
}
|
|
742
920
|
|
|
921
|
+
function checkManualValidation({ taskDir }: any): any {
|
|
922
|
+
// 1. Latest review-code artifact; none -> pass (no review, check not applicable).
|
|
923
|
+
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
924
|
+
if (!review.ok) {
|
|
925
|
+
return passResult("manual-validation", "No review-code artifact; manual validation not applicable");
|
|
926
|
+
}
|
|
927
|
+
// 2. Parse the numeric manual-validation count; unparseable/null -> fail closed
|
|
928
|
+
// (review-code.completed already enforces a numeric count on the normal path).
|
|
929
|
+
const parsed = parseReviewSummary(fs.readFileSync(review.path!, "utf8"));
|
|
930
|
+
if (!parsed.ok || parsed.summary.manualValidation === null) {
|
|
931
|
+
return failResult("manual-validation", "Latest review-code has no numeric manual-validation count");
|
|
932
|
+
}
|
|
933
|
+
// 3. Count 0 -> pass (no pending items to cover).
|
|
934
|
+
if (parsed.summary.manualValidation === 0) {
|
|
935
|
+
return passResult("manual-validation", "No pending manual validation items");
|
|
936
|
+
}
|
|
937
|
+
// 4. Highest-round manual-validation artifact; none -> fail (items pending,
|
|
938
|
+
// run complete-manual-validation first). A falsy artifactFile goes through
|
|
939
|
+
// pattern parsing; resolveArtifactPath only returns { ok, path }.
|
|
940
|
+
const resolved = resolveArtifactPath(taskDir, "manual-validation.md|manual-validation-r{N}.md", "");
|
|
941
|
+
if (!resolved.ok) {
|
|
942
|
+
return failResult(
|
|
943
|
+
"manual-validation",
|
|
944
|
+
`${parsed.summary.manualValidation} manual validation item(s) pending: run /complete-manual-validation`
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
// 5. The Activity Log must record the matching completion entry; the file name
|
|
948
|
+
// is derived with path.basename (resolveArtifactPath does not return fileName).
|
|
949
|
+
const artifactName = path.basename(resolved.path);
|
|
950
|
+
const task = loadTask(taskDir);
|
|
951
|
+
if (!task.ok) return failResult("manual-validation", task.message);
|
|
952
|
+
const log = getSectionContent(task.content, ["活动日志", "Activity Log"]);
|
|
953
|
+
const completed = new RegExp(
|
|
954
|
+
`\\*\\*Complete Manual Validation\\*\\*[\\s\\S]*?Manual validation passed → ${escapeRegExp(artifactName)};`
|
|
955
|
+
);
|
|
956
|
+
if (!completed.test(log)) {
|
|
957
|
+
return failResult(
|
|
958
|
+
"manual-validation",
|
|
959
|
+
`Manual validation artifact exists but completion is not recorded for ${artifactName}: re-run /complete-manual-validation`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
// 6. Timing correlation (PL-3 fix, PL-4 corrected): the completion entry must
|
|
963
|
+
// sit AFTER the latest review-code round's completion entry in the
|
|
964
|
+
// append-only Activity Log. indexOf is a literal search, so the artifactName
|
|
965
|
+
// must NOT be passed through escapeRegExp here (that escapes '.' to '\.',
|
|
966
|
+
// which indexOf never matches). Not reusing completed.exec().index: exec
|
|
967
|
+
// anchors to the first '**Complete Manual Validation**' heading, which can
|
|
968
|
+
// belong to an earlier round's entry and misorder the standard path.
|
|
969
|
+
const reviewDoneIndex = log.indexOf(`**Review Code (Round ${review.round})** by `);
|
|
970
|
+
const completedIndex = log.indexOf(`Manual validation passed → ${artifactName};`);
|
|
971
|
+
if (reviewDoneIndex === -1) {
|
|
972
|
+
// The latest review-code round's completion entry is missing entirely (abnormal
|
|
973
|
+
// state, e.g. a restored/historical task without a review-code.completed record).
|
|
974
|
+
// Re-running complete-manual-validation cannot restore it, so the remediation
|
|
975
|
+
// targets the review record itself. Fail closed either way.
|
|
976
|
+
return failResult(
|
|
977
|
+
"manual-validation",
|
|
978
|
+
`Latest review-code (round ${review.round}) completion entry is missing from the Activity Log (abnormal state): re-run review-code or restore the completion record`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
if (completedIndex < reviewDoneIndex) {
|
|
982
|
+
return failResult(
|
|
983
|
+
"manual-validation",
|
|
984
|
+
`Latest review-code (round ${review.round}) came after the manual validation completion recorded for ${artifactName}: re-run /complete-manual-validation to cover new pending items`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
return passResult("manual-validation", `Manual validation completed → ${artifactName}`);
|
|
988
|
+
}
|
|
989
|
+
|
|
743
990
|
function checkPostReviewCommit({ taskDir, config }: any): any {
|
|
744
991
|
const reviewArtifact = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
745
992
|
if (!reviewArtifact.ok) {
|