codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14
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 +27 -388
- package/dist/reviewer-runtime-build.json +89 -29
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +5 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +37 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +1 -1
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -0
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +48 -63
- package/dist/src/workflow.js +666 -1460
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -388
- package/docs/updating-existing-project.md +19 -717
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +39 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2438 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +843 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +572 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +660 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10172 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
package/dist/src/workflow.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { assessNavigationDirtyCarryover } from './alpha6/navigation-dirty-carryover.js';
|
|
2
|
+
import { navigationUpdateSource, assertNavigationSourceInstalled, inspectNavigationTransport } from './navigation-update.js';
|
|
3
|
+
import { verifyNavigationUpdateArtifact } from './navigation-update-artifact.js';
|
|
4
|
+
import { taskSyncChecks, taskSyncChecksHash, assertNoPendingBaseSyncValidation, assessPendingBaseSyncValidation, assertPendingBaseSyncValidationReady } from './checks/task-sync.js';
|
|
5
|
+
import { isVirginRegistrationRoot } from './domain/virgin-registration.js';
|
|
6
|
+
import { currentBaseSyncConflict, recordBaseSyncConflict } from './domain/base-sync-conflict.js';
|
|
7
|
+
import { stepStartPhase } from './domain/step-start-admission.js';
|
|
8
|
+
import { correctiveDecisionCheckoutEligible } from './alpha6/corrective-decision-boundary.js';
|
|
9
|
+
import { prepareCapturedCheckEvidence, assertCapturedCheckEvidence, assertCurrentCapturedCheckEvidence } from './alpha6/captured-check-evidence.js';
|
|
1
10
|
import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
|
|
2
11
|
import { explainChange } from './change-explanation.js';
|
|
3
|
-
import {
|
|
12
|
+
import { verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
4
13
|
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
14
|
import { createHash } from 'node:crypto';
|
|
6
15
|
import path from 'node:path';
|
|
7
16
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
8
|
-
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery,
|
|
17
|
+
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
9
18
|
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
10
19
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
11
20
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
@@ -15,19 +24,20 @@ import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './do
|
|
|
15
24
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
16
25
|
import { WorkflowError } from './errors.js';
|
|
17
26
|
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
27
|
+
import { withWorkflowBlockerRoute } from './workflow-blocker-route.js';
|
|
18
28
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
19
|
-
import {
|
|
29
|
+
import { inspectWorkflowVersions } from './diagnostics.js';
|
|
20
30
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
|
|
21
|
-
import { assessDownstreamProofCarryover,
|
|
31
|
+
import { assessDownstreamProofCarryover, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, prepareDownstreamProofInvalidation, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
22
32
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
23
33
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
24
34
|
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
25
35
|
import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
|
|
26
|
-
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision,
|
|
36
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, isKnowledgeReboundSupportDecision, findCurrentGuardedRemediationPosture, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
27
37
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
28
38
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
29
39
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
30
|
-
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
40
|
+
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, assessMilestoneScopeCompletion, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
31
41
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
32
42
|
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
|
|
33
43
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
@@ -38,7 +48,7 @@ import { ensureDirectory } from './fs-utils.js';
|
|
|
38
48
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
39
49
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
40
50
|
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
41
|
-
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
51
|
+
import { CoreTaskOperationLockManager, inspectCoreTaskOperationMutex, listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
42
52
|
import { FileStateStore } from './state/store.js';
|
|
43
53
|
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
44
54
|
import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
|
|
@@ -49,14 +59,14 @@ import { createEntityId } from './ulid.js';
|
|
|
49
59
|
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
50
60
|
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
51
61
|
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
52
|
-
// Release-reviewed exact compatibility target; never infer support from PACKAGE_VERSION.
|
|
53
|
-
const CONSUMED_CARRYOVER_TARGET_VERSION = '2.0.0-beta.13.9';
|
|
54
62
|
export class WorkflowService {
|
|
55
63
|
store;
|
|
64
|
+
checkOptions;
|
|
56
65
|
now;
|
|
57
66
|
transactionHooks;
|
|
58
|
-
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
|
|
67
|
+
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}, checkOptions = {}) {
|
|
59
68
|
this.store = store;
|
|
69
|
+
this.checkOptions = checkOptions;
|
|
60
70
|
this.now = now;
|
|
61
71
|
this.transactionHooks = transactionHooks;
|
|
62
72
|
}
|
|
@@ -91,6 +101,9 @@ export class WorkflowService {
|
|
|
91
101
|
const projectFile = path.join(projectRoot, 'project.json');
|
|
92
102
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
93
103
|
if (!projectAlreadyRegistered) {
|
|
104
|
+
if (!isVirginRegistrationRoot(projectRoot, this.store.root, identity, PACKAGE_VERSION)) {
|
|
105
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Missing registration with retained or unrecognized state requires explicit recovery.');
|
|
106
|
+
}
|
|
94
107
|
// The registration posture is a sidecar under the project root. A completely
|
|
95
108
|
// fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
|
|
96
109
|
// only that directory before writing the posture; project.json remains the
|
|
@@ -118,6 +131,36 @@ export class WorkflowService {
|
|
|
118
131
|
return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
|
|
119
132
|
}
|
|
120
133
|
repairStaleWriterLease(repository, entityId) {
|
|
134
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
135
|
+
const locksRoot = this.store.lockRoot(identity.projectId);
|
|
136
|
+
const core = inspectCoreTaskOperationMutex(locksRoot, entityId, this.store.root);
|
|
137
|
+
if (core) {
|
|
138
|
+
const project = this.store.readProject(identity.projectId);
|
|
139
|
+
if (!project.repositoryRoots.includes(identity.repositoryRoot)) {
|
|
140
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Repository is not registered for Core orphan repair.');
|
|
141
|
+
}
|
|
142
|
+
const taskRoot = this.store.taskRoot(identity.projectId, entityId);
|
|
143
|
+
this.store.readTask(identity.projectId, entityId);
|
|
144
|
+
return new CoreTaskOperationLockManager(locksRoot, this.now, this.store.root).repairExpiredOrphan(entityId, (mutex) => {
|
|
145
|
+
const replan = inspectCorrectiveReplanTaskTransactions(this.store.projectRoot(identity.projectId), this.store.root);
|
|
146
|
+
const yielded = inspectCorrectiveYieldTaskTransactions(this.store.projectRoot(identity.projectId), this.store.root);
|
|
147
|
+
if ([...replan.pending, ...replan.corrupt, ...yielded.pending, ...yielded.corrupt].some(item => item.taskId === entityId)) {
|
|
148
|
+
throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'Pending or corrupt Task journal state requires transaction recovery.');
|
|
149
|
+
}
|
|
150
|
+
for (const name of ['.transactions', '.yield-transactions']) {
|
|
151
|
+
const exactTransaction = path.join(taskRoot, name, mutex.operationId);
|
|
152
|
+
try {
|
|
153
|
+
lstatSync(exactTransaction);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code === 'ENOENT')
|
|
157
|
+
continue;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'The Core operation has retained journal state; use transaction recovery.');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
121
164
|
const context = this.#mutationContext(repository);
|
|
122
165
|
const lease = context.locks.repairStale(entityId);
|
|
123
166
|
return {
|
|
@@ -125,31 +168,6 @@ export class WorkflowService {
|
|
|
125
168
|
repaired: true,
|
|
126
169
|
};
|
|
127
170
|
}
|
|
128
|
-
prepareTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason) {
|
|
129
|
-
const snapshot = this.observationSnapshot(repository);
|
|
130
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
131
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
132
|
-
observation: snapshot.observation,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId) ?? null;
|
|
136
|
-
if (!task)
|
|
137
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
138
|
-
this.assertStopOverrideRepositoryBoundary(snapshot.identity, task);
|
|
139
|
-
return prepareStopEscalateOverride(this.store, task, stepId, expectedRevision, actor, reason, headCommit(snapshot.identity.repositoryRoot));
|
|
140
|
-
}
|
|
141
|
-
applyTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason, confirmationCode) {
|
|
142
|
-
const snapshot = this.observationSnapshot(repository);
|
|
143
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
144
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
145
|
-
observation: snapshot.observation,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
const context = this.#mutationContext(repository);
|
|
149
|
-
const task = context.store.readTask(context.identity.projectId, taskId);
|
|
150
|
-
this.assertStopOverrideRepositoryBoundary(context.identity, task, context.locks.list());
|
|
151
|
-
return appendStopEscalateOverride(context.store, task, stepId, expectedRevision, actor, reason, headCommit(context.identity.repositoryRoot), confirmationCode, this.now());
|
|
152
|
-
}
|
|
153
171
|
prepareCorrectiveReplan(repository, taskId, candidate) {
|
|
154
172
|
return this.correctiveReplanController(repository).prepare({ taskId, candidate });
|
|
155
173
|
}
|
|
@@ -295,92 +313,6 @@ export class WorkflowService {
|
|
|
295
313
|
invalidation: invalidation.event,
|
|
296
314
|
};
|
|
297
315
|
}
|
|
298
|
-
recoverDownstreamProofReplan(repository, taskId, expectedTaskRevision, actor, reason) {
|
|
299
|
-
const context = this.#mutationContext(repository);
|
|
300
|
-
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
301
|
-
assertExpectedRevision(task, expectedTaskRevision);
|
|
302
|
-
// This repair intentionally runs before Project Knowledge refresh: the exact
|
|
303
|
-
// dependency-only transport can make the current map stale, while the
|
|
304
|
-
// recovery assessment still binds the stranded Task to its recorded rebind.
|
|
305
|
-
const observation = this.observationSnapshot(repository).observation;
|
|
306
|
-
if (observation.kind !== 'assessment') {
|
|
307
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan recovery requires a coherent observation boundary.', {
|
|
308
|
-
observation,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
312
|
-
const assessment = assessDownstreamProofReplanRecovery(this.store, context.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
313
|
-
const environmentBlockers = [
|
|
314
|
-
...(!assessment.dependencyRecoveryCommit
|
|
315
|
-
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
316
|
-
: []),
|
|
317
|
-
...(observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
318
|
-
...(observation.pendingProjectTransactions.length > 0
|
|
319
|
-
|| observation.pendingMilestoneTransactions.length > 0
|
|
320
|
-
|| observation.pendingTaskTransactions.length > 0
|
|
321
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
322
|
-
...(observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
323
|
-
...(observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
324
|
-
];
|
|
325
|
-
if (!assessment.applicable || !assessment.eligible || environmentBlockers.length > 0) {
|
|
326
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Task is not eligible for bounded downstream-proof replan recovery.', {
|
|
327
|
-
taskId,
|
|
328
|
-
blockers: [...assessment.blockers, ...environmentBlockers],
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
const now = this.now();
|
|
332
|
-
const resultTask = {
|
|
333
|
-
...task,
|
|
334
|
-
revision: task.revision + 1,
|
|
335
|
-
updatedAt: now.toISOString(),
|
|
336
|
-
status: 'needs_fix',
|
|
337
|
-
authorizations: task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
338
|
-
? { ...authorization, decision: 'superseded' }
|
|
339
|
-
: authorization),
|
|
340
|
-
review: emptyReview(),
|
|
341
|
-
resultHash: null,
|
|
342
|
-
evidenceHash: null,
|
|
343
|
-
blockReason: `${assessment.invalidation.activeStepId} downstream-proof recovery requires replacement of the full unfinished Step Plan before execution can resume.`,
|
|
344
|
-
};
|
|
345
|
-
const recovery = prepareDownstreamProofReplanRecovery(this.store, resultTask, assessment, { previousTaskRevision: task.revision, actor, reason }, now);
|
|
346
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
347
|
-
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
348
|
-
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
349
|
-
applyProjectTransaction({
|
|
350
|
-
projectRoot,
|
|
351
|
-
projectId: task.projectId,
|
|
352
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
353
|
-
kind: 'downstream-proof-replan-recovery',
|
|
354
|
-
now,
|
|
355
|
-
targets: [
|
|
356
|
-
{
|
|
357
|
-
path: relative(path.join(taskRoot, downstreamProofReplanRecoverySidecar())),
|
|
358
|
-
content: recovery.postimage,
|
|
359
|
-
},
|
|
360
|
-
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
361
|
-
],
|
|
362
|
-
validate: () => {
|
|
363
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
364
|
-
const events = readDownstreamProofReplanRecoveries(this.store, saved);
|
|
365
|
-
if (saved.revision !== resultTask.revision
|
|
366
|
-
|| saved.status !== 'needs_fix'
|
|
367
|
-
|| saved.planHash !== task.planHash
|
|
368
|
-
|| events.at(-1)?.eventHash !== recovery.event.eventHash
|
|
369
|
-
|| headCommit(context.identity.repositoryRoot) !== recovery.event.preservedHeadCommit
|
|
370
|
-
|| JSON.stringify(changedFiles(context.identity.repositoryRoot).sort())
|
|
371
|
-
!== JSON.stringify(recovery.event.dirtyFiles)) {
|
|
372
|
-
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery transaction readback failed.', {
|
|
373
|
-
taskId,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
return {
|
|
380
|
-
task: this.store.readTask(task.projectId, task.id),
|
|
381
|
-
recovery: recovery.event,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
316
|
correctiveReplanController(repository) {
|
|
385
317
|
const identity = resolveRepositoryIdentity(repository);
|
|
386
318
|
return createCorrectiveReplanPublicController({
|
|
@@ -568,6 +500,7 @@ export class WorkflowService {
|
|
|
568
500
|
if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
|
|
569
501
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
570
502
|
}
|
|
503
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
571
504
|
if (milestone.baseBranch !== baseBranch) {
|
|
572
505
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task and Milestone must use the same base branch.', {
|
|
573
506
|
taskBaseBranch: baseBranch,
|
|
@@ -1009,7 +942,7 @@ export class WorkflowService {
|
|
|
1009
942
|
milestoneId: milestone.id,
|
|
1010
943
|
oldTaskId: oldTask.id,
|
|
1011
944
|
incidentDependencies,
|
|
1012
|
-
requiredAction: '
|
|
945
|
+
requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
|
|
1013
946
|
});
|
|
1014
947
|
}
|
|
1015
948
|
/* P04-B retains the existing one-for-one transaction below as implementation evidence,
|
|
@@ -1522,34 +1455,37 @@ export class WorkflowService {
|
|
|
1522
1455
|
locks.release(milestoneId, authorityLease.token);
|
|
1523
1456
|
}
|
|
1524
1457
|
}
|
|
1458
|
+
assertMilestoneValidationReady(repositoryRoot, milestone) {
|
|
1459
|
+
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
1460
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} cannot be validated while ${milestone.status}.`);
|
|
1461
|
+
}
|
|
1462
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1463
|
+
if (currentBranch(repositoryRoot) !== milestone.baseBranch || !isClean(repositoryRoot)) {
|
|
1464
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1465
|
+
expectedBranch: milestone.baseBranch,
|
|
1466
|
+
actualBranch: currentBranch(repositoryRoot),
|
|
1467
|
+
changedFiles: changedFiles(repositoryRoot),
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
const incomplete = milestone.memberships.filter((membership) => {
|
|
1471
|
+
if (membership.disposition !== 'required')
|
|
1472
|
+
return false;
|
|
1473
|
+
return this.store.readTask(milestone.projectId, membership.taskId).status !== 'merged';
|
|
1474
|
+
});
|
|
1475
|
+
if (incomplete.length > 0) {
|
|
1476
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1477
|
+
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1525
1481
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
1526
1482
|
const { identity, locks } = this.#mutationContext(repository);
|
|
1527
1483
|
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1528
1484
|
try {
|
|
1529
1485
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1530
1486
|
assertExpectedRevision(milestone, expectedRevision);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}
|
|
1534
|
-
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1535
|
-
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1536
|
-
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1537
|
-
expectedBranch: milestone.baseBranch,
|
|
1538
|
-
actualBranch: currentBranch(identity.repositoryRoot),
|
|
1539
|
-
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
const incomplete = milestone.memberships.filter((membership) => {
|
|
1543
|
-
if (membership.disposition !== 'required')
|
|
1544
|
-
return false;
|
|
1545
|
-
return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
|
|
1546
|
-
});
|
|
1547
|
-
if (incomplete.length > 0) {
|
|
1548
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1549
|
-
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
const checks = runChecks(identity.repositoryRoot, milestone.checks);
|
|
1487
|
+
this.assertMilestoneValidationReady(identity.repositoryRoot, milestone);
|
|
1488
|
+
const checks = runChecks(identity.repositoryRoot, milestone.checks, this.checkOptions);
|
|
1553
1489
|
const baseHead = headCommit(identity.repositoryRoot);
|
|
1554
1490
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1555
1491
|
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
@@ -2097,10 +2033,10 @@ export class WorkflowService {
|
|
|
2097
2033
|
const { identity } = this.#mutationContext(repository);
|
|
2098
2034
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2099
2035
|
assertExpectedRevision(task, expectedRevision);
|
|
2100
|
-
this.
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2036
|
+
const contextBlocker = this.taskContextChangeBlocker(task, 'rebind');
|
|
2037
|
+
// Preserve Plan-corruption precedence over the active-Step rejection.
|
|
2038
|
+
if (contextBlocker && !contextBlocker.activeStep)
|
|
2039
|
+
throw contextBlocker.error;
|
|
2104
2040
|
if (!task.planHash ||
|
|
2105
2041
|
!task.planObjective ||
|
|
2106
2042
|
task.knowledgeMapRevision === null ||
|
|
@@ -2110,10 +2046,8 @@ export class WorkflowService {
|
|
|
2110
2046
|
task.graphUse === null) {
|
|
2111
2047
|
throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no complete Plan binding to rebind.`);
|
|
2112
2048
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
|
|
2116
|
-
}
|
|
2049
|
+
if (contextBlocker)
|
|
2050
|
+
throw contextBlocker.error;
|
|
2117
2051
|
const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
2118
2052
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2119
2053
|
if (task.knowledgeMapRevision === knowledgeMap.revision &&
|
|
@@ -2190,21 +2124,20 @@ export class WorkflowService {
|
|
|
2190
2124
|
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
2191
2125
|
assertExpectedRevision(task, expectedTaskRevision);
|
|
2192
2126
|
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
2193
|
-
this.
|
|
2194
|
-
if (
|
|
2195
|
-
throw
|
|
2196
|
-
}
|
|
2197
|
-
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
2198
|
-
if (activeStep) {
|
|
2199
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
|
|
2200
|
-
}
|
|
2127
|
+
const contextBlocker = this.taskContextChangeBlocker(task, 'refresh');
|
|
2128
|
+
if (contextBlocker)
|
|
2129
|
+
throw contextBlocker.error;
|
|
2201
2130
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
2202
2131
|
const transitions = [];
|
|
2203
|
-
const
|
|
2132
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2133
|
+
const currentPlanRiskAudit = planningRecovery?.current ?? readCurrentPlanRiskAudit(this.store, task);
|
|
2204
2134
|
const currentPlanArtifactHash = task.planHash
|
|
2205
2135
|
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
|
|
2206
2136
|
: null;
|
|
2207
2137
|
const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
|
|
2138
|
+
if (planningRecovery && refreshAssessment.mode !== 'delegated-content-only') {
|
|
2139
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stopped planning context refresh permits content-only Knowledge drift.');
|
|
2140
|
+
}
|
|
2208
2141
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
2209
2142
|
// leave the composite operation half-applied.
|
|
2210
2143
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
@@ -2251,11 +2184,15 @@ export class WorkflowService {
|
|
|
2251
2184
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
2252
2185
|
}
|
|
2253
2186
|
transitions.push('task knowledge-rebind');
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2187
|
+
// A stopped Plan may regain its Knowledge binding, never execution authority.
|
|
2188
|
+
if (!planningRecovery)
|
|
2189
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
|
|
2190
|
+
? 'Delegated Plan-bounded supporting-source context refresh.'
|
|
2191
|
+
: 'Delegated content-only context refresh.', delegationGrantId);
|
|
2192
|
+
if (!planningRecovery)
|
|
2193
|
+
transitions.push('task execution authorize');
|
|
2258
2194
|
return {
|
|
2195
|
+
authority: planningRecovery ? 'planning-only' : 'execution',
|
|
2259
2196
|
mode: refreshAssessment.mode,
|
|
2260
2197
|
transitions,
|
|
2261
2198
|
addedSources: refreshAssessment.addedSources,
|
|
@@ -2265,8 +2202,16 @@ export class WorkflowService {
|
|
|
2265
2202
|
},
|
|
2266
2203
|
validate: (result) => {
|
|
2267
2204
|
const currentTask = this.store.readTask(identity.projectId, task.id);
|
|
2268
|
-
if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
|
|
2269
|
-
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit
|
|
2205
|
+
if (currentTask.revision !== result.task.revision || currentTask.status !== (planningRecovery ? 'awaiting_execution_authorization' : 'ready')) {
|
|
2206
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit the expected Task posture.');
|
|
2207
|
+
}
|
|
2208
|
+
if (planningRecovery) {
|
|
2209
|
+
const rebound = readFailedStepPlanningRecovery(this.store, currentTask);
|
|
2210
|
+
if (!rebound || rebound.baseline.eventHash !== planningRecovery.baseline.eventHash
|
|
2211
|
+
|| JSON.stringify(currentTask.steps) !== JSON.stringify(task.steps)
|
|
2212
|
+
|| JSON.stringify(currentTask.authorizations) !== JSON.stringify(task.authorizations)) {
|
|
2213
|
+
throw new WorkflowError('STATE_CORRUPT', 'Planning context refresh changed retained recovery evidence.');
|
|
2214
|
+
}
|
|
2270
2215
|
}
|
|
2271
2216
|
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
2272
2217
|
if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
|
|
@@ -2339,6 +2284,7 @@ export class WorkflowService {
|
|
|
2339
2284
|
const { identity } = this.#mutationContext(repository);
|
|
2340
2285
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2341
2286
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2287
|
+
assertNoPendingBaseSyncValidation(task);
|
|
2342
2288
|
assertExpectedRevision(task, expectedRevision);
|
|
2343
2289
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2344
2290
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires task plan-set; the blocking audit cannot be replaced in place.', { taskId, requiredAction: 'task plan-set' });
|
|
@@ -2489,7 +2435,16 @@ export class WorkflowService {
|
|
|
2489
2435
|
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
2490
2436
|
const context = this.#mutationContext(repository);
|
|
2491
2437
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2438
|
+
assertNoPendingBaseSyncValidation(task);
|
|
2492
2439
|
assertExpectedRevision(task, expectedRevision);
|
|
2440
|
+
if (task.status === 'ready' && changedFiles(context.identity.repositoryRoot).length > 0) {
|
|
2441
|
+
const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task);
|
|
2442
|
+
if (carryover.requiredActor && targetActor.trim() !== carryover.requiredActor) {
|
|
2443
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Native carryover handoff must return to its recorded execution actor.', {
|
|
2444
|
+
taskId, requiredActor: carryover.requiredActor,
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2493
2448
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2494
2449
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
|
|
2495
2450
|
taskId,
|
|
@@ -2793,7 +2748,7 @@ export class WorkflowService {
|
|
|
2793
2748
|
const step = requireStep(task, stepId);
|
|
2794
2749
|
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2795
2750
|
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
2796
|
-
if (
|
|
2751
|
+
if (stepStartPhase(step.status, false) === 'not-startable') {
|
|
2797
2752
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
2798
2753
|
}
|
|
2799
2754
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
@@ -2813,6 +2768,7 @@ export class WorkflowService {
|
|
|
2813
2768
|
? guardedPosture.correctiveDecision
|
|
2814
2769
|
: null;
|
|
2815
2770
|
const recovery = staleDecision?.decision === 'continue-fix'
|
|
2771
|
+
&& !isKnowledgeReboundSupportDecision(this.store, task, staleDecision)
|
|
2816
2772
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
2817
2773
|
: null;
|
|
2818
2774
|
const stopOverrideContextRebind = staleDecision?.decision === 'stop-escalate'
|
|
@@ -2823,23 +2779,28 @@ export class WorkflowService {
|
|
|
2823
2779
|
}
|
|
2824
2780
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2825
2781
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
2826
|
-
const
|
|
2782
|
+
const startPhase = stepStartPhase(step.status, dirty.length > 0);
|
|
2783
|
+
const downstreamProofCarryover = startPhase === 'planned-dirty'
|
|
2827
2784
|
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2828
2785
|
: null;
|
|
2829
|
-
const planIntegrityDirtyCarryover =
|
|
2786
|
+
const planIntegrityDirtyCarryover = startPhase === 'planned-dirty'
|
|
2830
2787
|
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2831
2788
|
: null;
|
|
2832
|
-
const
|
|
2789
|
+
const navigationDirtyCarryover = startPhase === 'planned-dirty'
|
|
2790
|
+
? assessNavigationDirtyCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2791
|
+
: null;
|
|
2792
|
+
const rootCauseReplanDirtyCarryover = startPhase === 'planned-dirty' && !navigationDirtyCarryover?.applicable
|
|
2833
2793
|
? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2834
2794
|
: null;
|
|
2835
|
-
if (
|
|
2836
|
-
&& dirty.length > 0
|
|
2795
|
+
if (startPhase === 'planned-dirty'
|
|
2837
2796
|
&& !resumesCorrectiveReplan
|
|
2838
2797
|
&& !downstreamProofCarryover?.eligible
|
|
2839
2798
|
&& !planIntegrityDirtyCarryover?.eligible
|
|
2799
|
+
&& !navigationDirtyCarryover?.eligible
|
|
2840
2800
|
&& !rootCauseReplanDirtyCarryover?.eligible) {
|
|
2841
2801
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2842
2802
|
changedFiles: dirty,
|
|
2803
|
+
...(navigationDirtyCarryover?.applicable ? { navigationDirtyCarryoverBlockers: navigationDirtyCarryover.blockers } : {}),
|
|
2843
2804
|
...(downstreamProofCarryover?.applicable
|
|
2844
2805
|
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2845
2806
|
: {}),
|
|
@@ -2852,7 +2813,7 @@ export class WorkflowService {
|
|
|
2852
2813
|
});
|
|
2853
2814
|
}
|
|
2854
2815
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
2855
|
-
if ((
|
|
2816
|
+
if ((startPhase === 'failed-retry' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
2856
2817
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
2857
2818
|
changedFiles: dirtyOutsideStep,
|
|
2858
2819
|
});
|
|
@@ -3073,7 +3034,7 @@ export class WorkflowService {
|
|
|
3073
3034
|
return task;
|
|
3074
3035
|
}
|
|
3075
3036
|
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
3076
|
-
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
3037
|
+
const checks = runChecks(context.identity.repositoryRoot, step.checks, this.checkOptions);
|
|
3077
3038
|
const failed = checks.filter((check) => !check.passed);
|
|
3078
3039
|
if (failed.length > 0) {
|
|
3079
3040
|
const now = this.now();
|
|
@@ -3081,13 +3042,16 @@ export class WorkflowService {
|
|
|
3081
3042
|
// Bind useful uncommitted work to this failure without treating it as
|
|
3082
3043
|
// completed-Step authority.
|
|
3083
3044
|
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3045
|
+
const capturedEvidence = prepareCapturedCheckEvidence(this.store, task, checks);
|
|
3084
3046
|
const failurePause = {
|
|
3085
3047
|
recordedAt: now.toISOString(),
|
|
3086
3048
|
dirtyFiles,
|
|
3087
3049
|
dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
|
|
3088
3050
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
3089
3051
|
failedChecks: normalizedFailures,
|
|
3052
|
+
checksEvidence: capturedEvidence.reference,
|
|
3090
3053
|
};
|
|
3054
|
+
const failureC1Posture = readTaskC1Posture(this.store, task);
|
|
3091
3055
|
const preparedRemediation = reviewRequired && planRiskAudit
|
|
3092
3056
|
? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
3093
3057
|
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
@@ -3096,6 +3060,11 @@ export class WorkflowService {
|
|
|
3096
3060
|
reviewerAttestationHash: null,
|
|
3097
3061
|
executionTaskRevision: task.revision,
|
|
3098
3062
|
dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
|
|
3063
|
+
...(failureC1Posture.state === 'none' ? { unclaimedExecutionBoundary: {
|
|
3064
|
+
kind: 'native-unclaimed-check-failure',
|
|
3065
|
+
actor: lease.owner,
|
|
3066
|
+
priorHandoffEventHash: failureC1Posture.latestEvent?.eventHash ?? null,
|
|
3067
|
+
} } : {}),
|
|
3099
3068
|
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
|
|
3100
3069
|
: null;
|
|
3101
3070
|
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
@@ -3143,6 +3112,7 @@ export class WorkflowService {
|
|
|
3143
3112
|
: 'failed-step-continuation',
|
|
3144
3113
|
now,
|
|
3145
3114
|
targets: [
|
|
3115
|
+
...(capturedEvidence.target ? [capturedEvidence.target] : []),
|
|
3146
3116
|
...(preparedRemediation?.postimage
|
|
3147
3117
|
? [{
|
|
3148
3118
|
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
@@ -3158,6 +3128,7 @@ export class WorkflowService {
|
|
|
3158
3128
|
validate: () => {
|
|
3159
3129
|
const saved = this.store.readTask(task.projectId, task.id);
|
|
3160
3130
|
const savedStep = requireStep(saved, stepId);
|
|
3131
|
+
assertCapturedCheckEvidence(this.store, saved, savedStep.failurePause, checks);
|
|
3161
3132
|
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3162
3133
|
const remediationHash = preparedRemediation
|
|
3163
3134
|
? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
|
|
@@ -3522,6 +3493,7 @@ export class WorkflowService {
|
|
|
3522
3493
|
prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
|
|
3523
3494
|
const identity = resolveRepositoryIdentity(repository);
|
|
3524
3495
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3496
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3525
3497
|
assertExpectedRevision(task, expectedRevision);
|
|
3526
3498
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
3527
3499
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
@@ -3604,6 +3576,7 @@ export class WorkflowService {
|
|
|
3604
3576
|
const { identity } = this.#mutationContext(repository);
|
|
3605
3577
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
3606
3578
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3579
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3607
3580
|
assertExpectedRevision(task, expectedRevision);
|
|
3608
3581
|
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
3609
3582
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
@@ -3654,12 +3627,52 @@ export class WorkflowService {
|
|
|
3654
3627
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3655
3628
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
3656
3629
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3630
|
+
/** Recording a machine finding may retain its exact failed execution context;
|
|
3631
|
+
* it never approves changed Knowledge or authorizes another task run. */
|
|
3632
|
+
assessPlanIntegrityRecoveryAdmission(repository, task, stepId) {
|
|
3633
|
+
const assessment = assessPlanIntegrityRecovery(this.store, repository, task, stepId);
|
|
3634
|
+
if (!assessment.eligible)
|
|
3635
|
+
return assessment;
|
|
3636
|
+
try {
|
|
3637
|
+
this.assertExecutionAuthorizationFresh(task, repository);
|
|
3638
|
+
const stored = this.store.readKnowledgeMap(task.projectId);
|
|
3639
|
+
const storedHash = hashKnowledgeMap(stored);
|
|
3640
|
+
if (stored.status !== 'active' || !stored.approval || stored.approval.mapHash !== storedHash
|
|
3641
|
+
|| task.knowledgeMapRevision !== stored.revision || task.knowledgeMapHash !== storedHash) {
|
|
3642
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery requires the exact approved Task Knowledge binding.');
|
|
3643
|
+
}
|
|
3644
|
+
const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repository, task.projectId, this.now()));
|
|
3645
|
+
if (inspected.status === 'active' && inspected.approval?.mapHash === storedHash && hashKnowledgeMap(inspected) === storedHash)
|
|
3646
|
+
return assessment;
|
|
3647
|
+
if (inspected.status !== 'stale' || !isContentOnlyKnowledgeRefresh(stored, inspected)) {
|
|
3648
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery permits only content changes within the recorded failed execution.');
|
|
3649
|
+
}
|
|
3650
|
+
const step = requireStep(task, stepId);
|
|
3651
|
+
const pause = step.failurePause;
|
|
3652
|
+
const last = readRemediationEvents(this.store, task).filter(event => event.stepId === stepId).at(-1);
|
|
3653
|
+
const dirty = changedFiles(repository).sort();
|
|
3654
|
+
const changedKnowledge = inspected.entries.filter(entry => stored.entries.find(previous => previous.id === entry.id)?.contentHash !== entry.contentHash);
|
|
3655
|
+
// Only the immediately recorded native failure may defer Knowledge approval;
|
|
3656
|
+
// historical transport or later Task revisions never establish this admission.
|
|
3657
|
+
if (!pause || !last || last.failureKind !== 'checks-failed'
|
|
3658
|
+
|| last.failureEvidence.executionTaskRevision !== task.revision - 1
|
|
3659
|
+
|| last.failureEvidence.completionCommit !== headCommit(repository)
|
|
3660
|
+
|| last.failureEvidence.checksHash !== pause.checksHash
|
|
3661
|
+
|| last.failureEvidence.dirtyWorktreeHash !== pause.dirtyWorktreeHash
|
|
3662
|
+
|| JSON.stringify(dirty) !== JSON.stringify([...pause.dirtyFiles].sort())
|
|
3663
|
+
|| hashDirtyWorktree(repository, dirty) !== pause.dirtyWorktreeHash
|
|
3664
|
+
|| changedKnowledge.some(entry => !dirty.includes(entry.path)
|
|
3665
|
+
|| !pathAllowed(entry.path, step.allowedWrites) || pathAllowed(entry.path, step.forbiddenScope))) {
|
|
3666
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Recovery Knowledge drift must match the exact native failed-Step HEAD, checks and dirty bytes.');
|
|
3667
|
+
}
|
|
3668
|
+
assertCurrentCapturedCheckEvidence(this.store, task, stepId);
|
|
3669
|
+
return assessment;
|
|
3670
|
+
}
|
|
3671
|
+
catch (error) {
|
|
3672
|
+
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3673
|
+
throw error;
|
|
3674
|
+
return { ...assessment, eligible: false, blockers: [...assessment.blockers, error.message] };
|
|
3675
|
+
}
|
|
3663
3676
|
}
|
|
3664
3677
|
recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
3665
3678
|
const context = this.#mutationContext(repository);
|
|
@@ -3688,29 +3701,12 @@ export class WorkflowService {
|
|
|
3688
3701
|
stepId,
|
|
3689
3702
|
});
|
|
3690
3703
|
}
|
|
3691
|
-
const assessment =
|
|
3704
|
+
const assessment = this.assessPlanIntegrityRecoveryAdmission(context.identity.repositoryRoot, task, stepId);
|
|
3692
3705
|
if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
|
|
3693
3706
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
|
|
3694
|
-
taskId,
|
|
3695
|
-
stepId,
|
|
3696
|
-
applicable: assessment.applicable,
|
|
3697
|
-
blockers: assessment.blockers,
|
|
3707
|
+
taskId, stepId, applicable: assessment.applicable, blockers: assessment.blockers,
|
|
3698
3708
|
});
|
|
3699
3709
|
}
|
|
3700
|
-
const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
3701
|
-
? [assessment.evidence.remediationEventId]
|
|
3702
|
-
: assessment.evidence.remediationEvents.map((event) => event.eventId);
|
|
3703
|
-
const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
|
|
3704
|
-
&& record.packageVersion === PACKAGE_VERSION
|
|
3705
|
-
&& task.systemCommits.includes(record.commitSha)
|
|
3706
|
-
&& record.planIntegrity?.stepId === stepId
|
|
3707
|
-
&& record.planIntegrity.planHash === task.planHash
|
|
3708
|
-
&& canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
|
|
3709
|
-
&& record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
|
|
3710
|
-
if (!compatibility) {
|
|
3711
|
-
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
3712
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3713
|
-
}
|
|
3714
3710
|
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
3715
3711
|
return { assessment, event };
|
|
3716
3712
|
}
|
|
@@ -3761,6 +3757,9 @@ export class WorkflowService {
|
|
|
3761
3757
|
resolution: currentCycle,
|
|
3762
3758
|
});
|
|
3763
3759
|
}
|
|
3760
|
+
if (!correctiveDecisionCheckoutEligible(this.store, identity.repositoryRoot, task, stepId)) {
|
|
3761
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision requires the Task checkout to be clean or match its exact in-scope failed checkpoint.', { taskId, stepId });
|
|
3762
|
+
}
|
|
3764
3763
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
3765
3764
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
3766
3765
|
}
|
|
@@ -3768,6 +3767,7 @@ export class WorkflowService {
|
|
|
3768
3767
|
const context = this.#mutationContext(repository);
|
|
3769
3768
|
const { identity } = context;
|
|
3770
3769
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3770
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3771
3771
|
assertExpectedRevision(task, expectedRevision);
|
|
3772
3772
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
3773
3773
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Result requires a passing independent review.');
|
|
@@ -3788,6 +3788,7 @@ export class WorkflowService {
|
|
|
3788
3788
|
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
3789
3789
|
const { identity } = this.#mutationContext(repository);
|
|
3790
3790
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3791
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3791
3792
|
assertExpectedRevision(task, expectedRevision);
|
|
3792
3793
|
if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
|
|
3793
3794
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for final acceptance.`);
|
|
@@ -3834,59 +3835,70 @@ export class WorkflowService {
|
|
|
3834
3835
|
}
|
|
3835
3836
|
syncTaskBase(repository, taskId, expectedRevision) {
|
|
3836
3837
|
const { identity } = this.#mutationContext(repository);
|
|
3837
|
-
|
|
3838
|
+
let task = this.store.readTask(identity.projectId, taskId);
|
|
3838
3839
|
assertExpectedRevision(task, expectedRevision);
|
|
3839
|
-
if (task.
|
|
3840
|
-
|
|
3841
|
-
}
|
|
3842
|
-
const currentBase = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
3843
|
-
if (currentBase === task.baseCommit) {
|
|
3844
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Base branch has not advanced; synchronization is unnecessary.');
|
|
3845
|
-
}
|
|
3846
|
-
const synced = syncBaseIntoTask(identity.repositoryRoot, task.taskBranch, task.baseBranch);
|
|
3847
|
-
const checks = runChecks(identity.repositoryRoot, [...new Set(task.steps.flatMap((step) => step.checks))]);
|
|
3848
|
-
const failed = checks.filter((check) => !check.passed);
|
|
3849
|
-
if (failed.length > 0) {
|
|
3850
|
-
return this.store.writeTask({
|
|
3851
|
-
...task,
|
|
3852
|
-
status: 'needs_fix',
|
|
3853
|
-
baseCommit: synced.baseCommit,
|
|
3854
|
-
systemCommits: [...task.systemCommits, synced.syncCommit],
|
|
3855
|
-
review: emptyReview(),
|
|
3856
|
-
resultHash: null,
|
|
3857
|
-
blockReason: 'Checks failed after synchronizing the base branch.',
|
|
3858
|
-
}, expectedRevision);
|
|
3840
|
+
if (task.pendingBaseSyncValidation !== undefined) {
|
|
3841
|
+
assertPendingBaseSyncValidationReady(task);
|
|
3859
3842
|
}
|
|
3843
|
+
else {
|
|
3844
|
+
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
|
|
3845
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Base sync is available only for an accepted local Task.');
|
|
3846
|
+
}
|
|
3847
|
+
const currentBase = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
3848
|
+
if (currentBase === task.baseCommit) {
|
|
3849
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Base branch has not advanced; synchronization is unnecessary.');
|
|
3850
|
+
}
|
|
3851
|
+
const taskHead = headCommit(identity.repositoryRoot);
|
|
3852
|
+
if (currentBaseSyncConflict(this.store, task, taskHead, currentBase)) {
|
|
3853
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Base sync awaits an external decision for this exact conflicting HEAD pair.', {
|
|
3854
|
+
taskId, taskHead, baseHead: currentBase, waitFor: 'The base owner resolves the conflict on the base branch without changing the reviewed Task head.',
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
let synced;
|
|
3858
|
+
try {
|
|
3859
|
+
synced = syncBaseIntoTask(identity.repositoryRoot, task.taskBranch, task.baseBranch);
|
|
3860
|
+
}
|
|
3861
|
+
catch (error) {
|
|
3862
|
+
if (error instanceof WorkflowError && error.code === 'GIT_PRECONDITION_FAILED'
|
|
3863
|
+
&& error.details.cleanAbortVerified === true && headCommit(identity.repositoryRoot) === taskHead
|
|
3864
|
+
&& runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]) === currentBase) {
|
|
3865
|
+
recordBaseSyncConflict(this.store, task, taskHead, currentBase);
|
|
3866
|
+
}
|
|
3867
|
+
throw error;
|
|
3868
|
+
}
|
|
3869
|
+
task = this.store.writeTask({
|
|
3870
|
+
...task, status: 'validating', baseCommit: synced.baseCommit,
|
|
3871
|
+
systemCommits: [...new Set([...task.systemCommits, synced.syncCommit])],
|
|
3872
|
+
evidenceHash: null, resultHash: null, review: emptyReview(),
|
|
3873
|
+
authorizations: task.authorizations.map(item => item.kind === 'final_acceptance' && item.decision === 'approved'
|
|
3874
|
+
? { ...item, decision: 'superseded' } : item),
|
|
3875
|
+
pendingBaseSyncValidation: { baseCommit: synced.baseCommit, syncCommit: synced.syncCommit,
|
|
3876
|
+
headCommit: headCommit(identity.repositoryRoot), checksHash: taskSyncChecksHash(task) },
|
|
3877
|
+
blockReason: 'Base synchronized; checks must complete before review and acceptance.',
|
|
3878
|
+
}, task.revision);
|
|
3879
|
+
}
|
|
3880
|
+
assertPendingBaseSyncValidationReady(task);
|
|
3881
|
+
const checks = runChecks(identity.repositoryRoot, taskSyncChecks(task), this.checkOptions);
|
|
3882
|
+
// A check that changes the repository cannot certify the recorded merge snapshot.
|
|
3883
|
+
assertPendingBaseSyncValidationReady(task);
|
|
3884
|
+
const { pendingBaseSyncValidation: _pending, ...validatedTask } = task;
|
|
3885
|
+
const failed = checks.some(check => !check.passed);
|
|
3860
3886
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
3861
3887
|
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
3862
|
-
taskId: task.id,
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
reason: 'base-sync',
|
|
3866
|
-
checks,
|
|
3867
|
-
steps: task.steps.map((step) => ({ id: step.id, evidence: step.evidence })),
|
|
3868
|
-
systemCommits: [...task.systemCommits, synced.syncCommit],
|
|
3888
|
+
taskId: task.id, headCommit: headCommit(identity.repositoryRoot), generatedAt: this.now().toISOString(),
|
|
3889
|
+
reason: 'base-sync', checks, steps: task.steps.map(step => ({ id: step.id, evidence: step.evidence })),
|
|
3890
|
+
systemCommits: task.systemCommits,
|
|
3869
3891
|
}, null, 2));
|
|
3870
|
-
const
|
|
3871
|
-
?
|
|
3872
|
-
|
|
3873
|
-
const saved = this.store.writeTask({
|
|
3874
|
-
...task,
|
|
3875
|
-
status: 'validating',
|
|
3876
|
-
baseCommit: synced.baseCommit,
|
|
3877
|
-
systemCommits: [...task.systemCommits, synced.syncCommit],
|
|
3878
|
-
evidenceHash: stagedEvidence.hash,
|
|
3879
|
-
review: emptyReview(),
|
|
3880
|
-
resultHash: null,
|
|
3881
|
-
authorizations,
|
|
3882
|
-
blockReason: 'Base changed; validation, review, and final acceptance must be repeated.',
|
|
3883
|
-
}, expectedRevision);
|
|
3892
|
+
const saved = this.store.writeTask({ ...validatedTask, status: failed ? 'needs_fix' : 'validating', evidenceHash: stagedEvidence.hash,
|
|
3893
|
+
blockReason: failed ? 'Checks failed after synchronizing the base branch.'
|
|
3894
|
+
: 'Base changed; validation, review, and final acceptance must be repeated.' }, task.revision);
|
|
3884
3895
|
this.store.publishArtifact(stagedEvidence);
|
|
3885
3896
|
return saved;
|
|
3886
3897
|
}
|
|
3887
3898
|
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
3888
3899
|
const context = this.#mutationContext(repository);
|
|
3889
3900
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
3901
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3890
3902
|
assertExpectedRevision(task, expectedRevision);
|
|
3891
3903
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
3892
3904
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
@@ -4036,137 +4048,6 @@ export class WorkflowService {
|
|
|
4036
4048
|
now: this.now(),
|
|
4037
4049
|
}, actor, confirmationCode);
|
|
4038
4050
|
}
|
|
4039
|
-
pendingReviewUpdatePreflight(repository, taskId) {
|
|
4040
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4041
|
-
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4042
|
-
try {
|
|
4043
|
-
pendingReviewRunnerFingerprint();
|
|
4044
|
-
}
|
|
4045
|
-
catch (error) {
|
|
4046
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4047
|
-
}
|
|
4048
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4049
|
-
let binding = null;
|
|
4050
|
-
if (PACKAGE_VERSION === PENDING_REVIEW_SOURCE_VERSION)
|
|
4051
|
-
blockers.push('The exact target runner must be newer than the supported source.');
|
|
4052
|
-
if (!task)
|
|
4053
|
-
blockers.push('Task not found.');
|
|
4054
|
-
if (task) {
|
|
4055
|
-
try {
|
|
4056
|
-
binding = pendingReviewSourceBinding(snapshot.identity.repositoryRoot, this.store, task);
|
|
4057
|
-
}
|
|
4058
|
-
catch (error) {
|
|
4059
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4060
|
-
}
|
|
4061
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4062
|
-
if ([versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4063
|
-
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PENDING_REVIEW_SOURCE_VERSION)) {
|
|
4064
|
-
blockers.push('All source version surfaces must equal the exact supported beta.13.1.');
|
|
4065
|
-
}
|
|
4066
|
-
}
|
|
4067
|
-
const otherRunningSteps = snapshot.tasks.flatMap((candidate) => candidate.steps.filter((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== binding?.stepId)));
|
|
4068
|
-
if (otherRunningSteps.length)
|
|
4069
|
-
blockers.push('No other Step may be running.');
|
|
4070
|
-
const stale = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= this.now().getTime());
|
|
4071
|
-
if (snapshot.leases.some((lease) => !stale.includes(lease)))
|
|
4072
|
-
blockers.push('No live writer lease may exist.');
|
|
4073
|
-
if (stale.some((lease) => lease.entityId !== taskId))
|
|
4074
|
-
blockers.push('No unrelated stale writer lease may exist.');
|
|
4075
|
-
const staleLeaseRepair = blockers.length === 0 && binding
|
|
4076
|
-
? stale.map((lease) => ({ action: 'locks repair', entityId: lease.entityId })) : [];
|
|
4077
|
-
if (stale.length)
|
|
4078
|
-
blockers.push('Repair the exact stale Task lease with the installed source locks repair, then rerun source preflight.');
|
|
4079
|
-
return { readOnly: true, eligible: blockers.length === 0 && binding !== null, binding,
|
|
4080
|
-
bindingHash: binding ? pendingReviewBindingHash(binding) : null, blockers, staleLeaseRepair };
|
|
4081
|
-
}
|
|
4082
|
-
pendingReviewDependencyPreflight(repository, taskId, source) {
|
|
4083
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4084
|
-
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4085
|
-
try {
|
|
4086
|
-
pendingReviewRunnerFingerprint();
|
|
4087
|
-
}
|
|
4088
|
-
catch (error) {
|
|
4089
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4090
|
-
}
|
|
4091
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4092
|
-
const taskHead = headCommit(snapshot.identity.repositoryRoot);
|
|
4093
|
-
let baseTransportCommit = null;
|
|
4094
|
-
if (snapshot.leases.length)
|
|
4095
|
-
blockers.push('No live or stale writer lease may exist.');
|
|
4096
|
-
if (!task)
|
|
4097
|
-
blockers.push('Task not found.');
|
|
4098
|
-
if (task && existsSync(path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)))
|
|
4099
|
-
blockers.push('A pending review update is already recorded.');
|
|
4100
|
-
if (task) {
|
|
4101
|
-
try {
|
|
4102
|
-
// Receipt fields are claims, not authority. Reconstruct every source binding from
|
|
4103
|
-
// canonical Task/sidecars and the exact parents of the candidate Git commits.
|
|
4104
|
-
const parent = runGit(repository, ['rev-parse', 'HEAD^']);
|
|
4105
|
-
baseTransportCommit = runGit(repository, ['rev-parse', task.baseBranch]);
|
|
4106
|
-
const baseParent = runGit(repository, ['rev-parse', `${baseTransportCommit}^`]);
|
|
4107
|
-
const reconstructed = pendingReviewSourceBinding(repository, this.store, task, parent, baseParent);
|
|
4108
|
-
if (canonicalJsonStringify(reconstructed) !== canonicalJsonStringify(source)) {
|
|
4109
|
-
throw new Error('Source receipt does not match reconstructed Task/revision/Plan/HEAD/pending bindings.');
|
|
4110
|
-
}
|
|
4111
|
-
assertPendingReviewTransport(repository, reconstructed, taskHead, baseTransportCommit);
|
|
4112
|
-
if (snapshot.tasks.some((candidate) => candidate.steps.some((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== source.stepId)))) {
|
|
4113
|
-
throw new Error('No other Step may be running.');
|
|
4114
|
-
}
|
|
4115
|
-
const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
|
|
4116
|
-
if ([versions.runtime, versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4117
|
-
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PACKAGE_VERSION)) {
|
|
4118
|
-
throw new Error('All target version surfaces must equal the exact target runner.');
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
catch (error) {
|
|
4122
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4123
|
-
}
|
|
4124
|
-
}
|
|
4125
|
-
return { readOnly: true, eligible: blockers.length === 0, blockers, taskHead, baseTransportCommit };
|
|
4126
|
-
}
|
|
4127
|
-
recoverPendingReviewDependency(repository, taskId, expectedRevision, source, actor, reason) {
|
|
4128
|
-
if (!actor.trim() || !reason.trim())
|
|
4129
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Actor and reason are required.');
|
|
4130
|
-
const initial = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4131
|
-
if (!initial.eligible)
|
|
4132
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight failed.', initial);
|
|
4133
|
-
const context = this.#mutationContext(repository);
|
|
4134
|
-
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
4135
|
-
assertExpectedRevision(task, expectedRevision);
|
|
4136
|
-
const preflight = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4137
|
-
if (!preflight.eligible || !preflight.baseTransportCommit) {
|
|
4138
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight changed.', preflight);
|
|
4139
|
-
}
|
|
4140
|
-
const now = this.now();
|
|
4141
|
-
const nextTask = { ...task, revision: task.revision + 1, updatedAt: now.toISOString(),
|
|
4142
|
-
systemCommits: [...task.systemCommits, preflight.taskHead],
|
|
4143
|
-
dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
|
|
4144
|
-
commitSha: preflight.taskHead, parentCommitSha: source.completionCommit,
|
|
4145
|
-
packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, files: ['package-lock.json', 'package.json'],
|
|
4146
|
-
actor: actor.trim(), reason: reason.trim(), recordedAt: now.toISOString(),
|
|
4147
|
-
}] };
|
|
4148
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
4149
|
-
const event = preparePendingReviewUpdateEvent({ source, baseTransportCommit: preflight.baseTransportCommit }, preflight.taskHead, now.toISOString());
|
|
4150
|
-
applyProjectTransaction({ projectRoot, projectId: task.projectId,
|
|
4151
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
4152
|
-
kind: 'pending-review-dependency-recovery', now,
|
|
4153
|
-
targets: [{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')).split(path.sep).join('/'),
|
|
4154
|
-
content: `${JSON.stringify(nextTask, null, 2)}\n` },
|
|
4155
|
-
{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)).split(path.sep).join('/'),
|
|
4156
|
-
content: `${canonicalJsonStringify(event)}\n` }],
|
|
4157
|
-
validate: () => {
|
|
4158
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
4159
|
-
if (saved.revision !== nextTask.revision || !verifiedPendingReviewTransport(repository, saved, source.stepId, this.store.taskRoot(task.projectId, task.id))) {
|
|
4160
|
-
throw new WorkflowError('STATE_CORRUPT', 'Pending review dependency recovery readback failed.');
|
|
4161
|
-
}
|
|
4162
|
-
const cycle = buildCurrentStrictStepReviewCycle(this.store, saved, source.stepId);
|
|
4163
|
-
if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== source.pendingEventHash) {
|
|
4164
|
-
throw new WorkflowError('STATE_CORRUPT', 'Pending review changed during dependency recovery.');
|
|
4165
|
-
}
|
|
4166
|
-
},
|
|
4167
|
-
});
|
|
4168
|
-
return this.store.readTask(task.projectId, task.id);
|
|
4169
|
-
}
|
|
4170
4051
|
updatePreflight(repository) {
|
|
4171
4052
|
const snapshot = this.observationSnapshot(repository);
|
|
4172
4053
|
const tasks = snapshot.tasks;
|
|
@@ -4248,6 +4129,69 @@ export class WorkflowService {
|
|
|
4248
4129
|
observation: snapshot.observation,
|
|
4249
4130
|
};
|
|
4250
4131
|
}
|
|
4132
|
+
navigationUpdateBoundary(repository, taskId) {
|
|
4133
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4134
|
+
const observation = snapshot.observation;
|
|
4135
|
+
const task = snapshot.tasks.find(item => item.id === taskId);
|
|
4136
|
+
if (!task || observation.kind !== 'assessment' || snapshot.leases.length
|
|
4137
|
+
|| snapshot.tasks.some(item => item.steps.some(step => step.status === 'in_progress'))
|
|
4138
|
+
|| observation.pendingProjectTransactions.length || observation.pendingMilestoneTransactions.length
|
|
4139
|
+
|| observation.pendingTaskTransactions.length || observation.corruptTaskTransactions.length
|
|
4140
|
+
|| observation.coreTaskOperations.length) {
|
|
4141
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation update requires coherent state and no leases or pending operations.');
|
|
4142
|
+
}
|
|
4143
|
+
const otherBases = snapshot.milestones.filter(item => !['accepted', 'cancelled'].includes(item.status)
|
|
4144
|
+
&& item.baseBranch !== task.baseBranch);
|
|
4145
|
+
if (otherBases.length)
|
|
4146
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation update supports one active base branch.');
|
|
4147
|
+
return { snapshot, task, root: snapshot.identity.repositoryRoot };
|
|
4148
|
+
}
|
|
4149
|
+
navigationUpdateSourcePreflight(repository, taskId, tarball) {
|
|
4150
|
+
try {
|
|
4151
|
+
const { task, root } = this.navigationUpdateBoundary(repository, taskId);
|
|
4152
|
+
assertNavigationSourceInstalled(root, task);
|
|
4153
|
+
const receipt = navigationUpdateSource(root, this.store, task, verifyNavigationUpdateArtifact(tarball), task.updatedAt);
|
|
4154
|
+
return { eligible: true, blockers: [], receipt };
|
|
4155
|
+
}
|
|
4156
|
+
catch (error) {
|
|
4157
|
+
return { eligible: false, blockers: [error instanceof Error ? error.message : String(error)], receipt: null };
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
navigationUpdateTransportPreflight(repository, taskId, tarball, receipt) {
|
|
4161
|
+
try {
|
|
4162
|
+
const { task, root } = this.navigationUpdateBoundary(repository, taskId);
|
|
4163
|
+
const proof = inspectNavigationTransport(root, this.store, task, receipt, verifyNavigationUpdateArtifact(tarball, path.join(root, 'node_modules/codex-workflow-v2')), this.now());
|
|
4164
|
+
return { eligible: true, blockers: [], proof,
|
|
4165
|
+
action: 'update navigation-recover', expectedRevision: task.revision, actor: receipt.actor };
|
|
4166
|
+
}
|
|
4167
|
+
catch (error) {
|
|
4168
|
+
return { eligible: false, blockers: [error instanceof Error ? error.message : String(error)], proof: null };
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
recoverNavigationUpdate(repository, taskId, expectedRevision, tarball, receipt, actor, reason) {
|
|
4172
|
+
this.#mutationContext(repository);
|
|
4173
|
+
const { task, root } = this.navigationUpdateBoundary(repository, taskId);
|
|
4174
|
+
assertExpectedRevision(task, expectedRevision);
|
|
4175
|
+
if (!actor.trim() || actor !== receipt?.actor || !reason.trim()) {
|
|
4176
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Navigation update requires the source claimant and an explicit reason.');
|
|
4177
|
+
}
|
|
4178
|
+
const preflight = this.navigationUpdateTransportPreflight(repository, taskId, tarball, receipt);
|
|
4179
|
+
if (!preflight.eligible || !preflight.proof) {
|
|
4180
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Navigation transport preflight failed.', { blockers: preflight.blockers });
|
|
4181
|
+
}
|
|
4182
|
+
// Recompute the source/state/Git bindings immediately before the atomic optimistic state write.
|
|
4183
|
+
const boundary = this.navigationUpdateBoundary(repository, taskId);
|
|
4184
|
+
const proof = inspectNavigationTransport(root, this.store, boundary.task, receipt, verifyNavigationUpdateArtifact(tarball, path.join(root, 'node_modules/codex-workflow-v2')), this.now());
|
|
4185
|
+
const saved = this.store.writeTask({ ...boundary.task, status: 'awaiting_execution_authorization',
|
|
4186
|
+
systemCommits: [...boundary.task.systemCommits, ...proof.commits.map(item => item.commitSha)],
|
|
4187
|
+
dependencyProvenanceRecoveries: [...(boundary.task.dependencyProvenanceRecoveries ?? []),
|
|
4188
|
+
...proof.commits.map(item => ({ ...item, packageName: PACKAGE_NAME,
|
|
4189
|
+
files: ['package-lock.json', 'package.json'],
|
|
4190
|
+
actor, reason: `${reason.trim()} [navigation-update ${receipt.artifact.sha256}]`, recordedAt: this.now().toISOString() }))],
|
|
4191
|
+
}, expectedRevision);
|
|
4192
|
+
validateTaskHistory(root, saved);
|
|
4193
|
+
return saved;
|
|
4194
|
+
}
|
|
4251
4195
|
dependencyProvenanceRecoveryPreflight(repository, taskId) {
|
|
4252
4196
|
const snapshot = this.observationSnapshot(repository);
|
|
4253
4197
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -4258,325 +4202,33 @@ export class WorkflowService {
|
|
|
4258
4202
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4259
4203
|
if (!task)
|
|
4260
4204
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4270
|
-
baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
|
|
4205
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4206
|
+
}
|
|
4207
|
+
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
4208
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4209
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
4210
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
|
|
4211
|
+
observation: snapshot.observation,
|
|
4212
|
+
});
|
|
4271
4213
|
}
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
...(preflight.dependencyCommits.length !== 1
|
|
4277
|
-
? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
|
|
4278
|
-
...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
|
|
4279
|
-
? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
|
|
4280
|
-
...(preflight.parentCommitSha
|
|
4281
|
-
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
|
|
4282
|
-
? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
|
|
4283
|
-
...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
|
|
4284
|
-
? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
|
|
4285
|
-
...(!baseDependency
|
|
4286
|
-
|| baseDependency.commitSha !== baseTip
|
|
4287
|
-
|| baseDependency.packageVersion !== PACKAGE_VERSION
|
|
4288
|
-
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
|
|
4289
|
-
!== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
|
|
4290
|
-
? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
|
|
4291
|
-
];
|
|
4292
|
-
return bridgeBlockers.length === 0
|
|
4293
|
-
? preflight
|
|
4294
|
-
: {
|
|
4295
|
-
...preflight,
|
|
4296
|
-
eligible: false,
|
|
4297
|
-
action: null,
|
|
4298
|
-
commitSha: null,
|
|
4299
|
-
parentCommitSha: null,
|
|
4300
|
-
blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
|
|
4301
|
-
};
|
|
4214
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4215
|
+
if (!task)
|
|
4216
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4217
|
+
return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
|
|
4302
4218
|
}
|
|
4303
|
-
|
|
4219
|
+
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
4304
4220
|
const snapshot = this.observationSnapshot(repository);
|
|
4221
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
4222
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
|
|
4223
|
+
observation: snapshot.observation,
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4305
4226
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4306
4227
|
if (!task)
|
|
4307
4228
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
const sourceSurfaces = {
|
|
4312
|
-
declared: versions.declared,
|
|
4313
|
-
locked: versions.locked,
|
|
4314
|
-
installed: versions.installed,
|
|
4315
|
-
currentBranch: versions.currentBranch,
|
|
4316
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4317
|
-
};
|
|
4318
|
-
const c1 = readTaskC1Posture(this.store, task);
|
|
4319
|
-
if (c1.state !== 'claimed') {
|
|
4320
|
-
return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
|
|
4321
|
-
}
|
|
4322
|
-
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4323
|
-
const now = this.now().getTime();
|
|
4324
|
-
const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
4325
|
-
const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4326
|
-
const transactionCount = snapshot.observation.pendingProjectTransactions.length
|
|
4327
|
-
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4328
|
-
+ snapshot.observation.pendingTaskTransactions.length;
|
|
4329
|
-
const authorizationBlockers = [];
|
|
4330
|
-
if (assessment.eligible) {
|
|
4331
|
-
try {
|
|
4332
|
-
this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
|
|
4333
|
-
}
|
|
4334
|
-
catch (error) {
|
|
4335
|
-
authorizationBlockers.push(error instanceof Error ? error.message : String(error));
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
const baseBlockers = [
|
|
4339
|
-
...assessment.blockers,
|
|
4340
|
-
...authorizationBlockers,
|
|
4341
|
-
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4342
|
-
...Object.entries(sourceSurfaces)
|
|
4343
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4344
|
-
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4345
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4346
|
-
...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
|
|
4347
|
-
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4348
|
-
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4349
|
-
...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
|
|
4350
|
-
? ['The exact original Worker C1 claim must remain current.'] : []),
|
|
4351
|
-
...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
|
|
4352
|
-
...(staleLeases.some((lease) => lease.entityId !== task.id
|
|
4353
|
-
|| c1.state !== 'claimed' || lease.owner !== c1.claimant
|
|
4354
|
-
|| createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
|
|
4355
|
-
? ['No unrelated or differently owned stale writer lease may exist.'] : []),
|
|
4356
|
-
...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
|
|
4357
|
-
];
|
|
4358
|
-
const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
|
|
4359
|
-
? staleLeases[0]
|
|
4360
|
-
: null;
|
|
4361
|
-
const blockers = [
|
|
4362
|
-
...baseBlockers,
|
|
4363
|
-
...(repairableStale
|
|
4364
|
-
? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
|
|
4365
|
-
: []),
|
|
4366
|
-
];
|
|
4367
|
-
const compatibility = assessment.compatibility;
|
|
4368
|
-
return {
|
|
4369
|
-
readOnly: true,
|
|
4370
|
-
eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
|
|
4371
|
-
action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
|
|
4372
|
-
? 'update corrective-carryover-transport'
|
|
4373
|
-
: null,
|
|
4374
|
-
projectId: task.projectId,
|
|
4375
|
-
taskId: task.id,
|
|
4376
|
-
taskRevision: task.revision,
|
|
4377
|
-
stepId: compatibility?.stepId ?? null,
|
|
4378
|
-
sourceVersion,
|
|
4379
|
-
targetVersion,
|
|
4380
|
-
sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
|
|
4381
|
-
taskBranch: task.taskBranch,
|
|
4382
|
-
baseBranch: task.baseBranch,
|
|
4383
|
-
dirtyFiles: compatibility?.dirtyFiles ?? [],
|
|
4384
|
-
dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
|
|
4385
|
-
compatibilityHash: compatibility?.compatibilityHash ?? null,
|
|
4386
|
-
c1Posture: c1.state === 'claimed' ? {
|
|
4387
|
-
state: 'claimed',
|
|
4388
|
-
claimant: c1.claimant,
|
|
4389
|
-
claimEventId: compatibility?.c1ClaimEventId ?? null,
|
|
4390
|
-
claimEventHash: compatibility?.c1ClaimEventHash ?? null,
|
|
4391
|
-
} : null,
|
|
4392
|
-
staleLeaseRepair: repairableStale ? [{
|
|
4393
|
-
action: 'locks repair',
|
|
4394
|
-
entityId: task.id,
|
|
4395
|
-
owner: repairableStale.owner,
|
|
4396
|
-
expiresAt: repairableStale.expiresAt,
|
|
4397
|
-
}] : [],
|
|
4398
|
-
requiredTransport: [
|
|
4399
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4400
|
-
'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
|
|
4401
|
-
'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
|
|
4402
|
-
],
|
|
4403
|
-
blockers,
|
|
4404
|
-
};
|
|
4405
|
-
}
|
|
4406
|
-
consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
|
|
4407
|
-
const repositoryRoot = snapshot.identity.repositoryRoot;
|
|
4408
|
-
const sourceVersion = '2.0.0-beta.13.6';
|
|
4409
|
-
const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
|
|
4410
|
-
const c1 = readTaskC1Posture(this.store, task);
|
|
4411
|
-
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
|
|
4412
|
-
const currentHead = headCommit(repositoryRoot);
|
|
4413
|
-
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
4414
|
-
const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
4415
|
-
const navigation = this.next(repositoryRoot, task.id);
|
|
4416
|
-
const action = String(navigation.action);
|
|
4417
|
-
const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
|
|
4418
|
-
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
4419
|
-
const terminal = readRemediationEvents(this.store, task)
|
|
4420
|
-
.filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
|
|
4421
|
-
const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
|
|
4422
|
-
const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
|
|
4423
|
-
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
|
|
4424
|
-
const missingClaimOnly = assessment.blockers.length === 1
|
|
4425
|
-
&& assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
|
|
4426
|
-
const rollingWave = navigation.rollingWave;
|
|
4427
|
-
const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
|
|
4428
|
-
&& rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
|
|
4429
|
-
|| (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
|
|
4430
|
-
|| (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
|
|
4431
|
-
&& c1.state === 'pending' && c1.targetActor === yielded?.actor);
|
|
4432
|
-
const blockers = [
|
|
4433
|
-
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4434
|
-
...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
|
|
4435
|
-
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4436
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4437
|
-
...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
|
|
4438
|
-
...(snapshot.observation.pendingProjectTransactions.length
|
|
4439
|
-
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4440
|
-
+ snapshot.observation.pendingTaskTransactions.length
|
|
4441
|
-
+ snapshot.observation.corruptTaskTransactions.length
|
|
4442
|
-
+ snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
|
|
4443
|
-
...(!recorded || recorded.stepId === stepId
|
|
4444
|
-
|| task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
|
|
4445
|
-
? ['A different, verified completed Step must own the historical carryover binding.'] : []),
|
|
4446
|
-
...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
|
|
4447
|
-
...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
|
|
4448
|
-
|| task.workspaceOwner !== 'local'
|
|
4449
|
-
|| currentBranch(repositoryRoot) !== task.taskBranch
|
|
4450
|
-
|| task.steps.some((candidate) => candidate.status === 'in_progress')
|
|
4451
|
-
? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
|
|
4452
|
-
...(!step || dirtyFiles.length === 0
|
|
4453
|
-
|| dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
|
|
4454
|
-
? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
|
|
4455
|
-
...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
|
|
4456
|
-
|| terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
|
|
4457
|
-
? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
|
|
4458
|
-
...(!yielded || !terminal || yielded.planHash === null
|
|
4459
|
-
|| !Number.isFinite(Date.parse(yielded.recordedAt))
|
|
4460
|
-
|| Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
|
|
4461
|
-
? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
|
|
4462
|
-
];
|
|
4463
|
-
let candidateBinding = null;
|
|
4464
|
-
if (action === 'task plan-set') {
|
|
4465
|
-
if (!candidate) {
|
|
4466
|
-
blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
|
|
4467
|
-
}
|
|
4468
|
-
else {
|
|
4469
|
-
try {
|
|
4470
|
-
validatePlan(candidate.plan);
|
|
4471
|
-
const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
|
|
4472
|
-
if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
|
|
4473
|
-
throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
|
|
4474
|
-
}
|
|
4475
|
-
const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
|
|
4476
|
-
if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
|
|
4477
|
-
throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
|
|
4478
|
-
}
|
|
4479
|
-
if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
|
|
4480
|
-
throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
|
|
4481
|
-
}
|
|
4482
|
-
if (candidate.plan.objective !== task.planObjective
|
|
4483
|
-
|| canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
|
|
4484
|
-
|| canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
|
|
4485
|
-
throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
|
|
4486
|
-
}
|
|
4487
|
-
const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
|
|
4488
|
-
this.validateKnowledgeTargets(map, candidate.plan);
|
|
4489
|
-
const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
|
|
4490
|
-
if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
|
|
4491
|
-
=== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
|
|
4492
|
-
throw new Error('Candidate must change the failed Plan semantics before source recovery.');
|
|
4493
|
-
}
|
|
4494
|
-
if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
|
|
4495
|
-
throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
|
|
4496
|
-
}
|
|
4497
|
-
const auditTask = {
|
|
4498
|
-
...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
|
|
4499
|
-
knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
|
|
4500
|
-
knowledgeMapHash: candidate.plan.knowledgeMapHash,
|
|
4501
|
-
steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
|
|
4502
|
-
...candidateStep, status: 'planned', evidence: null, failurePause: null,
|
|
4503
|
-
}),
|
|
4504
|
-
};
|
|
4505
|
-
validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
|
|
4506
|
-
candidateBinding = {
|
|
4507
|
-
candidatePlanHash: prepared.planHash,
|
|
4508
|
-
candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
|
|
4509
|
-
riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
|
|
4510
|
-
};
|
|
4511
|
-
}
|
|
4512
|
-
catch (error) {
|
|
4513
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4514
|
-
}
|
|
4515
|
-
}
|
|
4516
|
-
}
|
|
4517
|
-
try {
|
|
4518
|
-
validateTaskHistory(repositoryRoot, task);
|
|
4519
|
-
this.assertTaskKnowledgeBinding(repositoryRoot, task);
|
|
4520
|
-
const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
|
|
4521
|
-
if (!sourceAudit || !yielded?.planHash || !task.planHash
|
|
4522
|
-
|| normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
|
|
4523
|
-
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
|
|
4524
|
-
throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
|
|
4525
|
-
}
|
|
4526
|
-
}
|
|
4527
|
-
catch (error) {
|
|
4528
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4529
|
-
}
|
|
4530
|
-
const binding = {
|
|
4531
|
-
taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
|
|
4532
|
-
planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
|
|
4533
|
-
sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
|
|
4534
|
-
completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
|
|
4535
|
-
terminalRemediationEventHash: terminal?.eventHash ?? null,
|
|
4536
|
-
correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
|
|
4537
|
-
c1Posture: c1.state,
|
|
4538
|
-
candidateBinding,
|
|
4539
|
-
};
|
|
4540
|
-
return {
|
|
4541
|
-
readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
|
|
4542
|
-
action: null, transportAllowed: false,
|
|
4543
|
-
...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
|
|
4544
|
-
compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
|
|
4545
|
-
sourceRecovery: blockers.length === 0 ? {
|
|
4546
|
-
eligible: true, action,
|
|
4547
|
-
...(action === 'task plan-set' ? { ...candidateBinding } : {}),
|
|
4548
|
-
...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
|
|
4549
|
-
...(action === 'task claim' ? { actor: yielded.actor } : {}),
|
|
4550
|
-
instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
|
|
4551
|
-
} : null,
|
|
4552
|
-
blockers,
|
|
4553
|
-
};
|
|
4554
|
-
}
|
|
4555
|
-
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
4556
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4557
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4558
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
|
|
4559
|
-
observation: snapshot.observation,
|
|
4560
|
-
});
|
|
4561
|
-
}
|
|
4562
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4563
|
-
if (!task)
|
|
4564
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4565
|
-
return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
|
|
4566
|
-
}
|
|
4567
|
-
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
4568
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4569
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4570
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
|
|
4571
|
-
observation: snapshot.observation,
|
|
4572
|
-
});
|
|
4573
|
-
}
|
|
4574
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4575
|
-
if (!task)
|
|
4576
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4577
|
-
let candidateParent = null;
|
|
4578
|
-
try {
|
|
4579
|
-
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4229
|
+
let candidateParent = null;
|
|
4230
|
+
try {
|
|
4231
|
+
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4580
4232
|
}
|
|
4581
4233
|
catch {
|
|
4582
4234
|
candidateParent = null;
|
|
@@ -4621,7 +4273,7 @@ export class WorkflowService {
|
|
|
4621
4273
|
])],
|
|
4622
4274
|
};
|
|
4623
4275
|
}
|
|
4624
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null,
|
|
4276
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, {
|
|
4625
4277
|
activeStepId: assessment.activeStepId,
|
|
4626
4278
|
planHash: task.planHash,
|
|
4627
4279
|
dirtyFiles: assessment.dirtyFiles,
|
|
@@ -4720,289 +4372,15 @@ export class WorkflowService {
|
|
|
4720
4372
|
invalidatedStepIds: assessment.invalidatedSteps.map((step) => step.stepId),
|
|
4721
4373
|
});
|
|
4722
4374
|
}
|
|
4723
|
-
downstreamProofReplanUpdatePreflight(repository, taskId) {
|
|
4724
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4725
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4726
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan update preflight requires a coherent observation boundary.', {
|
|
4727
|
-
observation: snapshot.observation,
|
|
4728
|
-
});
|
|
4729
|
-
}
|
|
4730
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4731
|
-
if (!task)
|
|
4732
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4733
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
4734
|
-
const assessment = assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
4735
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4736
|
-
const sourceVersion = '2.0.0-beta.12.6';
|
|
4737
|
-
const versionSurfaces = {
|
|
4738
|
-
declared: versions.declared,
|
|
4739
|
-
locked: versions.locked,
|
|
4740
|
-
installed: versions.installed,
|
|
4741
|
-
currentBranch: versions.currentBranch,
|
|
4742
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4743
|
-
};
|
|
4744
|
-
const blockers = [
|
|
4745
|
-
...assessment.blockers,
|
|
4746
|
-
...(String(PACKAGE_VERSION) === sourceVersion ? ['Recovery runner must be newer than the beta.12.6 source package.'] : []),
|
|
4747
|
-
...Object.entries(versionSurfaces)
|
|
4748
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4749
|
-
.map(([surface]) => `${surface} Workflow version must equal the stranded source ${sourceVersion}.`),
|
|
4750
|
-
...(snapshot.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4751
|
-
...(snapshot.observation.pendingProjectTransactions.length > 0
|
|
4752
|
-
|| snapshot.observation.pendingMilestoneTransactions.length > 0
|
|
4753
|
-
|| snapshot.observation.pendingTaskTransactions.length > 0
|
|
4754
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
4755
|
-
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4756
|
-
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4757
|
-
];
|
|
4758
|
-
return {
|
|
4759
|
-
readOnly: true,
|
|
4760
|
-
eligible: assessment.applicable && assessment.eligible && blockers.length === 0,
|
|
4761
|
-
action: assessment.applicable && assessment.eligible && blockers.length === 0
|
|
4762
|
-
? 'update downstream-proof-replan-transport'
|
|
4763
|
-
: null,
|
|
4764
|
-
projectId: task.projectId,
|
|
4765
|
-
taskId: task.id,
|
|
4766
|
-
taskRevision: task.revision,
|
|
4767
|
-
sourceVersion,
|
|
4768
|
-
targetVersion: PACKAGE_VERSION,
|
|
4769
|
-
sourceHeadCommit: assessment.invalidation?.preservedHeadCommit ?? null,
|
|
4770
|
-
taskBranch: task.taskBranch,
|
|
4771
|
-
baseBranch: task.baseBranch,
|
|
4772
|
-
dirtyFiles: assessment.compatibility?.dirtyFiles ?? [],
|
|
4773
|
-
dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
|
|
4774
|
-
requiredTransport: [
|
|
4775
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4776
|
-
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
4777
|
-
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
4778
|
-
],
|
|
4779
|
-
blockers,
|
|
4780
|
-
};
|
|
4781
|
-
}
|
|
4782
|
-
planIntegrityUpdatePreflight(repository, taskId) {
|
|
4783
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4784
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4785
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
|
|
4786
|
-
observation: snapshot.observation,
|
|
4787
|
-
});
|
|
4788
|
-
}
|
|
4789
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4790
|
-
if (!task)
|
|
4791
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4792
|
-
const assessments = task.steps
|
|
4793
|
-
.filter((step) => step.status === 'failed')
|
|
4794
|
-
.map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
|
|
4795
|
-
.filter((assessment) => assessment.applicable);
|
|
4796
|
-
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4797
|
-
const adoptedCompatibility = assessment
|
|
4798
|
-
? null
|
|
4799
|
-
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4800
|
-
const dirtyCarryoverAssessment = assessment || adoptedCompatibility
|
|
4801
|
-
? null
|
|
4802
|
-
: this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4803
|
-
const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
|
|
4804
|
-
&& dirtyCarryoverAssessment.compatibility
|
|
4805
|
-
? {
|
|
4806
|
-
...dirtyCarryoverAssessment.compatibility,
|
|
4807
|
-
planHash: task.planHash,
|
|
4808
|
-
mode: 'post-rebind-dirty-carryover',
|
|
4809
|
-
}
|
|
4810
|
-
: null;
|
|
4811
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4812
|
-
const versionSurfaces = {
|
|
4813
|
-
declared: versions.declared,
|
|
4814
|
-
locked: versions.locked,
|
|
4815
|
-
installed: versions.installed,
|
|
4816
|
-
currentBranch: versions.currentBranch,
|
|
4817
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4818
|
-
};
|
|
4819
|
-
const sourceVersion = versionSurfaces.declared;
|
|
4820
|
-
const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
|
|
4821
|
-
const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
|
|
4822
|
-
const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
|
|
4823
|
-
&& sourceBetaVersion.patch !== null
|
|
4824
|
-
&& sourceBetaVersion.patch >= 11;
|
|
4825
|
-
const targetIsNewer = sourceBetaVersion !== null
|
|
4826
|
-
&& targetBetaVersion !== null
|
|
4827
|
-
&& (targetBetaVersion.train > sourceBetaVersion.train
|
|
4828
|
-
|| (targetBetaVersion.train === sourceBetaVersion.train
|
|
4829
|
-
&& targetBetaVersion.patch !== null
|
|
4830
|
-
&& sourceBetaVersion.patch !== null
|
|
4831
|
-
&& targetBetaVersion.patch > sourceBetaVersion.patch));
|
|
4832
|
-
const blockers = [
|
|
4833
|
-
...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
|
|
4834
|
-
...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
|
|
4835
|
-
? []
|
|
4836
|
-
: (dirtyCarryoverAssessment?.blockers.length
|
|
4837
|
-
? dirtyCarryoverAssessment.blockers
|
|
4838
|
-
: ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
|
|
4839
|
-
...(!sourceIsEligibleBeta12 || !targetIsNewer
|
|
4840
|
-
? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
|
|
4841
|
-
: []),
|
|
4842
|
-
...Object.entries(versionSurfaces)
|
|
4843
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4844
|
-
.map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
|
|
4845
|
-
...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4846
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4847
|
-
];
|
|
4848
|
-
return {
|
|
4849
|
-
readOnly: true,
|
|
4850
|
-
eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
|
|
4851
|
-
action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
|
|
4852
|
-
? 'update plan-integrity-transport'
|
|
4853
|
-
: null,
|
|
4854
|
-
projectId: task.projectId,
|
|
4855
|
-
taskId: task.id,
|
|
4856
|
-
taskRevision: task.revision,
|
|
4857
|
-
stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
|
|
4858
|
-
sourceVersion,
|
|
4859
|
-
targetVersion: PACKAGE_VERSION,
|
|
4860
|
-
taskBranch: task.taskBranch,
|
|
4861
|
-
baseBranch: task.baseBranch,
|
|
4862
|
-
dirtyFiles: assessment?.evidence?.changedFiles
|
|
4863
|
-
?? adoptedCompatibility?.dirtyFiles
|
|
4864
|
-
?? dirtyCarryoverCompatibility?.dirtyFiles
|
|
4865
|
-
?? [],
|
|
4866
|
-
conflictHash: assessment?.evidence?.conflictHash
|
|
4867
|
-
?? adoptedCompatibility?.conflictHash
|
|
4868
|
-
?? dirtyCarryoverCompatibility?.conflictHash
|
|
4869
|
-
?? null,
|
|
4870
|
-
compatibilityMode: adoptedCompatibility?.mode
|
|
4871
|
-
?? dirtyCarryoverCompatibility?.mode
|
|
4872
|
-
?? 'failed-step-conflict',
|
|
4873
|
-
requiredTransport: [
|
|
4874
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4875
|
-
'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
|
|
4876
|
-
'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
|
|
4877
|
-
],
|
|
4878
|
-
blockers,
|
|
4879
|
-
};
|
|
4880
|
-
}
|
|
4881
|
-
planIntegrityDependencyRecoveryPreflight(repository, taskId) {
|
|
4882
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4883
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4884
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
|
|
4885
|
-
observation: snapshot.observation,
|
|
4886
|
-
});
|
|
4887
|
-
}
|
|
4888
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4889
|
-
if (!task)
|
|
4890
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4891
|
-
const assessmentBlockers = [];
|
|
4892
|
-
const assessments = task.steps
|
|
4893
|
-
.filter((step) => step.status === 'failed')
|
|
4894
|
-
.flatMap((step) => {
|
|
4895
|
-
try {
|
|
4896
|
-
return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
|
|
4897
|
-
}
|
|
4898
|
-
catch (error) {
|
|
4899
|
-
if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
|
|
4900
|
-
assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
|
|
4901
|
-
return [];
|
|
4902
|
-
}
|
|
4903
|
-
throw error;
|
|
4904
|
-
}
|
|
4905
|
-
})
|
|
4906
|
-
.filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
|
|
4907
|
-
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4908
|
-
const adoptedCompatibility = assessment?.evidence
|
|
4909
|
-
? null
|
|
4910
|
-
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4911
|
-
const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
|
|
4912
|
-
? null
|
|
4913
|
-
: this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
|
|
4914
|
-
const carriedCompatibility = !assessment?.evidence && task.planHash
|
|
4915
|
-
? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
|
|
4916
|
-
: null;
|
|
4917
|
-
if ((!assessment?.evidence
|
|
4918
|
-
&& !carriedCompatibility
|
|
4919
|
-
&& !adoptedCompatibility
|
|
4920
|
-
&& !dirtyCarryoverCompatibility) || !task.planHash) {
|
|
4921
|
-
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4922
|
-
return {
|
|
4923
|
-
...ordinary,
|
|
4924
|
-
eligible: false,
|
|
4925
|
-
action: null,
|
|
4926
|
-
planIntegrity: null,
|
|
4927
|
-
blockers: [...new Set([
|
|
4928
|
-
...ordinary.blockers,
|
|
4929
|
-
...assessmentBlockers,
|
|
4930
|
-
'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
|
|
4931
|
-
])],
|
|
4932
|
-
};
|
|
4933
|
-
}
|
|
4934
|
-
const compatibility = carriedCompatibility
|
|
4935
|
-
?? adoptedCompatibility
|
|
4936
|
-
?? dirtyCarryoverCompatibility
|
|
4937
|
-
?? {
|
|
4938
|
-
stepId: assessment.stepId,
|
|
4939
|
-
planHash: task.planHash,
|
|
4940
|
-
conflictHash: assessment.evidence.conflictHash,
|
|
4941
|
-
dirtyFiles: [...assessment.evidence.changedFiles],
|
|
4942
|
-
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
|
|
4943
|
-
remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
4944
|
-
? [assessment.evidence.remediationEventId]
|
|
4945
|
-
: assessment.evidence.remediationEvents.map((event) => event.eventId),
|
|
4946
|
-
};
|
|
4947
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
|
|
4948
|
-
}
|
|
4949
|
-
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
4950
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4951
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4952
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan dependency recovery requires a coherent observation boundary.', {
|
|
4953
|
-
observation: snapshot.observation,
|
|
4954
|
-
});
|
|
4955
|
-
}
|
|
4956
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4957
|
-
if (!task)
|
|
4958
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4959
|
-
let candidateParent = null;
|
|
4960
|
-
try {
|
|
4961
|
-
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4962
|
-
}
|
|
4963
|
-
catch {
|
|
4964
|
-
candidateParent = null;
|
|
4965
|
-
}
|
|
4966
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
4967
|
-
const assessment = candidateParent
|
|
4968
|
-
? assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, {
|
|
4969
|
-
historyBoundary: 'HEAD^',
|
|
4970
|
-
preservedHeadCommit: candidateParent,
|
|
4971
|
-
currentPlanRiskAuditExists,
|
|
4972
|
-
})
|
|
4973
|
-
: null;
|
|
4974
|
-
if (!assessment?.applicable || !assessment.eligible || !assessment.compatibility) {
|
|
4975
|
-
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4976
|
-
return {
|
|
4977
|
-
...ordinary,
|
|
4978
|
-
eligible: false,
|
|
4979
|
-
action: null,
|
|
4980
|
-
activeDownstreamProof: null,
|
|
4981
|
-
downstreamProofReplan: null,
|
|
4982
|
-
blockers: [...new Set([
|
|
4983
|
-
...ordinary.blockers,
|
|
4984
|
-
...(assessment?.blockers ?? []),
|
|
4985
|
-
'The Task must preserve the exact beta.12.6 downstream-proof invalidation and accidental knowledge-rebind boundary.',
|
|
4986
|
-
])],
|
|
4987
|
-
};
|
|
4988
|
-
}
|
|
4989
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, assessment.compatibility);
|
|
4990
|
-
}
|
|
4991
4375
|
recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4992
4376
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, false);
|
|
4993
4377
|
}
|
|
4994
4378
|
recoverActiveDownstreamProofDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4995
4379
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, true);
|
|
4996
4380
|
}
|
|
4997
|
-
recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4998
|
-
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
|
|
4999
|
-
}
|
|
5000
4381
|
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
5001
4382
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
5002
4383
|
}
|
|
5003
|
-
recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
5004
|
-
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
|
|
5005
|
-
}
|
|
5006
4384
|
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
5007
4385
|
const normalizedActor = actor.trim();
|
|
5008
4386
|
const normalizedReason = reason.trim();
|
|
@@ -5015,15 +4393,9 @@ export class WorkflowService {
|
|
|
5015
4393
|
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
5016
4394
|
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead
|
|
5017
4395
|
&& record.packageVersion === PACKAGE_VERSION
|
|
5018
|
-
&& (recoveryMode === '
|
|
5019
|
-
? Boolean(record.
|
|
5020
|
-
:
|
|
5021
|
-
? Boolean(record.historicalStepProvenance)
|
|
5022
|
-
: recoveryMode === 'plan-integrity'
|
|
5023
|
-
? Boolean(record.planIntegrity)
|
|
5024
|
-
: recoveryMode === false && record.rootCauseReplan
|
|
5025
|
-
? true
|
|
5026
|
-
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
4396
|
+
&& (recoveryMode === 'historical-step'
|
|
4397
|
+
? Boolean(record.historicalStepProvenance)
|
|
4398
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
5027
4399
|
if (prior && current.systemCommits.includes(currentHead))
|
|
5028
4400
|
return current;
|
|
5029
4401
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -5031,34 +4403,17 @@ export class WorkflowService {
|
|
|
5031
4403
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
5032
4404
|
if (!task)
|
|
5033
4405
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
5034
|
-
const preflight = recoveryMode === '
|
|
5035
|
-
? this.
|
|
5036
|
-
: recoveryMode
|
|
5037
|
-
? this.
|
|
5038
|
-
:
|
|
5039
|
-
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
5040
|
-
: recoveryMode
|
|
5041
|
-
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
5042
|
-
: this.dependencyProvenanceRecoveryPreflight(repository, taskId);
|
|
4406
|
+
const preflight = recoveryMode === 'historical-step'
|
|
4407
|
+
? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
|
|
4408
|
+
: recoveryMode
|
|
4409
|
+
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4410
|
+
: this.dependencyProvenanceRecoveryPreflight(repository, taskId);
|
|
5043
4411
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
5044
4412
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
5045
4413
|
taskId,
|
|
5046
4414
|
blockers: preflight.blockers,
|
|
5047
4415
|
});
|
|
5048
4416
|
}
|
|
5049
|
-
if (preflight.rootCauseReplan) {
|
|
5050
|
-
const posture = readTaskC1Posture(this.store, task);
|
|
5051
|
-
if (posture.state !== 'claimed'
|
|
5052
|
-
|| posture.claimant !== preflight.rootCauseReplan.claimant
|
|
5053
|
-
|| normalizedActor !== posture.claimant) {
|
|
5054
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
|
|
5055
|
-
taskId,
|
|
5056
|
-
actor: normalizedActor,
|
|
5057
|
-
requiredActor: preflight.rootCauseReplan.claimant,
|
|
5058
|
-
posture: posture.state,
|
|
5059
|
-
});
|
|
5060
|
-
}
|
|
5061
|
-
}
|
|
5062
4417
|
const now = this.now();
|
|
5063
4418
|
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
5064
4419
|
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
@@ -5075,18 +4430,9 @@ export class WorkflowService {
|
|
|
5075
4430
|
...(isTerminalCommit && preflight.activeDownstreamProof
|
|
5076
4431
|
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
5077
4432
|
: {}),
|
|
5078
|
-
...(isTerminalCommit && preflight.downstreamProofReplan
|
|
5079
|
-
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
5080
|
-
: {}),
|
|
5081
4433
|
...(isTerminalCommit && preflight.historicalStepProvenance
|
|
5082
4434
|
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
5083
4435
|
: {}),
|
|
5084
|
-
...(isTerminalCommit && preflight.planIntegrity
|
|
5085
|
-
? { planIntegrity: preflight.planIntegrity }
|
|
5086
|
-
: {}),
|
|
5087
|
-
...(isTerminalCommit && preflight.rootCauseReplan
|
|
5088
|
-
? { rootCauseReplan: preflight.rootCauseReplan }
|
|
5089
|
-
: {}),
|
|
5090
4436
|
};
|
|
5091
4437
|
});
|
|
5092
4438
|
const nextTask = {
|
|
@@ -5102,62 +4448,7 @@ export class WorkflowService {
|
|
|
5102
4448
|
...recoveredRecords,
|
|
5103
4449
|
],
|
|
5104
4450
|
};
|
|
5105
|
-
|
|
5106
|
-
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
5107
|
-
}
|
|
5108
|
-
const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
|
|
5109
|
-
const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
|
|
5110
|
-
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
5111
|
-
let refreshedKnowledgeMap = null;
|
|
5112
|
-
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
5113
|
-
const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
|
|
5114
|
-
if (!knowledgeRefresh.eligible) {
|
|
5115
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
|
|
5116
|
-
taskId,
|
|
5117
|
-
dependencyKnowledgeRefresh: knowledgeRefresh,
|
|
5118
|
-
});
|
|
5119
|
-
}
|
|
5120
|
-
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
5121
|
-
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
5122
|
-
if (!approved.approval) {
|
|
5123
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
|
|
5124
|
-
}
|
|
5125
|
-
refreshedKnowledgeMap = {
|
|
5126
|
-
...approved,
|
|
5127
|
-
revision: currentKnowledgeMap.revision + 2,
|
|
5128
|
-
createdAt: currentKnowledgeMap.createdAt,
|
|
5129
|
-
updatedAt: now.toISOString(),
|
|
5130
|
-
approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
|
|
5131
|
-
};
|
|
5132
|
-
}
|
|
5133
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
5134
|
-
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
5135
|
-
applyProjectTransaction({
|
|
5136
|
-
projectRoot,
|
|
5137
|
-
projectId: task.projectId,
|
|
5138
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
5139
|
-
kind: 'plan-integrity-dependency-recovery',
|
|
5140
|
-
now,
|
|
5141
|
-
targets: [
|
|
5142
|
-
...(refreshedKnowledgeMap
|
|
5143
|
-
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
5144
|
-
: []),
|
|
5145
|
-
{
|
|
5146
|
-
path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
|
|
5147
|
-
content: `${JSON.stringify(nextTask, null, 2)}\n`,
|
|
5148
|
-
},
|
|
5149
|
-
],
|
|
5150
|
-
validate: () => {
|
|
5151
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
5152
|
-
if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
|
|
5153
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
|
|
5154
|
-
}
|
|
5155
|
-
if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
|
|
5156
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
|
|
5157
|
-
}
|
|
5158
|
-
},
|
|
5159
|
-
});
|
|
5160
|
-
return this.store.readTask(task.projectId, task.id);
|
|
4451
|
+
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
5161
4452
|
}
|
|
5162
4453
|
milestoneProgress(repository, milestoneId) {
|
|
5163
4454
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -5225,109 +4516,6 @@ export class WorkflowService {
|
|
|
5225
4516
|
tasks,
|
|
5226
4517
|
};
|
|
5227
4518
|
}
|
|
5228
|
-
alpha6StrictReviewRescuePreflight(repository) {
|
|
5229
|
-
const context = this.#mutationContext(repository);
|
|
5230
|
-
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
5231
|
-
const tasks = context.store.listTasks(context.identity.projectId);
|
|
5232
|
-
const leases = context.locks.list();
|
|
5233
|
-
const now = this.now().getTime();
|
|
5234
|
-
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
5235
|
-
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
5236
|
-
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
5237
|
-
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
5238
|
-
const pendingCandidates = [];
|
|
5239
|
-
const malformedCandidates = [];
|
|
5240
|
-
for (const task of tasks) {
|
|
5241
|
-
if (task.status !== 'in_progress')
|
|
5242
|
-
continue;
|
|
5243
|
-
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
5244
|
-
if (!planRiskAudit)
|
|
5245
|
-
continue;
|
|
5246
|
-
for (const step of task.steps) {
|
|
5247
|
-
if (step.status !== 'in_progress' || !step.evidence)
|
|
5248
|
-
continue;
|
|
5249
|
-
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
5250
|
-
continue;
|
|
5251
|
-
try {
|
|
5252
|
-
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
5253
|
-
if (cycle.resolution !== 'pending')
|
|
5254
|
-
continue;
|
|
5255
|
-
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
5256
|
-
let knowledgeBindingStale = false;
|
|
5257
|
-
try {
|
|
5258
|
-
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
5259
|
-
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
5260
|
-
knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
5261
|
-
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
5262
|
-
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
5263
|
-
}
|
|
5264
|
-
catch {
|
|
5265
|
-
knowledgeBindingStale = true;
|
|
5266
|
-
}
|
|
5267
|
-
pendingCandidates.push({
|
|
5268
|
-
taskId: task.id,
|
|
5269
|
-
taskRevision: task.revision,
|
|
5270
|
-
stepId: step.id,
|
|
5271
|
-
completionCommit: step.evidence.commitSha,
|
|
5272
|
-
currentHead,
|
|
5273
|
-
knowledgeBindingStale,
|
|
5274
|
-
});
|
|
5275
|
-
}
|
|
5276
|
-
catch (error) {
|
|
5277
|
-
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
}
|
|
5281
|
-
const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
|
|
5282
|
-
const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
|
|
5283
|
-
const blockers = [
|
|
5284
|
-
...(dependency.declared !== '2.0.0-alpha.6'
|
|
5285
|
-
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
5286
|
-
: []),
|
|
5287
|
-
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
5288
|
-
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
5289
|
-
...(pendingCandidates.length !== 1
|
|
5290
|
-
? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
|
|
5291
|
-
: []),
|
|
5292
|
-
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
5293
|
-
? ['HEAD does not match the pending strict review completion commit.']
|
|
5294
|
-
: []),
|
|
5295
|
-
...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
|
|
5296
|
-
? ['A stale writer lease exists outside the rescue Task.']
|
|
5297
|
-
: []),
|
|
5298
|
-
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
5299
|
-
];
|
|
5300
|
-
const eligible = blockers.length === 0 && candidate !== null;
|
|
5301
|
-
const requiredActions = eligible
|
|
5302
|
-
? [
|
|
5303
|
-
...staleLeases.map((lease) => ({
|
|
5304
|
-
action: 'locks repair',
|
|
5305
|
-
entityId: lease.entityId,
|
|
5306
|
-
})),
|
|
5307
|
-
{
|
|
5308
|
-
action: 'task step-review',
|
|
5309
|
-
taskId: candidate.taskId,
|
|
5310
|
-
stepId: candidate.stepId,
|
|
5311
|
-
expectedRevision: candidate.taskRevision,
|
|
5312
|
-
},
|
|
5313
|
-
]
|
|
5314
|
-
: [];
|
|
5315
|
-
return {
|
|
5316
|
-
eligible,
|
|
5317
|
-
mutationFree: true,
|
|
5318
|
-
runnerPackageVersion: PACKAGE_VERSION,
|
|
5319
|
-
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
5320
|
-
declaredProjectPackageVersion: dependency.declared,
|
|
5321
|
-
projectId: context.identity.projectId,
|
|
5322
|
-
repositoryRoot: context.identity.repositoryRoot,
|
|
5323
|
-
clean: dirty.length === 0,
|
|
5324
|
-
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
5325
|
-
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
5326
|
-
candidate,
|
|
5327
|
-
blockers,
|
|
5328
|
-
requiredActions,
|
|
5329
|
-
};
|
|
5330
|
-
}
|
|
5331
4519
|
scanKnowledgeMap(repository) {
|
|
5332
4520
|
const { identity } = this.#mutationContext(repository);
|
|
5333
4521
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
@@ -5394,10 +4582,13 @@ export class WorkflowService {
|
|
|
5394
4582
|
return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
5395
4583
|
}
|
|
5396
4584
|
graphStatus(repository, graphKind, mode = 'moderate') {
|
|
5397
|
-
|
|
4585
|
+
// Graph diagnostics must not register a virgin project or implicitly recover journals.
|
|
4586
|
+
// Surface the same structural boundary as status/next without loading or mutating Tasks.
|
|
4587
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
4588
|
+
const { observation } = inspectWorkflowObservationBoundary({ store: this.store, identity, now: this.now() });
|
|
5398
4589
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
5399
4590
|
const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
5400
|
-
return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
|
|
4591
|
+
return { request, binding: binding ? inspectGraphBinding(binding, request) : null, observation };
|
|
5401
4592
|
}
|
|
5402
4593
|
bindGraph(repository, expectedRevision, request, evidence) {
|
|
5403
4594
|
const { identity } = this.#mutationContext(repository);
|
|
@@ -5426,10 +4617,25 @@ export class WorkflowService {
|
|
|
5426
4617
|
return explainChange(input, repository, effectiveTask, navigation);
|
|
5427
4618
|
}
|
|
5428
4619
|
next(repository, taskId = null) {
|
|
5429
|
-
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
4620
|
+
return withWorkflowBlockerRoute(withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository)))), PACKAGE_VERSION);
|
|
5430
4621
|
}
|
|
5431
4622
|
nextFromStatus(repository, taskId, status) {
|
|
5432
4623
|
if (status.observation.kind === 'blocked') {
|
|
4624
|
+
// Observation stays write-free. Only an absent or proven virgin registration
|
|
4625
|
+
// may resume initialization; retained or unknown state remains blocked.
|
|
4626
|
+
if (taskId === null && status.observation.missingRegistration
|
|
4627
|
+
&& isVirginRegistrationRoot(this.store.projectRoot(status.projectId), this.store.root, resolveRepositoryIdentity(repository), PACKAGE_VERSION)
|
|
4628
|
+
&& status.observation.reasonCodes.every(code => code === 'PROJECT_REGISTRATION_MISSING' || code === 'ADOPTION_MISSING')) {
|
|
4629
|
+
return {
|
|
4630
|
+
scope: 'project', projectId: status.projectId,
|
|
4631
|
+
status: 'initialization-required', action: 'project-memory scan',
|
|
4632
|
+
observation: status.observation,
|
|
4633
|
+
navigation: {
|
|
4634
|
+
kind: 'executable', command: 'project-memory scan', cataloged: false,
|
|
4635
|
+
bindings: { projectId: status.projectId },
|
|
4636
|
+
},
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
5433
4639
|
return {
|
|
5434
4640
|
scope: 'project',
|
|
5435
4641
|
projectId: status.projectId,
|
|
@@ -5437,7 +4643,16 @@ export class WorkflowService {
|
|
|
5437
4643
|
action: 'blocked',
|
|
5438
4644
|
observation: status.observation,
|
|
5439
4645
|
navigation: {
|
|
5440
|
-
kind:
|
|
4646
|
+
kind: status.observation.pendingProjectTransactions.some(transaction => transaction.ownerActive)
|
|
4647
|
+
|| status.observation.reasonCodes.includes('TASK_OPERATION_IN_PROGRESS') ? 'wait' : 'blocked',
|
|
4648
|
+
...(status.observation.pendingProjectTransactions.some(transaction => transaction.ownerActive) ? {
|
|
4649
|
+
reason: 'project-transaction-in-progress',
|
|
4650
|
+
waitFor: 'The current transaction owner must finish or exit before recovery; obtain fresh status and next afterward.',
|
|
4651
|
+
} : status.observation.reasonCodes.includes('TASK_OPERATION_IN_PROGRESS') ? {
|
|
4652
|
+
reason: 'core-operation-in-progress',
|
|
4653
|
+
waitFor: 'The active Core operation must finish or its lease expire; obtain fresh status and next afterward.',
|
|
4654
|
+
operations: status.observation.coreTaskOperations,
|
|
4655
|
+
} : {}),
|
|
5441
4656
|
reasonCodes: status.observation.reasonCodes,
|
|
5442
4657
|
availableRepairTransitionId: null,
|
|
5443
4658
|
},
|
|
@@ -5574,6 +4789,46 @@ export class WorkflowService {
|
|
|
5574
4789
|
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
5575
4790
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
5576
4791
|
const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
|
|
4792
|
+
const incompleteScope = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status)
|
|
4793
|
+
&& (taskId === null || tasksById.get(taskId)?.milestoneId === milestone.id)
|
|
4794
|
+
&& !assessMilestoneInitialAssembly({ store: this.store, projectId: status.projectId, milestone,
|
|
4795
|
+
listTasks: () => status.tasks }).eligible
|
|
4796
|
+
&& assessMilestoneScopeCompletion(milestone, status.tasks).length > 0);
|
|
4797
|
+
if (incompleteScope) {
|
|
4798
|
+
const lease = this.inspectWriterLease(repositoryRoot, incompleteScope.id);
|
|
4799
|
+
if (lease)
|
|
4800
|
+
return withAdoption({ scope: 'milestone', id: incompleteScope.id, entityId: incompleteScope.id,
|
|
4801
|
+
revision: incompleteScope.revision, action: lease.stale ? 'locks repair' : 'blocked', lease,
|
|
4802
|
+
navigation: { kind: lease.stale ? 'repair' : 'wait', reason: 'milestone-authority-lease' } });
|
|
4803
|
+
if (incompleteScope.status === 'awaiting_final_acceptance')
|
|
4804
|
+
return withAdoption({
|
|
4805
|
+
scope: 'milestone', id: incompleteScope.id, revision: incompleteScope.revision, action: 'blocked',
|
|
4806
|
+
navigation: { kind: 'blocked', reason: 'retained-final-scope-gap' },
|
|
4807
|
+
reason: 'Retained final acceptance state contains unclassified linked Tasks; no safe scope transition is available.',
|
|
4808
|
+
scopeCompletion: { orphanTaskIds: assessMilestoneScopeCompletion(incompleteScope, status.tasks), executionAvailable: false },
|
|
4809
|
+
});
|
|
4810
|
+
const event = readMilestoneAutonomyEvents(this.store, status.projectId, incompleteScope.id).at(-1);
|
|
4811
|
+
let autonomous = null;
|
|
4812
|
+
if (event) {
|
|
4813
|
+
try {
|
|
4814
|
+
const authority = requireActiveMilestoneAutonomy({ store: this.store, projectId: status.projectId,
|
|
4815
|
+
milestone: incompleteScope, actor: event.delegate, now: this.now() });
|
|
4816
|
+
autonomous = { actor: event.delegate, grantId: authority.grant.id };
|
|
4817
|
+
}
|
|
4818
|
+
catch (error) {
|
|
4819
|
+
if (!(error instanceof WorkflowError) || !['TRANSITION_BLOCKED', 'NOT_FOUND'].includes(error.code))
|
|
4820
|
+
throw error;
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
return withAdoption({ scope: 'milestone', id: incompleteScope.id, status: incompleteScope.status,
|
|
4824
|
+
revision: incompleteScope.revision, action: autonomous ? 'milestone autonomy-evolve' : 'milestone scope-change-prepare',
|
|
4825
|
+
scopeCompletion: { orphanTaskIds: assessMilestoneScopeCompletion(incompleteScope, status.tasks),
|
|
4826
|
+
requiresCandidatePlan: true, executionAvailable: false, preservesExistingScopeAndDependencies: true },
|
|
4827
|
+
...(autonomous ? { autonomy: autonomous } : { scopeChangeBoundary: { state: 'human-decision-required',
|
|
4828
|
+
requiresCandidatePlan: true, prepareAction: 'milestone scope-change-prepare', applyAction: 'milestone scope-change-apply',
|
|
4829
|
+
requiresExplicitHumanConfirmation: true, confirmationCodeSource: 'scope-change-prepare-result',
|
|
4830
|
+
afterPreparation: 'await-human-confirmation-before-apply', applyRequiresSamePlanAndActor: true } }) });
|
|
4831
|
+
}
|
|
5577
4832
|
const requestedTask = taskId === null
|
|
5578
4833
|
? null
|
|
5579
4834
|
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
@@ -5615,23 +4870,27 @@ export class WorkflowService {
|
|
|
5615
4870
|
? [task]
|
|
5616
4871
|
: [];
|
|
5617
4872
|
}).at(0) ?? null;
|
|
4873
|
+
let prioritizedRefreshTask = null;
|
|
4874
|
+
if (!requestedTask) {
|
|
4875
|
+
let approved = null;
|
|
4876
|
+
try {
|
|
4877
|
+
approved = this.store.readKnowledgeMap(status.projectId);
|
|
4878
|
+
}
|
|
4879
|
+
catch (error) {
|
|
4880
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
4881
|
+
throw error;
|
|
4882
|
+
}
|
|
4883
|
+
if (approved?.status === 'active' && approved.approval) {
|
|
4884
|
+
const inspected = inspectCurrentKnowledgeMap();
|
|
4885
|
+
if (inspected.status === 'stale')
|
|
4886
|
+
prioritizedRefreshTask = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, approved, inspected)?.task ?? null;
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
5618
4889
|
const activeTask = requestedTask
|
|
4890
|
+
?? prioritizedRefreshTask
|
|
5619
4891
|
?? prioritizedRemediationTask
|
|
5620
4892
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
5621
4893
|
if (activeTask) {
|
|
5622
|
-
let pendingReviewDependencyRecovery = null;
|
|
5623
|
-
try {
|
|
5624
|
-
const reconstructed = pendingReviewSourceBinding(repositoryRoot, this.store, activeTask, runGit(repositoryRoot, ['rev-parse', 'HEAD^']), runGit(repositoryRoot, ['rev-parse', `${activeTask.baseBranch}^`]));
|
|
5625
|
-
pendingReviewDependencyRecovery = this.pendingReviewDependencyPreflight(repositoryRoot, activeTask.id, reconstructed);
|
|
5626
|
-
}
|
|
5627
|
-
catch { /* A nonmatching shape follows ordinary navigation without a compatibility bypass. */ }
|
|
5628
|
-
if (pendingReviewDependencyRecovery?.eligible) {
|
|
5629
|
-
return withAdoption({ scope: 'task', id: activeTask.id, status: activeTask.status,
|
|
5630
|
-
action: 'update pending-review-dependency-recover', revision: activeTask.revision,
|
|
5631
|
-
pendingReviewDependencyRecovery,
|
|
5632
|
-
sourceReceiptRequired: true,
|
|
5633
|
-
});
|
|
5634
|
-
}
|
|
5635
4894
|
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5636
4895
|
if (historicalStepProvenanceRecovery.eligible) {
|
|
5637
4896
|
return withAdoption({
|
|
@@ -5659,22 +4918,6 @@ export class WorkflowService {
|
|
|
5659
4918
|
},
|
|
5660
4919
|
});
|
|
5661
4920
|
}
|
|
5662
|
-
const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5663
|
-
if (planIntegrityDependencyRecovery.eligible) {
|
|
5664
|
-
return withAdoption({
|
|
5665
|
-
scope: 'task',
|
|
5666
|
-
id: activeTask.id,
|
|
5667
|
-
status: activeTask.status,
|
|
5668
|
-
action: 'update plan-integrity-dependency-recover',
|
|
5669
|
-
revision: activeTask.revision,
|
|
5670
|
-
dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
|
|
5671
|
-
compatibilityBoundary: {
|
|
5672
|
-
preservesWorktree: true,
|
|
5673
|
-
prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
|
|
5674
|
-
continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
|
|
5675
|
-
},
|
|
5676
|
-
});
|
|
5677
|
-
}
|
|
5678
4921
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5679
4922
|
if (dependencyRecovery.eligible) {
|
|
5680
4923
|
return withAdoption({
|
|
@@ -5686,6 +4929,18 @@ export class WorkflowService {
|
|
|
5686
4929
|
dependencyProvenanceRecovery: dependencyRecovery,
|
|
5687
4930
|
});
|
|
5688
4931
|
}
|
|
4932
|
+
const navigationSourceHead = activeTask.authorizations.filter(item => item.kind === 'execution' && item.decision === 'approved').at(-1)?.headCommit;
|
|
4933
|
+
if (activeTask.status === 'ready' && navigationSourceHead
|
|
4934
|
+
&& activeTask.dependencyProvenanceRecoveries?.some(item => item.planIntegrity?.mode === 'navigation-fix-dirty-carryover')) {
|
|
4935
|
+
const transport = inspectBoundedWorkflowDependencyCommit(repositoryRoot, headCommit(repositoryRoot));
|
|
4936
|
+
if (transport?.packageVersion === PACKAGE_VERSION && transport.parentCommitSha === navigationSourceHead) {
|
|
4937
|
+
return withAdoption({ scope: 'task', id: activeTask.id, revision: activeTask.revision,
|
|
4938
|
+
action: 'update navigation-transport-preflight',
|
|
4939
|
+
requiredInputs: ['--tarball <exact target artifact>', '--file <source receipt>'],
|
|
4940
|
+
reason: 'Verify the preserved source receipt before registering navigation dependency transport. Missing evidence blocks recovery.',
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
5689
4944
|
const activeDownstreamProofDependencyRecovery = this.activeDownstreamProofDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5690
4945
|
if (activeDownstreamProofDependencyRecovery.eligible) {
|
|
5691
4946
|
return withAdoption({
|
|
@@ -5702,22 +4957,6 @@ export class WorkflowService {
|
|
|
5702
4957
|
},
|
|
5703
4958
|
});
|
|
5704
4959
|
}
|
|
5705
|
-
const downstreamProofReplanDependencyRecovery = this.downstreamProofReplanDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5706
|
-
if (downstreamProofReplanDependencyRecovery.eligible) {
|
|
5707
|
-
return withAdoption({
|
|
5708
|
-
scope: 'task',
|
|
5709
|
-
id: activeTask.id,
|
|
5710
|
-
status: activeTask.status,
|
|
5711
|
-
action: 'update downstream-proof-replan-dependency-recover',
|
|
5712
|
-
revision: activeTask.revision,
|
|
5713
|
-
dependencyProvenanceRecovery: downstreamProofReplanDependencyRecovery,
|
|
5714
|
-
compatibilityBoundary: {
|
|
5715
|
-
preservesWorktree: true,
|
|
5716
|
-
prerequisite: 'A newer package preflight approved the exact beta.12.6 stranded replan transport before dependency-only commits were created.',
|
|
5717
|
-
continuation: 'Register this dependency commit, then run task downstream-proof-replan-recover.',
|
|
5718
|
-
},
|
|
5719
|
-
});
|
|
5720
|
-
}
|
|
5721
4960
|
const activeTaskMilestone = activeTask.milestoneId
|
|
5722
4961
|
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
5723
4962
|
: null;
|
|
@@ -5741,54 +4980,6 @@ export class WorkflowService {
|
|
|
5741
4980
|
},
|
|
5742
4981
|
}
|
|
5743
4982
|
: value);
|
|
5744
|
-
const currentPlanRiskAuditExists = activeTask.planHash
|
|
5745
|
-
? readPlanRiskAuditEvents(this.store, activeTask).some((event) => event.taskRevision === activeTask.revision && event.planHash === activeTask.planHash)
|
|
5746
|
-
: false;
|
|
5747
|
-
const downstreamReplanRecovery = assessDownstreamProofReplanRecovery(this.store, repositoryRoot, activeTask, { currentPlanRiskAuditExists });
|
|
5748
|
-
if (downstreamReplanRecovery.applicable) {
|
|
5749
|
-
const environmentBlockers = [
|
|
5750
|
-
...(!downstreamReplanRecovery.dependencyRecoveryCommit
|
|
5751
|
-
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
5752
|
-
: []),
|
|
5753
|
-
...(status.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
5754
|
-
...(status.observation.pendingProjectTransactions.length > 0
|
|
5755
|
-
|| status.observation.pendingMilestoneTransactions.length > 0
|
|
5756
|
-
|| status.observation.pendingTaskTransactions.length > 0
|
|
5757
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
5758
|
-
...(status.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
5759
|
-
...(status.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
5760
|
-
];
|
|
5761
|
-
return withAdoption(withMilestoneCancellation(downstreamReplanRecovery.eligible && environmentBlockers.length === 0
|
|
5762
|
-
? {
|
|
5763
|
-
scope: 'task',
|
|
5764
|
-
id: activeTask.id,
|
|
5765
|
-
status: activeTask.status,
|
|
5766
|
-
action: 'task downstream-proof-replan-recover',
|
|
5767
|
-
revision: activeTask.revision,
|
|
5768
|
-
downstreamProofReplanRecovery: {
|
|
5769
|
-
state: 'eligible',
|
|
5770
|
-
invalidationEventId: downstreamReplanRecovery.invalidation.eventId,
|
|
5771
|
-
sourcePlanHash: downstreamReplanRecovery.invalidation.planHash,
|
|
5772
|
-
reboundPlanHash: activeTask.planHash,
|
|
5773
|
-
dirtyFiles: downstreamReplanRecovery.compatibility.dirtyFiles,
|
|
5774
|
-
dirtyWorktreeHash: downstreamReplanRecovery.compatibility.dirtyWorktreeHash,
|
|
5775
|
-
dependencyRecoveryCommit: downstreamReplanRecovery.dependencyRecoveryCommit,
|
|
5776
|
-
boundedEffect: 'restore-needs-fix-and-return-same-task-to-plan-set',
|
|
5777
|
-
},
|
|
5778
|
-
}
|
|
5779
|
-
: {
|
|
5780
|
-
scope: 'task',
|
|
5781
|
-
id: activeTask.id,
|
|
5782
|
-
status: activeTask.status,
|
|
5783
|
-
action: 'doctor',
|
|
5784
|
-
blockedAction: 'task downstream-proof-replan-recover',
|
|
5785
|
-
revision: activeTask.revision,
|
|
5786
|
-
downstreamProofReplanRecovery: {
|
|
5787
|
-
state: 'blocked',
|
|
5788
|
-
blockers: [...downstreamReplanRecovery.blockers, ...environmentBlockers],
|
|
5789
|
-
},
|
|
5790
|
-
}));
|
|
5791
|
-
}
|
|
5792
4983
|
let activePlanRiskAudit = null;
|
|
5793
4984
|
let planRiskAuditRecovery = null;
|
|
5794
4985
|
const requiresReplacementPlan = activeTask.status === 'needs_fix'
|
|
@@ -6004,9 +5195,8 @@ export class WorkflowService {
|
|
|
6004
5195
|
if (taskNext?.action === 'task step-complete'
|
|
6005
5196
|
|| taskNext?.action === 'task step-review'
|
|
6006
5197
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
6007
|
-
|| taskNext?.action === 'task remediation-mode-recover'
|
|
6008
5198
|
|| (taskNext?.action === 'task writer-credential-replace'
|
|
6009
|
-
&& ['task step-complete', 'task step-review'
|
|
5199
|
+
&& ['task step-complete', 'task step-review'].includes(String(taskNext?.blockedAction)))
|
|
6010
5200
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
6011
5201
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
6012
5202
|
|| taskNext?.action === 'task corrective-yield'
|
|
@@ -6082,6 +5272,7 @@ export class WorkflowService {
|
|
|
6082
5272
|
contextRefresh: {
|
|
6083
5273
|
action: 'task context-refresh',
|
|
6084
5274
|
mode: refreshAssessment.mode,
|
|
5275
|
+
authority: readFailedStepPlanningRecovery(this.store, activeTask) ? 'planning-only' : 'execution',
|
|
6085
5276
|
addedSources: refreshAssessment.addedSources,
|
|
6086
5277
|
options: contextRefreshOptions,
|
|
6087
5278
|
},
|
|
@@ -6186,6 +5377,24 @@ export class WorkflowService {
|
|
|
6186
5377
|
milestoneInitialAssembly: initialAssembly,
|
|
6187
5378
|
});
|
|
6188
5379
|
}
|
|
5380
|
+
// Only this Milestone's authority lease gates its own transitions.
|
|
5381
|
+
// Keep this outside validation admission: the executor already owns its lease there.
|
|
5382
|
+
const milestoneLease = this.inspectWriterLease(repositoryRoot, activeMilestone.id);
|
|
5383
|
+
if (milestoneLease) {
|
|
5384
|
+
return withAdoption({
|
|
5385
|
+
scope: 'milestone',
|
|
5386
|
+
id: activeMilestone.id,
|
|
5387
|
+
entityId: activeMilestone.id,
|
|
5388
|
+
status: activeMilestone.status,
|
|
5389
|
+
action: milestoneLease.stale ? 'locks repair' : 'blocked',
|
|
5390
|
+
...(!milestoneLease.stale ? { navigation: { kind: 'wait', reason: 'active-milestone-writer-lease' } } : {}),
|
|
5391
|
+
revision: activeMilestone.revision,
|
|
5392
|
+
lease: milestoneLease,
|
|
5393
|
+
reason: milestoneLease.stale
|
|
5394
|
+
? 'The expired Milestone authority lease requires explicit repair before another transition.'
|
|
5395
|
+
: 'Another writer owns this Milestone; wait for that operation to release its lease.',
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
6189
5398
|
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
6190
5399
|
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
6191
5400
|
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
@@ -6227,6 +5436,33 @@ export class WorkflowService {
|
|
|
6227
5436
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6228
5437
|
});
|
|
6229
5438
|
}
|
|
5439
|
+
let milestoneAction = nextForMilestone(activeMilestone);
|
|
5440
|
+
let validationBlock = null;
|
|
5441
|
+
if (activeMilestone.status === 'active') {
|
|
5442
|
+
try {
|
|
5443
|
+
this.assertMilestoneValidationReady(repositoryRoot, activeMilestone);
|
|
5444
|
+
milestoneAction = 'milestone validate';
|
|
5445
|
+
}
|
|
5446
|
+
catch (error) {
|
|
5447
|
+
if (!(error instanceof WorkflowError))
|
|
5448
|
+
throw error;
|
|
5449
|
+
if (error.code === 'GIT_PRECONDITION_FAILED') {
|
|
5450
|
+
milestoneAction = 'restore clean base, then milestone validate';
|
|
5451
|
+
validationBlock = { blockedAction: 'milestone validate', reason: error.message };
|
|
5452
|
+
}
|
|
5453
|
+
else if (error.code === 'TRANSITION_BLOCKED' && Array.isArray(error.details.taskIds)) {
|
|
5454
|
+
validationBlock = { blockedAction: 'milestone validate', incompleteTaskIds: error.details.taskIds };
|
|
5455
|
+
}
|
|
5456
|
+
else if (error.code === 'TRANSITION_BLOCKED') {
|
|
5457
|
+
milestoneAction = 'doctor';
|
|
5458
|
+
validationBlock = { blockedAction: 'milestone validate', reason: error.message };
|
|
5459
|
+
}
|
|
5460
|
+
else
|
|
5461
|
+
throw error;
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
if (activeMilestone.status === 'blocked')
|
|
5465
|
+
milestoneAction = 'milestone scope-change-prepare';
|
|
6230
5466
|
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
6231
5467
|
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
6232
5468
|
: [];
|
|
@@ -6234,7 +5470,24 @@ export class WorkflowService {
|
|
|
6234
5470
|
scope: 'milestone',
|
|
6235
5471
|
id: activeMilestone.id,
|
|
6236
5472
|
status: activeMilestone.status,
|
|
6237
|
-
action:
|
|
5473
|
+
action: milestoneAction,
|
|
5474
|
+
...(validationBlock ?? {}),
|
|
5475
|
+
...(activeMilestone.status === 'blocked' ? {
|
|
5476
|
+
options: [
|
|
5477
|
+
{ action: 'milestone scope-change-prepare', eligible: true, requiresCandidatePlan: true },
|
|
5478
|
+
{ action: 'milestone cancel', eligible: true, requiresExplicitCancellationIntent: true },
|
|
5479
|
+
],
|
|
5480
|
+
scopeChangeBoundary: {
|
|
5481
|
+
state: 'human-decision-required',
|
|
5482
|
+
requiresCandidatePlan: true,
|
|
5483
|
+
prepareAction: 'milestone scope-change-prepare',
|
|
5484
|
+
applyAction: 'milestone scope-change-apply',
|
|
5485
|
+
requiresExplicitHumanConfirmation: true,
|
|
5486
|
+
confirmationCodeSource: 'scope-change-prepare-result',
|
|
5487
|
+
afterPreparation: 'await-human-confirmation-before-apply',
|
|
5488
|
+
applyRequiresSamePlanAndActor: true,
|
|
5489
|
+
},
|
|
5490
|
+
} : {}),
|
|
6238
5491
|
revision: activeMilestone.revision,
|
|
6239
5492
|
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
6240
5493
|
? {
|
|
@@ -6438,6 +5691,8 @@ export class WorkflowService {
|
|
|
6438
5691
|
return { milestone, authorization, scopeChange };
|
|
6439
5692
|
}
|
|
6440
5693
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
5694
|
+
if (task.pendingBaseSyncValidation !== undefined)
|
|
5695
|
+
return this.taskGitNavigationOverride(task);
|
|
6441
5696
|
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
6442
5697
|
// A stopped rebound Plan can expose planning recovery, never an approval route.
|
|
6443
5698
|
const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
|
|
@@ -6454,49 +5709,44 @@ export class WorkflowService {
|
|
|
6454
5709
|
const correctiveDerivedAuthority = transition && correctiveDerivedActor
|
|
6455
5710
|
? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
|
|
6456
5711
|
: null;
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
if (planRiskAudit) {
|
|
6460
|
-
try {
|
|
6461
|
-
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
6462
|
-
}
|
|
6463
|
-
catch (error) {
|
|
6464
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
6465
|
-
if (!remediationModeRecovery)
|
|
6466
|
-
throw error;
|
|
6467
|
-
}
|
|
6468
|
-
}
|
|
6469
|
-
let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
|
|
6470
|
-
? guardedCorrectivePosture
|
|
5712
|
+
const guardedCorrectivePosture = planRiskAudit
|
|
5713
|
+
? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
|
|
6471
5714
|
: null;
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
catch (error) {
|
|
6477
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
6478
|
-
if (!remediationModeRecovery)
|
|
6479
|
-
throw error;
|
|
6480
|
-
}
|
|
5715
|
+
let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
|
|
5716
|
+
? guardedCorrectivePosture : null;
|
|
5717
|
+
if (planRiskAudit && guardedCorrectivePosture === null) {
|
|
5718
|
+
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
6481
5719
|
}
|
|
6482
5720
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
6483
5721
|
const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
6484
5722
|
const rootCauseStepId = nextStepId
|
|
6485
5723
|
?? task.steps.find((candidate) => candidate.status === 'planned')?.id
|
|
6486
5724
|
?? null;
|
|
5725
|
+
const hasDirtyFiles = changedFiles(repositoryRoot).length > 0;
|
|
5726
|
+
const nextStartPhase = stepStartPhase(task.steps.find(step => step.id === nextStepId)?.status, hasDirtyFiles);
|
|
5727
|
+
const rootCauseStartPhase = stepStartPhase(task.steps.find(step => step.id === rootCauseStepId)?.status, hasDirtyFiles);
|
|
6487
5728
|
const downstreamProofCarryover = next.action === 'task run'
|
|
6488
5729
|
&& nextStepId
|
|
6489
|
-
&&
|
|
5730
|
+
&& nextStartPhase === 'planned-dirty'
|
|
6490
5731
|
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
6491
5732
|
: null;
|
|
6492
5733
|
const planIntegrityDirtyCarryover = next.action === 'task run'
|
|
6493
5734
|
&& nextStepId
|
|
6494
|
-
&&
|
|
5735
|
+
&& nextStartPhase === 'planned-dirty'
|
|
6495
5736
|
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
6496
5737
|
: null;
|
|
6497
|
-
|
|
5738
|
+
const navigationDirtyCarryover = next.action === 'task run' && rootCauseStartPhase === 'planned-dirty' && rootCauseStepId
|
|
5739
|
+
? assessNavigationDirtyCarryover(this.store, repositoryRoot, task, rootCauseStepId)
|
|
5740
|
+
: null;
|
|
5741
|
+
const navigationDirtyOverlay = navigationDirtyCarryover?.applicable
|
|
5742
|
+
? navigationDirtyCarryover.eligible
|
|
5743
|
+
? { navigationDirtyCarryover: { state: 'validated', stepId: rootCauseStepId, dirtyFiles: navigationDirtyCarryover.compatibility?.dirtyFiles, dirtyWorktreeHash: navigationDirtyCarryover.compatibility?.dirtyWorktreeHash } }
|
|
5744
|
+
: { action: 'doctor', blockedAction: 'task run', navigationDirtyCarryover: { state: 'blocked', stepId: rootCauseStepId, blockers: navigationDirtyCarryover.blockers } }
|
|
5745
|
+
: {};
|
|
5746
|
+
let rootCauseReplanDirtyCarryover = !navigationDirtyCarryover?.applicable && ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
|
|
6498
5747
|
&& rootCauseStepId
|
|
6499
|
-
|
|
5748
|
+
// Match runStep: historical carryover admits a replacement start, never a failed retry.
|
|
5749
|
+
&& rootCauseStartPhase === 'planned-dirty'
|
|
6500
5750
|
? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
|
|
6501
5751
|
: null;
|
|
6502
5752
|
if (rootCauseReplanDirtyCarryover?.eligible) {
|
|
@@ -6562,14 +5812,14 @@ export class WorkflowService {
|
|
|
6562
5812
|
}
|
|
6563
5813
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
6564
5814
|
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
6565
|
-
const failedStepRemediations = failedStep
|
|
5815
|
+
const failedStepRemediations = failedStep
|
|
6566
5816
|
? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
|
|
6567
5817
|
: [];
|
|
6568
5818
|
const failedStepReviewRouting = failedStep
|
|
6569
5819
|
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
6570
5820
|
: null;
|
|
6571
|
-
const planIntegrityAssessment =
|
|
6572
|
-
?
|
|
5821
|
+
const planIntegrityAssessment = planRiskAudit && failedStep
|
|
5822
|
+
? this.assessPlanIntegrityRecoveryAdmission(repositoryRoot, task, failedStep.id)
|
|
6573
5823
|
: null;
|
|
6574
5824
|
const downstreamProofAssessment = assessDownstreamProofRecovery(this.store, repositoryRoot, task);
|
|
6575
5825
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -6727,28 +5977,10 @@ export class WorkflowService {
|
|
|
6727
5977
|
},
|
|
6728
5978
|
};
|
|
6729
5979
|
}
|
|
6730
|
-
if (
|
|
6731
|
-
guardedCorrectiveOverlay = {
|
|
6732
|
-
action: 'task remediation-mode-recover',
|
|
6733
|
-
stepId: remediationModeRecovery.remediation.stepId,
|
|
6734
|
-
remediationModeRecovery: {
|
|
6735
|
-
state: 'required',
|
|
6736
|
-
remediationEventId: remediationModeRecovery.remediation.eventId,
|
|
6737
|
-
remediationEventHash: remediationModeRecovery.remediation.eventHash,
|
|
6738
|
-
correctiveDecisionEventId: remediationModeRecovery.decision.eventId,
|
|
6739
|
-
correctiveDecisionRecoveryEventId: remediationModeRecovery.correctiveRecovery.eventId,
|
|
6740
|
-
attemptOrdinal: remediationModeRecovery.remediation.attemptOrdinal,
|
|
6741
|
-
fromMode: 'ordinary',
|
|
6742
|
-
toMode: 'corrective',
|
|
6743
|
-
requiredActor: remediationModeRecovery.actor,
|
|
6744
|
-
},
|
|
6745
|
-
};
|
|
6746
|
-
}
|
|
6747
|
-
if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
5980
|
+
if (guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
6748
5981
|
guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
|
|
6749
5982
|
}
|
|
6750
|
-
if (
|
|
6751
|
-
&& guardedCorrectivePosture
|
|
5983
|
+
if (guardedCorrectivePosture
|
|
6752
5984
|
&& (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
|
|
6753
5985
|
switch (guardedCorrectivePosture.posture) {
|
|
6754
5986
|
case 'decision-required':
|
|
@@ -6929,62 +6161,42 @@ export class WorkflowService {
|
|
|
6929
6161
|
retainedImplementation: 'journal-recoverable-1-to-1',
|
|
6930
6162
|
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
6931
6163
|
structuralReplacementAvailable: false,
|
|
6164
|
+
productDecisionRequired: true,
|
|
6165
|
+
executionAvailable: false,
|
|
6166
|
+
decisionEffect: 'No decision authorizes structural replacement in this package.',
|
|
6167
|
+
productDecisionOptions: replacementMilestone && !['accepted', 'cancelled'].includes(replacementMilestone.status)
|
|
6168
|
+
? [{
|
|
6169
|
+
command: 'milestone cancel',
|
|
6170
|
+
milestoneId: replacementMilestone.id,
|
|
6171
|
+
expectedRevision: replacementMilestone.revision,
|
|
6172
|
+
reasonRequired: true,
|
|
6173
|
+
effect: 'Cancels the whole Milestone; does not create or authorize a replacement Task.',
|
|
6174
|
+
}]
|
|
6175
|
+
: [],
|
|
6932
6176
|
incidentDependencies,
|
|
6933
6177
|
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
6934
6178
|
id: discovery.id,
|
|
6935
6179
|
revision: discovery.revision,
|
|
6936
6180
|
goal: discovery.goal,
|
|
6937
6181
|
})),
|
|
6938
|
-
requiredAction: '
|
|
6182
|
+
requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
|
|
6939
6183
|
},
|
|
6940
6184
|
};
|
|
6941
6185
|
}
|
|
6942
6186
|
break;
|
|
6943
6187
|
case 'stop-escalate':
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
delegatedApprovalAllowed: false,
|
|
6957
|
-
},
|
|
6958
|
-
stopOverride: {
|
|
6959
|
-
state: 'eligible',
|
|
6960
|
-
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
6961
|
-
prepareCommand: 'task stop-override-prepare',
|
|
6962
|
-
applyCommand: 'task stop-override-apply',
|
|
6963
|
-
expectedRevision: task.revision,
|
|
6964
|
-
appendOnly: true,
|
|
6965
|
-
preservesOriginalDecision: true,
|
|
6966
|
-
requiredAction: 'Prepare with an explicit human actor and reason, then apply the exact confirmation code in a later user turn.',
|
|
6967
|
-
},
|
|
6968
|
-
};
|
|
6969
|
-
}
|
|
6970
|
-
catch (error) {
|
|
6971
|
-
guardedCorrectiveOverlay = {
|
|
6972
|
-
action: 'doctor',
|
|
6973
|
-
blockedAction: 'task stop-override-prepare',
|
|
6974
|
-
stepId: guardedCorrectivePosture.stepId,
|
|
6975
|
-
correctiveDecisionGate: {
|
|
6976
|
-
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
6977
|
-
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
6978
|
-
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
6979
|
-
humanApprovalRequired: true,
|
|
6980
|
-
},
|
|
6981
|
-
stopOverride: {
|
|
6982
|
-
state: 'blocked',
|
|
6983
|
-
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
6984
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
6985
|
-
},
|
|
6986
|
-
};
|
|
6987
|
-
}
|
|
6188
|
+
guardedCorrectiveOverlay = {
|
|
6189
|
+
action: 'doctor',
|
|
6190
|
+
blockedAction: 'task run',
|
|
6191
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
6192
|
+
correctiveDecisionGate: {
|
|
6193
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
6194
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
6195
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
6196
|
+
humanApprovalRequired: true,
|
|
6197
|
+
},
|
|
6198
|
+
reason: 'The recorded stop decision blocks execution; legacy stop-override creation is no longer supported.',
|
|
6199
|
+
};
|
|
6988
6200
|
break;
|
|
6989
6201
|
default:
|
|
6990
6202
|
break;
|
|
@@ -7135,6 +6347,7 @@ export class WorkflowService {
|
|
|
7135
6347
|
}
|
|
7136
6348
|
: {}),
|
|
7137
6349
|
...mechanicalFeasibilityOverlay,
|
|
6350
|
+
...navigationDirtyOverlay,
|
|
7138
6351
|
...(downstreamProofCarryover?.applicable
|
|
7139
6352
|
? downstreamProofCarryover.eligible
|
|
7140
6353
|
? {
|
|
@@ -7203,10 +6416,11 @@ export class WorkflowService {
|
|
|
7203
6416
|
rootCauseReplanDirtyCarryover: {
|
|
7204
6417
|
state: 'awaiting-c1',
|
|
7205
6418
|
stepId: rootCauseStepId,
|
|
7206
|
-
requiredActor: defaultTaskWorkerActor(task.id),
|
|
6419
|
+
requiredActor: rootCauseReplanDirtyCarryover.requiredActor ?? defaultTaskWorkerActor(task.id),
|
|
7207
6420
|
},
|
|
7208
6421
|
}
|
|
7209
6422
|
: handoffPosture.state === 'pending'
|
|
6423
|
+
&& (!rootCauseReplanDirtyCarryover.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)
|
|
7210
6424
|
? {}
|
|
7211
6425
|
: {
|
|
7212
6426
|
action: 'doctor',
|
|
@@ -7255,7 +6469,7 @@ export class WorkflowService {
|
|
|
7255
6469
|
&& failedStepRemediations.length >= 2
|
|
7256
6470
|
&& handoffPosture.state !== 'pending'
|
|
7257
6471
|
&& !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
|
|
7258
|
-
&&
|
|
6472
|
+
&& correctiveDecisionCheckoutEligible(this.store, repositoryRoot, task, failedStep.id)
|
|
7259
6473
|
? {
|
|
7260
6474
|
command: 'task corrective-decision',
|
|
7261
6475
|
state: 'eligible',
|
|
@@ -7290,7 +6504,8 @@ export class WorkflowService {
|
|
|
7290
6504
|
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
7291
6505
|
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
7292
6506
|
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
7293
|
-
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim
|
|
6507
|
+
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim
|
|
6508
|
+
&& (!rootCauseReplanDirtyCarryover?.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)) {
|
|
7294
6509
|
return this.withTaskNavigationContracts(task, {
|
|
7295
6510
|
...resultWithCorrectiveOption,
|
|
7296
6511
|
action: 'task claim',
|
|
@@ -7323,28 +6538,43 @@ export class WorkflowService {
|
|
|
7323
6538
|
},
|
|
7324
6539
|
};
|
|
7325
6540
|
}
|
|
7326
|
-
|
|
6541
|
+
taskContextChangeBlocker(task, operation) {
|
|
6542
|
+
const blocked = (error) => ({ error, activeStep: false });
|
|
7327
6543
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
7328
|
-
|
|
6544
|
+
return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has a pending pre-execution replan; replace the implementation Plan before changing context.`, { taskId: task.id, requiredAction: 'task plan-set' }));
|
|
7329
6545
|
}
|
|
7330
6546
|
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
7331
|
-
|
|
6547
|
+
return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
7332
6548
|
taskId: task.id,
|
|
7333
6549
|
requiredAction: 'Run task plan-set with the full unfinished remainder before requesting authorization.',
|
|
7334
|
-
});
|
|
6550
|
+
}));
|
|
7335
6551
|
}
|
|
7336
6552
|
const posture = this.taskTerminalCorrectivePosture(task);
|
|
7337
|
-
if (
|
|
7338
|
-
return;
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
}
|
|
6553
|
+
if (posture) {
|
|
6554
|
+
return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
|
|
6555
|
+
taskId: task.id,
|
|
6556
|
+
stepId: posture.stepId,
|
|
6557
|
+
decision: posture.posture,
|
|
6558
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
6559
|
+
requiredAction: posture.posture === 'split-required'
|
|
6560
|
+
? 'This Task cannot resume after split-required; no executable replacement route is available. Request a human decision without changing retained state.'
|
|
6561
|
+
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
6562
|
+
}));
|
|
6563
|
+
}
|
|
6564
|
+
if (!taskCanRebindKnowledge(task)) {
|
|
6565
|
+
return blocked(new WorkflowError('TRANSITION_BLOCKED', operation === 'rebind'
|
|
6566
|
+
? `Cannot rebind Task Plan knowledge while Task is ${task.status}.`
|
|
6567
|
+
: `Cannot refresh Task context while Task is ${task.status}.`));
|
|
6568
|
+
}
|
|
6569
|
+
const activeStep = task.steps.find(step => step.status === 'in_progress');
|
|
6570
|
+
if (activeStep)
|
|
6571
|
+
return {
|
|
6572
|
+
activeStep: true,
|
|
6573
|
+
error: new WorkflowError('TRANSITION_BLOCKED', operation === 'rebind'
|
|
6574
|
+
? `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`
|
|
6575
|
+
: `Cannot refresh Task context while ${activeStep.id} is in progress.`),
|
|
6576
|
+
};
|
|
6577
|
+
return null;
|
|
7348
6578
|
}
|
|
7349
6579
|
stopOverrideContextRebindDecisionId(task, stepId, repositoryRoot) {
|
|
7350
6580
|
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
@@ -7368,6 +6598,14 @@ export class WorkflowService {
|
|
|
7368
6598
|
taskTerminalCorrectivePosture(task) {
|
|
7369
6599
|
if (!task.planHash)
|
|
7370
6600
|
return null;
|
|
6601
|
+
if (task.status === 'awaiting_execution_authorization' && task.authorizations.length === 0
|
|
6602
|
+
&& task.baseCommit === null && task.taskBranch === null && task.workspaceOwner === null
|
|
6603
|
+
&& task.mergedCommit === null && task.systemCommits.length === 0
|
|
6604
|
+
&& task.resultHash === null && task.evidenceHash === null && task.review.status === 'pending'
|
|
6605
|
+
&& task.steps.every((step) => (step.status === 'planned' || (step.status === 'blocked' && step.dependencies.length > 0))
|
|
6606
|
+
&& step.evidence === null && !step.failurePause)
|
|
6607
|
+
&& readPlanRiskAuditEvents(this.store, task).length === 0)
|
|
6608
|
+
return null;
|
|
7371
6609
|
const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
|
|
7372
6610
|
if (!planRiskAudit)
|
|
7373
6611
|
return null;
|
|
@@ -7376,42 +6614,6 @@ export class WorkflowService {
|
|
|
7376
6614
|
? posture
|
|
7377
6615
|
: null;
|
|
7378
6616
|
}
|
|
7379
|
-
assertStopOverrideRepositoryBoundary(identity, task, observedLeases) {
|
|
7380
|
-
const blockers = [];
|
|
7381
|
-
const dirty = changedFiles(identity.repositoryRoot);
|
|
7382
|
-
if (dirty.length > 0)
|
|
7383
|
-
blockers.push('Repository checkout must be clean.');
|
|
7384
|
-
if (task.workspaceOwner !== 'local')
|
|
7385
|
-
blockers.push('Stop override is limited to a local Task workspace.');
|
|
7386
|
-
if (!task.taskBranch || currentBranch(identity.repositoryRoot) !== task.taskBranch) {
|
|
7387
|
-
blockers.push('The current branch must be the recorded Task branch.');
|
|
7388
|
-
}
|
|
7389
|
-
if (task.steps.some((step) => step.status === 'in_progress'))
|
|
7390
|
-
blockers.push('No Step may be in progress.');
|
|
7391
|
-
const leases = observedLeases
|
|
7392
|
-
?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
|
|
7393
|
-
if (leases.some((lease) => lease.entityId === task.id))
|
|
7394
|
-
blockers.push('No active or stale Task writer lease may exist.');
|
|
7395
|
-
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
7396
|
-
const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
|
|
7397
|
-
const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
|
|
7398
|
-
if (replanTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
7399
|
-
|| replanTransactions.corrupt.some((transaction) => transaction.taskId === task.id)
|
|
7400
|
-
|| yieldTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
7401
|
-
|| yieldTransactions.corrupt.some((transaction) => transaction.taskId === task.id)) {
|
|
7402
|
-
blockers.push('No pending or corrupt Task transaction may exist.');
|
|
7403
|
-
}
|
|
7404
|
-
if (listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root)
|
|
7405
|
-
.some((operation) => operation.entityId === task.id)) {
|
|
7406
|
-
blockers.push('No Core Task operation may exist.');
|
|
7407
|
-
}
|
|
7408
|
-
if (blockers.length > 0) {
|
|
7409
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override repository preconditions are not satisfied.', {
|
|
7410
|
-
taskId: task.id,
|
|
7411
|
-
blockers,
|
|
7412
|
-
});
|
|
7413
|
-
}
|
|
7414
|
-
}
|
|
7415
6617
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
7416
6618
|
return this.listDelegations(projectId)
|
|
7417
6619
|
.filter((grant) => {
|
|
@@ -7435,6 +6637,19 @@ export class WorkflowService {
|
|
|
7435
6637
|
}));
|
|
7436
6638
|
}
|
|
7437
6639
|
delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
|
|
6640
|
+
let planningRecovery;
|
|
6641
|
+
try {
|
|
6642
|
+
planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
6643
|
+
if (!planningRecovery)
|
|
6644
|
+
readCurrentPlanRiskAudit(this.store, task);
|
|
6645
|
+
}
|
|
6646
|
+
catch (error) {
|
|
6647
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
6648
|
+
return [];
|
|
6649
|
+
throw error;
|
|
6650
|
+
}
|
|
6651
|
+
if (planningRecovery && mode !== 'delegated-content-only')
|
|
6652
|
+
return [];
|
|
7438
6653
|
return this.listDelegations(projectId)
|
|
7439
6654
|
.filter((grant) => {
|
|
7440
6655
|
try {
|
|
@@ -7454,8 +6669,8 @@ export class WorkflowService {
|
|
|
7454
6669
|
grantId: grant.id,
|
|
7455
6670
|
principal: grant.principal,
|
|
7456
6671
|
delegate: grant.delegate,
|
|
7457
|
-
transition: 'task.execution_authorize',
|
|
7458
|
-
transitions: ['project_memory.approve', 'task.execution_authorize'],
|
|
6672
|
+
transition: planningRecovery ? 'project_memory.approve' : 'task.execution_authorize',
|
|
6673
|
+
transitions: planningRecovery ? ['project_memory.approve'] : ['project_memory.approve', 'task.execution_authorize'],
|
|
7459
6674
|
scope: grant.scope,
|
|
7460
6675
|
expiresAt: grant.expiresAt,
|
|
7461
6676
|
policyHash: grant.policyHash,
|
|
@@ -7465,16 +6680,13 @@ export class WorkflowService {
|
|
|
7465
6680
|
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
7466
6681
|
for (const task of tasks) {
|
|
7467
6682
|
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
7468
|
-
|| !task.planHash
|
|
7469
|
-
|| !taskCanRebindKnowledge(task)
|
|
7470
|
-
|| (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed'))
|
|
7471
|
-
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
6683
|
+
|| !task.planHash)
|
|
7472
6684
|
continue;
|
|
7473
6685
|
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
7474
6686
|
if (!assessment.eligible)
|
|
7475
6687
|
continue;
|
|
7476
6688
|
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
7477
|
-
if (options.length > 0)
|
|
6689
|
+
if (options.length > 0 && this.taskContextChangeBlocker(task, 'refresh') === null)
|
|
7478
6690
|
return { task, assessment, options };
|
|
7479
6691
|
}
|
|
7480
6692
|
return null;
|
|
@@ -7582,12 +6794,31 @@ export class WorkflowService {
|
|
|
7582
6794
|
: inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
7583
6795
|
}
|
|
7584
6796
|
taskGitNavigationOverride(task) {
|
|
6797
|
+
if (task.pendingBaseSyncValidation !== undefined) {
|
|
6798
|
+
const assessment = assessPendingBaseSyncValidation(task);
|
|
6799
|
+
return { scope: 'task', id: task.id, status: task.status, revision: task.revision,
|
|
6800
|
+
action: assessment.reason ? 'blocked' : 'task sync-base',
|
|
6801
|
+
pendingBaseSyncValidation: task.pendingBaseSyncValidation,
|
|
6802
|
+
...(assessment.reason ? { dirtyFiles: assessment.dirtyFiles,
|
|
6803
|
+
navigation: { kind: 'wait', reason: 'base-sync-validation-snapshot', waitFor: assessment.reason } } : {}),
|
|
6804
|
+
};
|
|
6805
|
+
}
|
|
7585
6806
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
|
|
7586
6807
|
return null;
|
|
7587
6808
|
}
|
|
7588
6809
|
const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
7589
6810
|
if (currentBase === task.baseCommit)
|
|
7590
6811
|
return null;
|
|
6812
|
+
const taskHead = runGit(task.repositoryRoot, ['rev-parse', task.taskBranch]);
|
|
6813
|
+
const conflict = currentBaseSyncConflict(this.store, task, taskHead, currentBase);
|
|
6814
|
+
if (conflict)
|
|
6815
|
+
return {
|
|
6816
|
+
scope: 'task', id: task.id, status: task.status, revision: task.revision, action: 'blocked',
|
|
6817
|
+
baseSyncConflict: conflict,
|
|
6818
|
+
navigation: { kind: 'wait', reason: 'base-sync-conflict', externalDecisionRequired: true,
|
|
6819
|
+
waitFor: 'The base owner resolves the conflict on the base branch without changing the reviewed Task head.',
|
|
6820
|
+
resume: 'After the base HEAD changes, obtain fresh status and next; synchronization still requires review and acceptance.' },
|
|
6821
|
+
};
|
|
7591
6822
|
return {
|
|
7592
6823
|
scope: 'task',
|
|
7593
6824
|
id: task.id,
|
|
@@ -7601,43 +6832,6 @@ export class WorkflowService {
|
|
|
7601
6832
|
},
|
|
7602
6833
|
};
|
|
7603
6834
|
}
|
|
7604
|
-
carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
|
|
7605
|
-
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
7606
|
-
let dependencyCommitCount = 0;
|
|
7607
|
-
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
7608
|
-
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
7609
|
-
if (!dependencyCommit)
|
|
7610
|
-
return null;
|
|
7611
|
-
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
7612
|
-
dependencyCommitCount += 1;
|
|
7613
|
-
}
|
|
7614
|
-
if (dependencyCommitCount === 0)
|
|
7615
|
-
return null;
|
|
7616
|
-
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
7617
|
-
const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
|
|
7618
|
-
const compatibility = record.planIntegrity;
|
|
7619
|
-
if (!compatibility
|
|
7620
|
-
|| record.commitSha !== boundaryCommit
|
|
7621
|
-
|| !task.systemCommits.includes(record.commitSha)
|
|
7622
|
-
|| compatibility.planHash !== task.planHash
|
|
7623
|
-
|| canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
|
|
7624
|
-
|| compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
|
|
7625
|
-
return false;
|
|
7626
|
-
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
|
|
7627
|
-
if (!step || step.status !== 'failed')
|
|
7628
|
-
return false;
|
|
7629
|
-
const remediationEventIds = readRemediationEvents(this.store, task)
|
|
7630
|
-
.filter((event) => event.stepId === compatibility.stepId)
|
|
7631
|
-
.map((event) => event.eventId);
|
|
7632
|
-
if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
|
|
7633
|
-
return false;
|
|
7634
|
-
return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
|
|
7635
|
-
&& event.stepId === compatibility.stepId
|
|
7636
|
-
&& event.decision === 'replan-required')
|
|
7637
|
-
&& readTaskC1Posture(this.store, task).state === 'none';
|
|
7638
|
-
});
|
|
7639
|
-
return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
|
|
7640
|
-
}
|
|
7641
6835
|
postRebindCheckSupportCompatibility(snapshot, task) {
|
|
7642
6836
|
if (task.status !== 'awaiting_execution_authorization'
|
|
7643
6837
|
|| !task.planHash
|
|
@@ -7812,9 +7006,12 @@ export class WorkflowService {
|
|
|
7812
7006
|
blockers,
|
|
7813
7007
|
};
|
|
7814
7008
|
}
|
|
7815
|
-
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null,
|
|
7009
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, historicalStepProvenance = null) {
|
|
7816
7010
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
7817
7011
|
const now = this.now().getTime();
|
|
7012
|
+
const navigationCarryover = !activeDownstreamProof && !historicalStepProvenance && changedFiles(snapshot.identity.repositoryRoot).length > 0
|
|
7013
|
+
? assessNavigationDirtyCarryover(this.store, snapshot.identity.repositoryRoot, task, undefined, 'HEAD^')
|
|
7014
|
+
: null;
|
|
7818
7015
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
7819
7016
|
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
7820
7017
|
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
@@ -7832,7 +7029,7 @@ export class WorkflowService {
|
|
|
7832
7029
|
currentBranch: versions.currentBranch,
|
|
7833
7030
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
7834
7031
|
},
|
|
7835
|
-
}, activeDownstreamProof,
|
|
7032
|
+
}, activeDownstreamProof, historicalStepProvenance, navigationCarryover?.eligible ? navigationCarryover.compatibility : null);
|
|
7836
7033
|
}
|
|
7837
7034
|
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
|
|
7838
7035
|
const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
|
|
@@ -7898,10 +7095,19 @@ export class WorkflowService {
|
|
|
7898
7095
|
}
|
|
7899
7096
|
const candidates = task.steps
|
|
7900
7097
|
.filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
|
|
7901
|
-
.flatMap((step) =>
|
|
7902
|
-
|
|
7903
|
-
.
|
|
7904
|
-
|
|
7098
|
+
.flatMap((step) => {
|
|
7099
|
+
const failures = readRemediationEvents(this.store, task)
|
|
7100
|
+
.filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed');
|
|
7101
|
+
const terminal = failures.at(-1);
|
|
7102
|
+
if (!terminal)
|
|
7103
|
+
return [];
|
|
7104
|
+
const yields = readTaskHandoffEvents(this.store, task)
|
|
7105
|
+
.filter((event) => matchesTerminalCorrectiveYield(event, terminal, task.id));
|
|
7106
|
+
// Missing native boundary must remain visibly blocked, never fall through to task run.
|
|
7107
|
+
if (yields.length > 1)
|
|
7108
|
+
return [];
|
|
7109
|
+
return [assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead: terminal.failureEvidence.completionCommit })];
|
|
7110
|
+
})
|
|
7905
7111
|
.filter((assessment) => assessment.applicable);
|
|
7906
7112
|
const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
|
|
7907
7113
|
if (eligible.length === 1)
|
|
@@ -7910,6 +7116,7 @@ export class WorkflowService {
|
|
|
7910
7116
|
applicable: candidates.length > 0,
|
|
7911
7117
|
eligible: false,
|
|
7912
7118
|
compatibility: null,
|
|
7119
|
+
...(candidates.length === 1 && candidates[0].requiredActor ? { requiredActor: candidates[0].requiredActor } : {}),
|
|
7913
7120
|
blockers: [...new Set([
|
|
7914
7121
|
...candidates.flatMap((assessment) => assessment.blockers),
|
|
7915
7122
|
...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
|
|
@@ -8885,7 +8092,6 @@ function writerTokenContractForAction(action, claimedC1Posture = false, existing
|
|
|
8885
8092
|
'task review-launch',
|
|
8886
8093
|
'task review-sealed-record',
|
|
8887
8094
|
'task corrective-decision-recover',
|
|
8888
|
-
'task remediation-mode-recover',
|
|
8889
8095
|
'task plan-integrity-recover',
|
|
8890
8096
|
'task corrective-decision',
|
|
8891
8097
|
'task corrective-yield',
|