@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
|
@@ -4,8 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
6
6
|
import { validateCurrentTaskContract } from './current-contract.ts';
|
|
7
|
-
import { parseReviewSummary
|
|
8
|
-
import { inspectArtifactDirectory } from './artifact-lifecycle.ts';
|
|
7
|
+
import { parseReviewSummary } from './review-artifacts.ts';
|
|
9
8
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
10
9
|
import type { LedgerDocument } from './ledger.ts';
|
|
11
10
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
@@ -53,6 +52,11 @@ import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex
|
|
|
53
52
|
import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
54
53
|
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.ts';
|
|
55
54
|
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.ts';
|
|
55
|
+
import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.ts';
|
|
56
|
+
import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
57
|
+
import { invalidationBlocks } from './invalidation.ts';
|
|
58
|
+
import type { InvalidationDocument } from './invalidation.ts';
|
|
59
|
+
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
56
60
|
|
|
57
61
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
58
62
|
type ModelPolicySource = Readonly<{
|
|
@@ -108,7 +112,7 @@ type OrchestrationRun = Readonly<{
|
|
|
108
112
|
updatedAt: string;
|
|
109
113
|
}>;
|
|
110
114
|
type OrchestrationNext = Readonly<{
|
|
111
|
-
action: 'analyze-task' | 'review-analysis' | 'plan-task' | 'review-plan' | 'code-task' | 'review-code'
|
|
115
|
+
action: 'analyze-task' | 'review-analysis' | 'plan-task' | 'review-plan' | 'code-task' | 'review-code';
|
|
112
116
|
role: DelegationRole;
|
|
113
117
|
stage: DelegationStage;
|
|
114
118
|
round: number;
|
|
@@ -561,78 +565,19 @@ function validateSavedCurrentDiffBase(
|
|
|
561
565
|
return null;
|
|
562
566
|
}
|
|
563
567
|
|
|
564
|
-
function
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
try { content = fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8'); }
|
|
578
|
-
catch (error) { return { approved: false, error: String(error) }; }
|
|
579
|
-
const parsed = parseReviewSummary(content);
|
|
580
|
-
if (!parsed.ok) return { approved: false, error: `${parsed.code}: ${parsed.message}` };
|
|
581
|
-
const verdict = resolveCanonicalVerdict(parsed.summary);
|
|
582
|
-
if (!verdict.ok) return { approved: false, error: `${verdict.code}: ${verdict.message}` };
|
|
583
|
-
return { approved: verdict.verdict === 'Approved', error: null };
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { error: string } | null {
|
|
587
|
-
const analysisRound = highestRound(taskDir, 'analysis');
|
|
588
|
-
const analysisReviewRound = highestRound(taskDir, 'review-analysis');
|
|
589
|
-
const planRound = highestRound(taskDir, 'plan');
|
|
590
|
-
const planReviewRound = highestRound(taskDir, 'review-plan');
|
|
591
|
-
const codeRound = highestRound(taskDir, 'code');
|
|
592
|
-
const codeReviewRound = highestRound(taskDir, 'review-code');
|
|
593
|
-
if (analysisRound === 0) {
|
|
594
|
-
const round = analysisRound + 1;
|
|
595
|
-
return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
|
|
596
|
-
}
|
|
597
|
-
if (!hasReviewAfterArtifact(taskDir, 'analysis', 'review-analysis')) {
|
|
598
|
-
const round = analysisReviewRound + 1;
|
|
599
|
-
return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
|
|
600
|
-
}
|
|
601
|
-
const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
|
|
602
|
-
if (analysisVerdict.error) return { error: analysisVerdict.error };
|
|
603
|
-
if (!analysisVerdict.approved) {
|
|
604
|
-
const round = analysisRound + 1;
|
|
605
|
-
return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
|
|
606
|
-
}
|
|
607
|
-
if (planRound === 0) {
|
|
608
|
-
const round = planRound + 1;
|
|
609
|
-
return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
|
|
610
|
-
}
|
|
611
|
-
if (!hasReviewAfterArtifact(taskDir, 'plan', 'review-plan')) {
|
|
612
|
-
const round = planReviewRound + 1;
|
|
613
|
-
return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
|
|
614
|
-
}
|
|
615
|
-
const planVerdict = latestReviewApproved(taskDir, 'review-plan');
|
|
616
|
-
if (planVerdict.error) return { error: planVerdict.error };
|
|
617
|
-
if (!planVerdict.approved) {
|
|
618
|
-
const round = planRound + 1;
|
|
619
|
-
return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
|
|
620
|
-
}
|
|
621
|
-
if (codeRound === 0) {
|
|
622
|
-
const round = codeRound + 1;
|
|
623
|
-
return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
|
|
624
|
-
}
|
|
625
|
-
if (!hasReviewAfterArtifact(taskDir, 'code', 'review-code')) {
|
|
626
|
-
const round = codeReviewRound + 1;
|
|
627
|
-
return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
|
|
628
|
-
}
|
|
629
|
-
const codeVerdict = latestReviewApproved(taskDir, 'review-code');
|
|
630
|
-
if (codeVerdict.error) return { error: codeVerdict.error };
|
|
631
|
-
if (!codeVerdict.approved) {
|
|
632
|
-
const round = codeRound + 1;
|
|
633
|
-
return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
|
|
634
|
-
}
|
|
635
|
-
return { action: 'commit', role: 'executor', stage: 'commit', round: 1, artifact: 'commit' };
|
|
568
|
+
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
|
|
569
|
+
const recommendation = recommendNext(facts);
|
|
570
|
+
if (!recommendation.action) return { completion: true };
|
|
571
|
+
const action = recommendation.action;
|
|
572
|
+
if (action === 'manual-validation' || action === 'validation-run') return null;
|
|
573
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
574
|
+
const round = Math.max(0, ...[...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])].map((name) => {
|
|
575
|
+
const match = /-r(\d+)\.md$/.exec(name);
|
|
576
|
+
return match ? Number(match[1]) : 1;
|
|
577
|
+
})) + 1;
|
|
578
|
+
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
579
|
+
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
580
|
+
return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
|
|
636
581
|
}
|
|
637
582
|
|
|
638
583
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -652,11 +597,13 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
652
597
|
}
|
|
653
598
|
let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
654
599
|
let currentLedger: LedgerDocument | null = null;
|
|
600
|
+
let currentInvalidation: InvalidationDocument = { operations: [], targets: [] };
|
|
655
601
|
if (resolved.state === 'active') {
|
|
656
602
|
const contract = validateCurrentTaskContract(content);
|
|
657
603
|
if (!contract.ok) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
|
|
658
604
|
metadata = contract.metadata;
|
|
659
605
|
currentLedger = contract.ledger;
|
|
606
|
+
currentInvalidation = contract.invalidation;
|
|
660
607
|
} else {
|
|
661
608
|
try {
|
|
662
609
|
metadata = parseTypedTaskFrontmatter(content);
|
|
@@ -664,17 +611,25 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
664
611
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
665
612
|
}
|
|
666
613
|
}
|
|
667
|
-
|
|
668
|
-
|
|
614
|
+
if (invalidationBlocks(currentInvalidation)) {
|
|
615
|
+
return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
|
|
616
|
+
}
|
|
617
|
+
let run: OrchestrationRun | null;
|
|
618
|
+
try {
|
|
619
|
+
run = readRun(resolved.taskDir);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
622
|
+
}
|
|
623
|
+
const facts = buildLifecycleFacts(
|
|
624
|
+
resolved.taskDir,
|
|
625
|
+
content,
|
|
626
|
+
resolved.state,
|
|
627
|
+
hasOpenLifecycleExecution(content) || Boolean(run?.pendingDelegation)
|
|
628
|
+
);
|
|
629
|
+
if (!facts.ok) return failed('ORCHESTRATION_CAPABILITY_FACTS_INVALID', facts.message, resolved.taskId);
|
|
630
|
+
const routed = routeFromFacts(facts.facts);
|
|
669
631
|
if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
670
|
-
|
|
671
|
-
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
672
|
-
const next: OrchestrationNext = {
|
|
673
|
-
...routed,
|
|
674
|
-
requestedModel: policy?.model ?? null,
|
|
675
|
-
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
676
|
-
};
|
|
677
|
-
if (next.stage === 'commit') {
|
|
632
|
+
if ('completion' in routed) {
|
|
678
633
|
const reviewRound = highestRound(resolved.taskDir, 'review-code');
|
|
679
634
|
const review = parseReviewSummary(fs.readFileSync(
|
|
680
635
|
path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
|
|
@@ -756,6 +711,18 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
756
711
|
saveRun(resolved.taskDir, completed);
|
|
757
712
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
758
713
|
}
|
|
714
|
+
const action = routed.stage as LifecycleAction;
|
|
715
|
+
const capability = canStart(action, facts.facts, {
|
|
716
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${routed.round}`,
|
|
717
|
+
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
718
|
+
});
|
|
719
|
+
if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
|
|
720
|
+
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
721
|
+
const next: OrchestrationNext = {
|
|
722
|
+
...routed,
|
|
723
|
+
requestedModel: policy?.model ?? null,
|
|
724
|
+
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
725
|
+
};
|
|
759
726
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
760
727
|
}
|
|
761
728
|
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
import type { ResourceIdentity } from '../platform/resource-identity.ts';
|
|
2
|
+
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
|
|
3
|
+
import {
|
|
4
|
+
isResourceIdentity,
|
|
5
|
+
parseResourceIdentity,
|
|
6
|
+
resourceIdentityNumber,
|
|
7
|
+
serializeResourceIdentity
|
|
8
|
+
} from '../platform/resource-identity.ts';
|
|
9
|
+
|
|
1
10
|
const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
|
|
2
11
|
|
|
3
12
|
type PrDeliveryIdentity = {
|
|
13
|
+
resource: ResourceIdentity;
|
|
14
|
+
repository: string;
|
|
15
|
+
url: string;
|
|
16
|
+
head: { repository: string; ref: string; sha: string };
|
|
17
|
+
base: { repository: string; ref: string; sha: string };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type LegacyPrDeliveryIdentity = {
|
|
4
21
|
repository: string;
|
|
5
22
|
number: number;
|
|
6
23
|
nodeId: string;
|
|
@@ -8,33 +25,30 @@ type PrDeliveryIdentity = {
|
|
|
8
25
|
head: { repository: string; ref: string; sha: string };
|
|
9
26
|
base: { repository: string; ref: string; sha: string };
|
|
10
27
|
};
|
|
28
|
+
type CurrentPrDeliveryIdentityInput = {
|
|
29
|
+
resource: ResourceIdentity;
|
|
30
|
+
repository: string;
|
|
31
|
+
url: string;
|
|
32
|
+
head: { repository: string; ref: string; sha: string };
|
|
33
|
+
base: { repository: string; ref: string; sha: string };
|
|
34
|
+
};
|
|
35
|
+
type PrDeliveryIdentityInput = CurrentPrDeliveryIdentityInput;
|
|
11
36
|
|
|
12
|
-
type PrDeliveryBindingSource =
|
|
13
|
-
|
|
14
|
-
| 'reused'
|
|
15
|
-
| 'explicit-bind'
|
|
16
|
-
| 'external-unique'
|
|
17
|
-
| 'external-explicit';
|
|
18
|
-
|
|
19
|
-
type PrDeliveryProvenance =
|
|
20
|
-
| 'create-post'
|
|
21
|
-
| 'reuse'
|
|
22
|
-
| 'explicit-bind'
|
|
23
|
-
| 'external-unique'
|
|
24
|
-
| 'external-explicit';
|
|
37
|
+
type PrDeliveryBindingSource = 'created' | 'reused' | 'explicit-bind' | 'external-unique' | 'external-explicit';
|
|
38
|
+
type PrDeliveryProvenance = 'create-post' | 'reuse' | 'explicit-bind' | 'external-unique' | 'external-explicit';
|
|
25
39
|
|
|
26
40
|
type PrDeliveryFact =
|
|
27
|
-
| { version:
|
|
28
|
-
| { version:
|
|
41
|
+
| { version: 2; state: 'unbound'; reason: 'initial' }
|
|
42
|
+
| { version: 2; state: 'skipped'; reason: 'explicit'; decidedAt: string }
|
|
29
43
|
| {
|
|
30
|
-
version:
|
|
44
|
+
version: 2;
|
|
31
45
|
state: 'bound';
|
|
32
46
|
identity: PrDeliveryIdentity;
|
|
33
47
|
binding: {
|
|
34
48
|
status: 'verified';
|
|
35
49
|
source: PrDeliveryBindingSource;
|
|
36
50
|
verifiedAt: string;
|
|
37
|
-
|
|
51
|
+
issueIdentity: ResourceIdentity | null;
|
|
38
52
|
remoteState: 'open' | 'closed';
|
|
39
53
|
mergedAt: string | null;
|
|
40
54
|
mergeCommitSha: string | null;
|
|
@@ -50,205 +64,174 @@ type CreationOutcome =
|
|
|
50
64
|
| { kind: 'no-op'; createdByCurrentOperation: false }
|
|
51
65
|
| { kind: 'unknown'; errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' };
|
|
52
66
|
|
|
67
|
+
type PrDeliveryFactError = Error & { code: string };
|
|
53
68
|
type PrDeliveryFactReadResult =
|
|
54
69
|
| { status: 'valid'; fact: PrDeliveryFact }
|
|
55
70
|
| { status: 'missing'; fact: null }
|
|
56
|
-
| { status: 'invalid'; fact: null; error:
|
|
71
|
+
| { status: 'invalid'; fact: null; error: PrDeliveryFactError };
|
|
57
72
|
|
|
58
73
|
function factError(message: string): Error & { code: string } {
|
|
59
74
|
return Object.assign(new Error(`PR_DELIVERY_FACT_INVALID: ${message}`), { code: 'PR_DELIVERY_FACT_INVALID' });
|
|
60
75
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
76
|
+
function asFactError(error: unknown): PrDeliveryFactError {
|
|
77
|
+
if (error instanceof Error && typeof (error as { code?: unknown }).code === 'string') return error as PrDeliveryFactError;
|
|
78
|
+
return factError(error instanceof Error ? error.message : String(error));
|
|
64
79
|
}
|
|
65
|
-
|
|
80
|
+
function isRecord(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
|
|
66
81
|
function exactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
|
|
67
82
|
const actual = Object.keys(value).sort();
|
|
68
83
|
const expected = [...keys].sort();
|
|
69
|
-
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
70
|
-
throw factError(`${label} has an invalid field set`);
|
|
71
|
-
}
|
|
84
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw factError(`${label} has an invalid field set`);
|
|
72
85
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (typeof value !== 'string' || !value || value.trim() !== value) throw factError(`${label} must be a non-empty string`);
|
|
86
|
+
function text(value: unknown, label: string, allowEmpty = false): string {
|
|
87
|
+
if (typeof value !== 'string' || (!allowEmpty && !value) || value.trim() !== value) throw factError(`${label} must be a string`);
|
|
76
88
|
return value;
|
|
77
89
|
}
|
|
78
|
-
|
|
79
|
-
function
|
|
80
|
-
|
|
90
|
+
function nullableText(value: unknown, label: string): string | null { return value === null ? null : text(value, label); }
|
|
91
|
+
function timestamp(value: unknown, label: string): string {
|
|
92
|
+
const result = text(value, label);
|
|
93
|
+
if (Number.isNaN(Date.parse(result))) throw factError(`${label} must be a valid timestamp`);
|
|
94
|
+
return new Date(result).toISOString();
|
|
81
95
|
}
|
|
82
|
-
|
|
83
96
|
function positiveNumber(value: unknown, label: string): number {
|
|
84
97
|
if (!Number.isSafeInteger(value) || (value as number) <= 0) throw factError(`${label} must be a positive integer`);
|
|
85
98
|
return value as number;
|
|
86
99
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return result;
|
|
100
|
+
function parseRef(value: unknown, label: string): { repository: string; ref: string; sha: string } {
|
|
101
|
+
if (!isRecord(value)) throw factError(`${label} must be an object`);
|
|
102
|
+
exactKeys(value, ['repository', 'ref', 'sha'], label);
|
|
103
|
+
return { repository: text(value.repository, `${label}.repository`), ref: text(value.ref, `${label}.ref`), sha: text(value.sha, `${label}.sha`) };
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
function
|
|
106
|
+
function canonicalIdentity(value: unknown): PrDeliveryIdentity {
|
|
95
107
|
if (!isRecord(value)) throw factError('identity must be an object');
|
|
96
|
-
exactKeys(value, ['
|
|
97
|
-
const head = parseRef(value.head, 'head');
|
|
98
|
-
const base = parseRef(value.base, 'base');
|
|
108
|
+
exactKeys(value, ['resource', 'repository', 'url', 'head', 'base'], 'identity');
|
|
99
109
|
return {
|
|
110
|
+
resource: parseResourceIdentity(value.resource, 'identity.resource'),
|
|
100
111
|
repository: text(value.repository, 'identity.repository'),
|
|
101
|
-
number: positiveNumber(value.number, 'identity.number'),
|
|
102
|
-
nodeId: text(value.nodeId, 'identity.nodeId'),
|
|
103
112
|
url: text(value.url, 'identity.url'),
|
|
104
|
-
head,
|
|
105
|
-
base
|
|
113
|
+
head: parseRef(value.head, 'identity.head'),
|
|
114
|
+
base: parseRef(value.base, 'identity.base')
|
|
106
115
|
};
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
function
|
|
110
|
-
if (!isRecord(value)) throw factError(
|
|
111
|
-
|
|
118
|
+
function decodeLegacyIdentity(value: unknown): PrDeliveryIdentity {
|
|
119
|
+
if (!isRecord(value)) throw factError('legacy identity must be an object');
|
|
120
|
+
// TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
|
|
121
|
+
exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
|
|
122
|
+
const legacy = {
|
|
123
|
+
number: positiveNumber(value.number, 'legacy identity.number'),
|
|
124
|
+
repository: text(value.repository, 'identity.repository'),
|
|
125
|
+
nodeId: text(value.nodeId, 'legacy identity.nodeId'),
|
|
126
|
+
url: text(value.url, 'identity.url'),
|
|
127
|
+
head: parseRef(value.head, 'identity.head'),
|
|
128
|
+
base: parseRef(value.base, 'identity.base')
|
|
129
|
+
} satisfies LegacyPrDeliveryIdentity;
|
|
112
130
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
resource: { kind: 'number', value: legacy.number },
|
|
132
|
+
repository: legacy.repository,
|
|
133
|
+
url: legacy.url,
|
|
134
|
+
head: legacy.head,
|
|
135
|
+
base: legacy.base
|
|
116
136
|
};
|
|
117
137
|
}
|
|
118
138
|
|
|
119
|
-
function
|
|
139
|
+
function provenanceForSource(source: PrDeliveryBindingSource): PrDeliveryProvenance { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
|
|
140
|
+
|
|
141
|
+
function parseFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
|
|
120
142
|
if (!isRecord(value)) throw factError('fact must be an object');
|
|
121
|
-
if (value.version !== 1 || typeof value.state !== 'string') throw factError('version or state is invalid');
|
|
143
|
+
if ((value.version !== 1 && value.version !== 2) || typeof value.state !== 'string') throw factError('version or state is invalid');
|
|
144
|
+
if (value.version === 1 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
145
|
+
throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
|
|
146
|
+
}
|
|
122
147
|
if (value.state === 'unbound') {
|
|
123
148
|
exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
|
|
124
149
|
if (value.reason !== 'initial') throw factError('unbound reason is invalid');
|
|
125
|
-
return { version:
|
|
150
|
+
return { version: 2, state: 'unbound', reason: 'initial' };
|
|
126
151
|
}
|
|
127
152
|
if (value.state === 'skipped') {
|
|
128
153
|
exactKeys(value, ['version', 'state', 'reason', 'decidedAt'], 'skipped fact');
|
|
129
154
|
if (value.reason !== 'explicit') throw factError('skipped reason is invalid');
|
|
130
|
-
|
|
131
|
-
if (Number.isNaN(Date.parse(decidedAt))) throw factError('skipped.decidedAt must be a valid timestamp');
|
|
132
|
-
return { version: 1, state: 'skipped', reason: 'explicit', decidedAt };
|
|
155
|
+
return { version: 2, state: 'skipped', reason: 'explicit', decidedAt: timestamp(value.decidedAt, 'skipped.decidedAt') };
|
|
133
156
|
}
|
|
134
157
|
if (value.state !== 'bound') throw factError('state is invalid');
|
|
158
|
+
if (value.version === 1) {
|
|
159
|
+
// TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
|
|
160
|
+
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
|
|
161
|
+
if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('legacy binding or provenance is invalid');
|
|
162
|
+
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
|
|
163
|
+
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
|
|
164
|
+
return parseFact({
|
|
165
|
+
version: 2,
|
|
166
|
+
state: 'bound',
|
|
167
|
+
identity: decodeLegacyIdentity(value.identity),
|
|
168
|
+
binding: {
|
|
169
|
+
status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
|
|
170
|
+
issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
|
|
171
|
+
mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
|
|
172
|
+
},
|
|
173
|
+
provenance: value.provenance
|
|
174
|
+
}, runtimeVersion);
|
|
175
|
+
}
|
|
135
176
|
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
|
|
136
|
-
if (!isRecord(value.binding)) throw factError('binding
|
|
137
|
-
exactKeys(value.binding, ['status', 'source', 'verifiedAt', '
|
|
177
|
+
if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('binding or provenance is invalid');
|
|
178
|
+
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueIdentity', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
|
|
138
179
|
if (value.binding.status !== 'verified') throw factError('binding status is invalid');
|
|
139
180
|
const sources: readonly PrDeliveryBindingSource[] = ['created', 'reused', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
140
181
|
if (!sources.includes(value.binding.source as PrDeliveryBindingSource)) throw factError('binding source is invalid');
|
|
141
|
-
const
|
|
142
|
-
if (Number.isNaN(Date.parse(verifiedAt))) throw factError('binding.verifiedAt must be a valid timestamp');
|
|
143
|
-
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'binding.issueNumber');
|
|
182
|
+
const issueIdentity = value.binding.issueIdentity === null ? null : parseResourceIdentity(value.binding.issueIdentity, 'binding.issueIdentity');
|
|
144
183
|
if (value.binding.remoteState !== 'open' && value.binding.remoteState !== 'closed') throw factError('binding.remoteState is invalid');
|
|
145
|
-
const mergedAt =
|
|
184
|
+
const mergedAt = value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt');
|
|
146
185
|
const mergeCommitSha = nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha');
|
|
147
186
|
if (Boolean(mergedAt) !== Boolean(mergeCommitSha)) throw factError('mergedAt and mergeCommitSha must be paired');
|
|
148
|
-
if (!isRecord(value.provenance)) throw factError('provenance must be an object');
|
|
149
187
|
exactKeys(value.provenance, ['establishedBy'], 'provenance');
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
if (value.provenance.establishedBy !== provenanceForSource(value.binding.source as PrDeliveryBindingSource))
|
|
153
|
-
throw factError('binding.source and provenance.establishedBy do not match');
|
|
154
|
-
}
|
|
188
|
+
const provenances: readonly PrDeliveryProvenance[] = ['create-post', 'reuse', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
189
|
+
if (!provenances.includes(value.provenance.establishedBy as PrDeliveryProvenance)) throw factError('provenance.establishedBy is invalid');
|
|
190
|
+
if (value.provenance.establishedBy !== provenanceForSource(value.binding.source as PrDeliveryBindingSource)) throw factError('binding.source and provenance.establishedBy do not match');
|
|
155
191
|
return {
|
|
156
|
-
version:
|
|
157
|
-
|
|
158
|
-
identity: parseIdentity(value.identity),
|
|
159
|
-
binding: {
|
|
160
|
-
status: 'verified',
|
|
161
|
-
source: value.binding.source as PrDeliveryBindingSource,
|
|
162
|
-
verifiedAt,
|
|
163
|
-
issueNumber,
|
|
164
|
-
remoteState: value.binding.remoteState,
|
|
165
|
-
mergedAt,
|
|
166
|
-
mergeCommitSha
|
|
167
|
-
},
|
|
192
|
+
version: 2, state: 'bound', identity: canonicalIdentity(value.identity),
|
|
193
|
+
binding: { status: 'verified', source: value.binding.source as PrDeliveryBindingSource, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'), issueIdentity, remoteState: value.binding.remoteState, mergedAt, mergeCommitSha },
|
|
168
194
|
provenance: { establishedBy: value.provenance.establishedBy as PrDeliveryProvenance }
|
|
169
195
|
};
|
|
170
196
|
}
|
|
171
197
|
|
|
172
|
-
function decodePrDeliveryFact(value: unknown): PrDeliveryFact {
|
|
198
|
+
function decodePrDeliveryFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
|
|
173
199
|
if (typeof value === 'string') {
|
|
174
|
-
|
|
175
|
-
|
|
200
|
+
let decoded: unknown;
|
|
201
|
+
try { decoded = JSON.parse(value); }
|
|
202
|
+
catch { throw factError('value is not valid JSON'); }
|
|
203
|
+
return parseFact(decoded, runtimeVersion);
|
|
176
204
|
}
|
|
177
|
-
return parseFact(value);
|
|
205
|
+
return parseFact(value, runtimeVersion);
|
|
178
206
|
}
|
|
179
|
-
|
|
180
|
-
function
|
|
181
|
-
return JSON.stringify(parseFact(fact));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function readPrDeliveryFact(metadata: Record<string, unknown>): PrDeliveryFactReadResult {
|
|
207
|
+
function encodePrDeliveryFact(fact: PrDeliveryFact): string { return JSON.stringify(parseFact(fact)); }
|
|
208
|
+
function readPrDeliveryFact(metadata: Record<string, unknown>, runtimeVersion = VERSION): PrDeliveryFactReadResult {
|
|
185
209
|
if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '') return { status: 'missing', fact: null };
|
|
186
|
-
try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) }; }
|
|
187
|
-
catch (error) { return { status: 'invalid', fact: null, error:
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function buildUnboundFact(): PrDeliveryFact {
|
|
191
|
-
return { version: 1, state: 'unbound', reason: 'initial' };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function buildSkippedFact(decidedAt: string): PrDeliveryFact {
|
|
195
|
-
return parseFact({ version: 1, state: 'skipped', reason: 'explicit', decidedAt });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function provenanceForSource(source: PrDeliveryBindingSource): PrDeliveryProvenance {
|
|
199
|
-
return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source;
|
|
210
|
+
try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY], runtimeVersion) }; }
|
|
211
|
+
catch (error) { return { status: 'invalid', fact: null, error: asFactError(error) }; }
|
|
200
212
|
}
|
|
213
|
+
function buildUnboundFact(): PrDeliveryFact { return { version: 2, state: 'unbound', reason: 'initial' }; }
|
|
214
|
+
function buildSkippedFact(decidedAt: string): PrDeliveryFact { return parseFact({ version: 2, state: 'skipped', reason: 'explicit', decidedAt }); }
|
|
201
215
|
|
|
202
216
|
function buildBoundFact(input: {
|
|
203
|
-
identity:
|
|
217
|
+
identity: CurrentPrDeliveryIdentityInput;
|
|
204
218
|
source: PrDeliveryBindingSource;
|
|
205
219
|
verifiedAt: string;
|
|
206
|
-
|
|
220
|
+
issueIdentity?: ResourceIdentity | null;
|
|
207
221
|
remoteState: 'open' | 'closed';
|
|
208
222
|
mergedAt?: string | null;
|
|
209
223
|
mergeCommitSha?: string | null;
|
|
210
224
|
}): PrDeliveryFact {
|
|
211
|
-
return parseFact({
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
status: 'verified',
|
|
217
|
-
source: input.source,
|
|
218
|
-
verifiedAt: input.verifiedAt,
|
|
219
|
-
issueNumber: input.issueNumber ?? null,
|
|
220
|
-
remoteState: input.remoteState,
|
|
221
|
-
mergedAt: input.mergedAt ?? null,
|
|
222
|
-
mergeCommitSha: input.mergeCommitSha ?? null
|
|
223
|
-
},
|
|
224
|
-
provenance: { establishedBy: provenanceForSource(input.source) }
|
|
225
|
-
});
|
|
225
|
+
return parseFact({ version: 2, state: 'bound', identity: input.identity, binding: {
|
|
226
|
+
status: 'verified', source: input.source, verifiedAt: input.verifiedAt,
|
|
227
|
+
issueIdentity: input.issueIdentity === undefined ? null : input.issueIdentity,
|
|
228
|
+
remoteState: input.remoteState, mergedAt: input.mergedAt ?? null, mergeCommitSha: input.mergeCommitSha ?? null
|
|
229
|
+
}, provenance: { establishedBy: provenanceForSource(input.source) } });
|
|
226
230
|
}
|
|
227
|
-
|
|
228
|
-
function
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function identityFromPullRequest(pullRequest: PrDeliveryIdentity): PrDeliveryIdentity {
|
|
233
|
-
return parseIdentity(pullRequest);
|
|
231
|
+
function factFrontmatterMutation(fact: PrDeliveryFact): { set: { [PR_DELIVERY_FACT_KEY]: string } } { return { set: { [PR_DELIVERY_FACT_KEY]: encodePrDeliveryFact(fact) } }; }
|
|
232
|
+
function identityFromPullRequest(pullRequest: PrDeliveryIdentityInput): PrDeliveryIdentity {
|
|
233
|
+
return canonicalIdentity(pullRequest);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
export {
|
|
237
|
-
|
|
238
|
-
buildBoundFact,
|
|
239
|
-
buildSkippedFact,
|
|
240
|
-
buildUnboundFact,
|
|
241
|
-
decodePrDeliveryFact,
|
|
242
|
-
encodePrDeliveryFact,
|
|
243
|
-
factFrontmatterMutation,
|
|
244
|
-
identityFromPullRequest,
|
|
245
|
-
readPrDeliveryFact
|
|
246
|
-
};
|
|
247
|
-
export type {
|
|
248
|
-
CreationOutcome,
|
|
249
|
-
PrDeliveryBindingSource,
|
|
250
|
-
PrDeliveryFact,
|
|
251
|
-
PrDeliveryFactReadResult,
|
|
252
|
-
PrDeliveryIdentity,
|
|
253
|
-
PrDeliveryProvenance
|
|
254
|
-
};
|
|
236
|
+
export { PR_DELIVERY_FACT_KEY, buildBoundFact, buildSkippedFact, buildUnboundFact, decodePrDeliveryFact, encodePrDeliveryFact, factFrontmatterMutation, identityFromPullRequest, readPrDeliveryFact, serializeResourceIdentity };
|
|
237
|
+
export type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact, PrDeliveryFactError, PrDeliveryFactReadResult, PrDeliveryIdentity, PrDeliveryIdentityInput, PrDeliveryProvenance };
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { artifactName, maxRound } from "./review-artifacts.ts";
|
|
5
|
+
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.ts";
|
|
5
6
|
|
|
6
7
|
type ReviewedGitTree = string & { readonly __reviewedGitTree: unique symbol };
|
|
7
8
|
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
@@ -46,15 +47,35 @@ export function findAuthoritativeReviewCodeArtifact(taskDir: string) {
|
|
|
46
47
|
const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
|
|
47
48
|
const round = maxRound(entries, "review-code");
|
|
48
49
|
if (round === 0) {
|
|
49
|
-
return { ok: false, round: 0, fileName: null, path: null };
|
|
50
|
+
return { ok: false, round: 0, fileName: null, path: null, error: null };
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const fileName = artifactName("review-code", round);
|
|
54
|
+
let taskContent: string;
|
|
55
|
+
try {
|
|
56
|
+
taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false, round, fileName, path: null,
|
|
60
|
+
error: error instanceof Error ? error.message : String(error)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const invalidation = parseInvalidationDocument(taskContent);
|
|
64
|
+
if (!invalidation.ok) {
|
|
65
|
+
return { ok: false, round, fileName, path: null, error: invalidation.message };
|
|
66
|
+
}
|
|
67
|
+
if (invalidationBlocks(invalidation.document)) {
|
|
68
|
+
return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
|
|
69
|
+
}
|
|
70
|
+
if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
|
|
71
|
+
return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
|
|
72
|
+
}
|
|
53
73
|
return {
|
|
54
74
|
ok: true,
|
|
55
75
|
round,
|
|
56
76
|
fileName,
|
|
57
|
-
path: path.join(taskDir, fileName)
|
|
77
|
+
path: path.join(taskDir, fileName),
|
|
78
|
+
error: null
|
|
58
79
|
};
|
|
59
80
|
}
|
|
60
81
|
|