codex-workflow-v2 2.0.0-beta.13.8 → 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 +96 -32
- 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/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- 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/plan-risk.d.ts +2 -0
- package/dist/src/alpha6/plan-risk.js +10 -3
- package/dist/src/alpha6/plan-risk.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/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- 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 +6 -5
- 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-executor.js +2 -2
- package/dist/src/state/corrective-yield-executor.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 +698 -1470
- 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 -363
- 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 -740
- 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 -508
- 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,10 +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';
|
|
10
|
+
import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
|
|
1
11
|
import { explainChange } from './change-explanation.js';
|
|
2
|
-
import {
|
|
12
|
+
import { verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
3
13
|
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
14
|
import { createHash } from 'node:crypto';
|
|
5
15
|
import path from 'node:path';
|
|
6
16
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
7
|
-
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery,
|
|
17
|
+
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
8
18
|
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
9
19
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
10
20
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
@@ -14,19 +24,20 @@ import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './do
|
|
|
14
24
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
15
25
|
import { WorkflowError } from './errors.js';
|
|
16
26
|
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
27
|
+
import { withWorkflowBlockerRoute } from './workflow-blocker-route.js';
|
|
17
28
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
18
|
-
import {
|
|
29
|
+
import { inspectWorkflowVersions } from './diagnostics.js';
|
|
19
30
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
|
|
20
|
-
import { assessDownstreamProofCarryover,
|
|
31
|
+
import { assessDownstreamProofCarryover, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, prepareDownstreamProofInvalidation, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
21
32
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
22
|
-
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
33
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
23
34
|
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
24
35
|
import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
|
|
25
|
-
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';
|
|
26
37
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
27
38
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
28
39
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
29
|
-
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';
|
|
30
41
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
31
42
|
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
|
|
32
43
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
@@ -37,7 +48,7 @@ import { ensureDirectory } from './fs-utils.js';
|
|
|
37
48
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
38
49
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
39
50
|
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
40
|
-
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';
|
|
41
52
|
import { FileStateStore } from './state/store.js';
|
|
42
53
|
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
43
54
|
import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
|
|
@@ -50,10 +61,12 @@ import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluate
|
|
|
50
61
|
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
51
62
|
export class WorkflowService {
|
|
52
63
|
store;
|
|
64
|
+
checkOptions;
|
|
53
65
|
now;
|
|
54
66
|
transactionHooks;
|
|
55
|
-
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
|
|
67
|
+
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}, checkOptions = {}) {
|
|
56
68
|
this.store = store;
|
|
69
|
+
this.checkOptions = checkOptions;
|
|
57
70
|
this.now = now;
|
|
58
71
|
this.transactionHooks = transactionHooks;
|
|
59
72
|
}
|
|
@@ -88,6 +101,9 @@ export class WorkflowService {
|
|
|
88
101
|
const projectFile = path.join(projectRoot, 'project.json');
|
|
89
102
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
90
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
|
+
}
|
|
91
107
|
// The registration posture is a sidecar under the project root. A completely
|
|
92
108
|
// fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
|
|
93
109
|
// only that directory before writing the posture; project.json remains the
|
|
@@ -115,6 +131,36 @@ export class WorkflowService {
|
|
|
115
131
|
return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
|
|
116
132
|
}
|
|
117
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
|
+
}
|
|
118
164
|
const context = this.#mutationContext(repository);
|
|
119
165
|
const lease = context.locks.repairStale(entityId);
|
|
120
166
|
return {
|
|
@@ -122,31 +168,6 @@ export class WorkflowService {
|
|
|
122
168
|
repaired: true,
|
|
123
169
|
};
|
|
124
170
|
}
|
|
125
|
-
prepareTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason) {
|
|
126
|
-
const snapshot = this.observationSnapshot(repository);
|
|
127
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
128
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
129
|
-
observation: snapshot.observation,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId) ?? null;
|
|
133
|
-
if (!task)
|
|
134
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
135
|
-
this.assertStopOverrideRepositoryBoundary(snapshot.identity, task);
|
|
136
|
-
return prepareStopEscalateOverride(this.store, task, stepId, expectedRevision, actor, reason, headCommit(snapshot.identity.repositoryRoot));
|
|
137
|
-
}
|
|
138
|
-
applyTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason, confirmationCode) {
|
|
139
|
-
const snapshot = this.observationSnapshot(repository);
|
|
140
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
141
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
142
|
-
observation: snapshot.observation,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
const context = this.#mutationContext(repository);
|
|
146
|
-
const task = context.store.readTask(context.identity.projectId, taskId);
|
|
147
|
-
this.assertStopOverrideRepositoryBoundary(context.identity, task, context.locks.list());
|
|
148
|
-
return appendStopEscalateOverride(context.store, task, stepId, expectedRevision, actor, reason, headCommit(context.identity.repositoryRoot), confirmationCode, this.now());
|
|
149
|
-
}
|
|
150
171
|
prepareCorrectiveReplan(repository, taskId, candidate) {
|
|
151
172
|
return this.correctiveReplanController(repository).prepare({ taskId, candidate });
|
|
152
173
|
}
|
|
@@ -292,92 +313,6 @@ export class WorkflowService {
|
|
|
292
313
|
invalidation: invalidation.event,
|
|
293
314
|
};
|
|
294
315
|
}
|
|
295
|
-
recoverDownstreamProofReplan(repository, taskId, expectedTaskRevision, actor, reason) {
|
|
296
|
-
const context = this.#mutationContext(repository);
|
|
297
|
-
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
298
|
-
assertExpectedRevision(task, expectedTaskRevision);
|
|
299
|
-
// This repair intentionally runs before Project Knowledge refresh: the exact
|
|
300
|
-
// dependency-only transport can make the current map stale, while the
|
|
301
|
-
// recovery assessment still binds the stranded Task to its recorded rebind.
|
|
302
|
-
const observation = this.observationSnapshot(repository).observation;
|
|
303
|
-
if (observation.kind !== 'assessment') {
|
|
304
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan recovery requires a coherent observation boundary.', {
|
|
305
|
-
observation,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
309
|
-
const assessment = assessDownstreamProofReplanRecovery(this.store, context.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
310
|
-
const environmentBlockers = [
|
|
311
|
-
...(!assessment.dependencyRecoveryCommit
|
|
312
|
-
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
313
|
-
: []),
|
|
314
|
-
...(observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
315
|
-
...(observation.pendingProjectTransactions.length > 0
|
|
316
|
-
|| observation.pendingMilestoneTransactions.length > 0
|
|
317
|
-
|| observation.pendingTaskTransactions.length > 0
|
|
318
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
319
|
-
...(observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
320
|
-
...(observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
321
|
-
];
|
|
322
|
-
if (!assessment.applicable || !assessment.eligible || environmentBlockers.length > 0) {
|
|
323
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Task is not eligible for bounded downstream-proof replan recovery.', {
|
|
324
|
-
taskId,
|
|
325
|
-
blockers: [...assessment.blockers, ...environmentBlockers],
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
const now = this.now();
|
|
329
|
-
const resultTask = {
|
|
330
|
-
...task,
|
|
331
|
-
revision: task.revision + 1,
|
|
332
|
-
updatedAt: now.toISOString(),
|
|
333
|
-
status: 'needs_fix',
|
|
334
|
-
authorizations: task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
335
|
-
? { ...authorization, decision: 'superseded' }
|
|
336
|
-
: authorization),
|
|
337
|
-
review: emptyReview(),
|
|
338
|
-
resultHash: null,
|
|
339
|
-
evidenceHash: null,
|
|
340
|
-
blockReason: `${assessment.invalidation.activeStepId} downstream-proof recovery requires replacement of the full unfinished Step Plan before execution can resume.`,
|
|
341
|
-
};
|
|
342
|
-
const recovery = prepareDownstreamProofReplanRecovery(this.store, resultTask, assessment, { previousTaskRevision: task.revision, actor, reason }, now);
|
|
343
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
344
|
-
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
345
|
-
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
346
|
-
applyProjectTransaction({
|
|
347
|
-
projectRoot,
|
|
348
|
-
projectId: task.projectId,
|
|
349
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
350
|
-
kind: 'downstream-proof-replan-recovery',
|
|
351
|
-
now,
|
|
352
|
-
targets: [
|
|
353
|
-
{
|
|
354
|
-
path: relative(path.join(taskRoot, downstreamProofReplanRecoverySidecar())),
|
|
355
|
-
content: recovery.postimage,
|
|
356
|
-
},
|
|
357
|
-
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
358
|
-
],
|
|
359
|
-
validate: () => {
|
|
360
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
361
|
-
const events = readDownstreamProofReplanRecoveries(this.store, saved);
|
|
362
|
-
if (saved.revision !== resultTask.revision
|
|
363
|
-
|| saved.status !== 'needs_fix'
|
|
364
|
-
|| saved.planHash !== task.planHash
|
|
365
|
-
|| events.at(-1)?.eventHash !== recovery.event.eventHash
|
|
366
|
-
|| headCommit(context.identity.repositoryRoot) !== recovery.event.preservedHeadCommit
|
|
367
|
-
|| JSON.stringify(changedFiles(context.identity.repositoryRoot).sort())
|
|
368
|
-
!== JSON.stringify(recovery.event.dirtyFiles)) {
|
|
369
|
-
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery transaction readback failed.', {
|
|
370
|
-
taskId,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
return {
|
|
377
|
-
task: this.store.readTask(task.projectId, task.id),
|
|
378
|
-
recovery: recovery.event,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
316
|
correctiveReplanController(repository) {
|
|
382
317
|
const identity = resolveRepositoryIdentity(repository);
|
|
383
318
|
return createCorrectiveReplanPublicController({
|
|
@@ -565,6 +500,7 @@ export class WorkflowService {
|
|
|
565
500
|
if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
|
|
566
501
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
567
502
|
}
|
|
503
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
568
504
|
if (milestone.baseBranch !== baseBranch) {
|
|
569
505
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task and Milestone must use the same base branch.', {
|
|
570
506
|
taskBaseBranch: baseBranch,
|
|
@@ -1006,7 +942,7 @@ export class WorkflowService {
|
|
|
1006
942
|
milestoneId: milestone.id,
|
|
1007
943
|
oldTaskId: oldTask.id,
|
|
1008
944
|
incidentDependencies,
|
|
1009
|
-
requiredAction: '
|
|
945
|
+
requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
|
|
1010
946
|
});
|
|
1011
947
|
}
|
|
1012
948
|
/* P04-B retains the existing one-for-one transaction below as implementation evidence,
|
|
@@ -1519,34 +1455,37 @@ export class WorkflowService {
|
|
|
1519
1455
|
locks.release(milestoneId, authorityLease.token);
|
|
1520
1456
|
}
|
|
1521
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
|
+
}
|
|
1522
1481
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
1523
1482
|
const { identity, locks } = this.#mutationContext(repository);
|
|
1524
1483
|
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1525
1484
|
try {
|
|
1526
1485
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1527
1486
|
assertExpectedRevision(milestone, expectedRevision);
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1532
|
-
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1533
|
-
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1534
|
-
expectedBranch: milestone.baseBranch,
|
|
1535
|
-
actualBranch: currentBranch(identity.repositoryRoot),
|
|
1536
|
-
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
const incomplete = milestone.memberships.filter((membership) => {
|
|
1540
|
-
if (membership.disposition !== 'required')
|
|
1541
|
-
return false;
|
|
1542
|
-
return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
|
|
1543
|
-
});
|
|
1544
|
-
if (incomplete.length > 0) {
|
|
1545
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1546
|
-
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
const checks = runChecks(identity.repositoryRoot, milestone.checks);
|
|
1487
|
+
this.assertMilestoneValidationReady(identity.repositoryRoot, milestone);
|
|
1488
|
+
const checks = runChecks(identity.repositoryRoot, milestone.checks, this.checkOptions);
|
|
1550
1489
|
const baseHead = headCommit(identity.repositoryRoot);
|
|
1551
1490
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1552
1491
|
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
@@ -2094,10 +2033,10 @@ export class WorkflowService {
|
|
|
2094
2033
|
const { identity } = this.#mutationContext(repository);
|
|
2095
2034
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2096
2035
|
assertExpectedRevision(task, expectedRevision);
|
|
2097
|
-
this.
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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;
|
|
2101
2040
|
if (!task.planHash ||
|
|
2102
2041
|
!task.planObjective ||
|
|
2103
2042
|
task.knowledgeMapRevision === null ||
|
|
@@ -2107,10 +2046,8 @@ export class WorkflowService {
|
|
|
2107
2046
|
task.graphUse === null) {
|
|
2108
2047
|
throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no complete Plan binding to rebind.`);
|
|
2109
2048
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
|
|
2113
|
-
}
|
|
2049
|
+
if (contextBlocker)
|
|
2050
|
+
throw contextBlocker.error;
|
|
2114
2051
|
const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
2115
2052
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2116
2053
|
if (task.knowledgeMapRevision === knowledgeMap.revision &&
|
|
@@ -2187,21 +2124,20 @@ export class WorkflowService {
|
|
|
2187
2124
|
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
2188
2125
|
assertExpectedRevision(task, expectedTaskRevision);
|
|
2189
2126
|
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
2190
|
-
this.
|
|
2191
|
-
if (
|
|
2192
|
-
throw
|
|
2193
|
-
}
|
|
2194
|
-
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
2195
|
-
if (activeStep) {
|
|
2196
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
|
|
2197
|
-
}
|
|
2127
|
+
const contextBlocker = this.taskContextChangeBlocker(task, 'refresh');
|
|
2128
|
+
if (contextBlocker)
|
|
2129
|
+
throw contextBlocker.error;
|
|
2198
2130
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
2199
2131
|
const transitions = [];
|
|
2200
|
-
const
|
|
2132
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2133
|
+
const currentPlanRiskAudit = planningRecovery?.current ?? readCurrentPlanRiskAudit(this.store, task);
|
|
2201
2134
|
const currentPlanArtifactHash = task.planHash
|
|
2202
2135
|
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
|
|
2203
2136
|
: null;
|
|
2204
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
|
+
}
|
|
2205
2141
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
2206
2142
|
// leave the composite operation half-applied.
|
|
2207
2143
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
@@ -2248,11 +2184,15 @@ export class WorkflowService {
|
|
|
2248
2184
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
2249
2185
|
}
|
|
2250
2186
|
transitions.push('task knowledge-rebind');
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
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');
|
|
2255
2194
|
return {
|
|
2195
|
+
authority: planningRecovery ? 'planning-only' : 'execution',
|
|
2256
2196
|
mode: refreshAssessment.mode,
|
|
2257
2197
|
transitions,
|
|
2258
2198
|
addedSources: refreshAssessment.addedSources,
|
|
@@ -2262,8 +2202,16 @@ export class WorkflowService {
|
|
|
2262
2202
|
},
|
|
2263
2203
|
validate: (result) => {
|
|
2264
2204
|
const currentTask = this.store.readTask(identity.projectId, task.id);
|
|
2265
|
-
if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
|
|
2266
|
-
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
|
+
}
|
|
2267
2215
|
}
|
|
2268
2216
|
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
2269
2217
|
if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
|
|
@@ -2336,6 +2284,7 @@ export class WorkflowService {
|
|
|
2336
2284
|
const { identity } = this.#mutationContext(repository);
|
|
2337
2285
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2338
2286
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2287
|
+
assertNoPendingBaseSyncValidation(task);
|
|
2339
2288
|
assertExpectedRevision(task, expectedRevision);
|
|
2340
2289
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2341
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' });
|
|
@@ -2486,7 +2435,16 @@ export class WorkflowService {
|
|
|
2486
2435
|
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
2487
2436
|
const context = this.#mutationContext(repository);
|
|
2488
2437
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2438
|
+
assertNoPendingBaseSyncValidation(task);
|
|
2489
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
|
+
}
|
|
2490
2448
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2491
2449
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
|
|
2492
2450
|
taskId,
|
|
@@ -2648,8 +2606,12 @@ export class WorkflowService {
|
|
|
2648
2606
|
});
|
|
2649
2607
|
}
|
|
2650
2608
|
}
|
|
2651
|
-
const
|
|
2652
|
-
const
|
|
2609
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2610
|
+
const navigation = planningRecovery
|
|
2611
|
+
? this.nextForTaskWithDelegation(task.projectId, context.identity.repositoryRoot, task, planningRecovery.current)
|
|
2612
|
+
: this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
2613
|
+
const action = String(navigation.action === 'task writer-credential-replace'
|
|
2614
|
+
? navigation.blockedAction : navigation.action);
|
|
2653
2615
|
const currentLease = context.locks.inspect(taskId);
|
|
2654
2616
|
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
2655
2617
|
const requiredActor = writerTokenContract
|
|
@@ -2786,7 +2748,7 @@ export class WorkflowService {
|
|
|
2786
2748
|
const step = requireStep(task, stepId);
|
|
2787
2749
|
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2788
2750
|
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
2789
|
-
if (
|
|
2751
|
+
if (stepStartPhase(step.status, false) === 'not-startable') {
|
|
2790
2752
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
2791
2753
|
}
|
|
2792
2754
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
@@ -2806,6 +2768,7 @@ export class WorkflowService {
|
|
|
2806
2768
|
? guardedPosture.correctiveDecision
|
|
2807
2769
|
: null;
|
|
2808
2770
|
const recovery = staleDecision?.decision === 'continue-fix'
|
|
2771
|
+
&& !isKnowledgeReboundSupportDecision(this.store, task, staleDecision)
|
|
2809
2772
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
2810
2773
|
: null;
|
|
2811
2774
|
const stopOverrideContextRebind = staleDecision?.decision === 'stop-escalate'
|
|
@@ -2816,23 +2779,28 @@ export class WorkflowService {
|
|
|
2816
2779
|
}
|
|
2817
2780
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2818
2781
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
2819
|
-
const
|
|
2782
|
+
const startPhase = stepStartPhase(step.status, dirty.length > 0);
|
|
2783
|
+
const downstreamProofCarryover = startPhase === 'planned-dirty'
|
|
2820
2784
|
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2821
2785
|
: null;
|
|
2822
|
-
const planIntegrityDirtyCarryover =
|
|
2786
|
+
const planIntegrityDirtyCarryover = startPhase === 'planned-dirty'
|
|
2823
2787
|
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2824
2788
|
: null;
|
|
2825
|
-
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
|
|
2826
2793
|
? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2827
2794
|
: null;
|
|
2828
|
-
if (
|
|
2829
|
-
&& dirty.length > 0
|
|
2795
|
+
if (startPhase === 'planned-dirty'
|
|
2830
2796
|
&& !resumesCorrectiveReplan
|
|
2831
2797
|
&& !downstreamProofCarryover?.eligible
|
|
2832
2798
|
&& !planIntegrityDirtyCarryover?.eligible
|
|
2799
|
+
&& !navigationDirtyCarryover?.eligible
|
|
2833
2800
|
&& !rootCauseReplanDirtyCarryover?.eligible) {
|
|
2834
2801
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2835
2802
|
changedFiles: dirty,
|
|
2803
|
+
...(navigationDirtyCarryover?.applicable ? { navigationDirtyCarryoverBlockers: navigationDirtyCarryover.blockers } : {}),
|
|
2836
2804
|
...(downstreamProofCarryover?.applicable
|
|
2837
2805
|
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2838
2806
|
: {}),
|
|
@@ -2845,7 +2813,7 @@ export class WorkflowService {
|
|
|
2845
2813
|
});
|
|
2846
2814
|
}
|
|
2847
2815
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
2848
|
-
if ((
|
|
2816
|
+
if ((startPhase === 'failed-retry' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
2849
2817
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
2850
2818
|
changedFiles: dirtyOutsideStep,
|
|
2851
2819
|
});
|
|
@@ -3066,7 +3034,7 @@ export class WorkflowService {
|
|
|
3066
3034
|
return task;
|
|
3067
3035
|
}
|
|
3068
3036
|
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
3069
|
-
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
3037
|
+
const checks = runChecks(context.identity.repositoryRoot, step.checks, this.checkOptions);
|
|
3070
3038
|
const failed = checks.filter((check) => !check.passed);
|
|
3071
3039
|
if (failed.length > 0) {
|
|
3072
3040
|
const now = this.now();
|
|
@@ -3074,13 +3042,16 @@ export class WorkflowService {
|
|
|
3074
3042
|
// Bind useful uncommitted work to this failure without treating it as
|
|
3075
3043
|
// completed-Step authority.
|
|
3076
3044
|
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3045
|
+
const capturedEvidence = prepareCapturedCheckEvidence(this.store, task, checks);
|
|
3077
3046
|
const failurePause = {
|
|
3078
3047
|
recordedAt: now.toISOString(),
|
|
3079
3048
|
dirtyFiles,
|
|
3080
3049
|
dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
|
|
3081
3050
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
3082
3051
|
failedChecks: normalizedFailures,
|
|
3052
|
+
checksEvidence: capturedEvidence.reference,
|
|
3083
3053
|
};
|
|
3054
|
+
const failureC1Posture = readTaskC1Posture(this.store, task);
|
|
3084
3055
|
const preparedRemediation = reviewRequired && planRiskAudit
|
|
3085
3056
|
? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
3086
3057
|
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
@@ -3089,6 +3060,11 @@ export class WorkflowService {
|
|
|
3089
3060
|
reviewerAttestationHash: null,
|
|
3090
3061
|
executionTaskRevision: task.revision,
|
|
3091
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
|
+
} } : {}),
|
|
3092
3068
|
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
|
|
3093
3069
|
: null;
|
|
3094
3070
|
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
@@ -3136,6 +3112,7 @@ export class WorkflowService {
|
|
|
3136
3112
|
: 'failed-step-continuation',
|
|
3137
3113
|
now,
|
|
3138
3114
|
targets: [
|
|
3115
|
+
...(capturedEvidence.target ? [capturedEvidence.target] : []),
|
|
3139
3116
|
...(preparedRemediation?.postimage
|
|
3140
3117
|
? [{
|
|
3141
3118
|
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
@@ -3151,6 +3128,7 @@ export class WorkflowService {
|
|
|
3151
3128
|
validate: () => {
|
|
3152
3129
|
const saved = this.store.readTask(task.projectId, task.id);
|
|
3153
3130
|
const savedStep = requireStep(saved, stepId);
|
|
3131
|
+
assertCapturedCheckEvidence(this.store, saved, savedStep.failurePause, checks);
|
|
3154
3132
|
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3155
3133
|
const remediationHash = preparedRemediation
|
|
3156
3134
|
? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
|
|
@@ -3515,6 +3493,7 @@ export class WorkflowService {
|
|
|
3515
3493
|
prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
|
|
3516
3494
|
const identity = resolveRepositoryIdentity(repository);
|
|
3517
3495
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3496
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3518
3497
|
assertExpectedRevision(task, expectedRevision);
|
|
3519
3498
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
3520
3499
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
@@ -3597,6 +3576,7 @@ export class WorkflowService {
|
|
|
3597
3576
|
const { identity } = this.#mutationContext(repository);
|
|
3598
3577
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
3599
3578
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3579
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3600
3580
|
assertExpectedRevision(task, expectedRevision);
|
|
3601
3581
|
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
3602
3582
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
@@ -3647,12 +3627,52 @@ export class WorkflowService {
|
|
|
3647
3627
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3648
3628
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
3649
3629
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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
|
+
}
|
|
3656
3676
|
}
|
|
3657
3677
|
recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
3658
3678
|
const context = this.#mutationContext(repository);
|
|
@@ -3681,29 +3701,12 @@ export class WorkflowService {
|
|
|
3681
3701
|
stepId,
|
|
3682
3702
|
});
|
|
3683
3703
|
}
|
|
3684
|
-
const assessment =
|
|
3704
|
+
const assessment = this.assessPlanIntegrityRecoveryAdmission(context.identity.repositoryRoot, task, stepId);
|
|
3685
3705
|
if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
|
|
3686
3706
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
|
|
3687
|
-
taskId,
|
|
3688
|
-
stepId,
|
|
3689
|
-
applicable: assessment.applicable,
|
|
3690
|
-
blockers: assessment.blockers,
|
|
3707
|
+
taskId, stepId, applicable: assessment.applicable, blockers: assessment.blockers,
|
|
3691
3708
|
});
|
|
3692
3709
|
}
|
|
3693
|
-
const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
3694
|
-
? [assessment.evidence.remediationEventId]
|
|
3695
|
-
: assessment.evidence.remediationEvents.map((event) => event.eventId);
|
|
3696
|
-
const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
|
|
3697
|
-
&& record.packageVersion === PACKAGE_VERSION
|
|
3698
|
-
&& task.systemCommits.includes(record.commitSha)
|
|
3699
|
-
&& record.planIntegrity?.stepId === stepId
|
|
3700
|
-
&& record.planIntegrity.planHash === task.planHash
|
|
3701
|
-
&& canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
|
|
3702
|
-
&& record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
|
|
3703
|
-
if (!compatibility) {
|
|
3704
|
-
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
3705
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3706
|
-
}
|
|
3707
3710
|
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
3708
3711
|
return { assessment, event };
|
|
3709
3712
|
}
|
|
@@ -3714,10 +3717,14 @@ export class WorkflowService {
|
|
|
3714
3717
|
assertExpectedRevision(task, expectedRevision);
|
|
3715
3718
|
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
3716
3719
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
3717
|
-
|
|
3720
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
3721
|
+
if (!['ready', 'needs_fix', 'blocked'].includes(task.status) && !planningRecovery) {
|
|
3718
3722
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
3719
3723
|
}
|
|
3720
|
-
|
|
3724
|
+
if (planningRecovery && correctiveAudit.decision === 'continue-fix') {
|
|
3725
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A stopped rebound Plan requires replan, split, or escalation; continue-fix cannot restore execution authority.', { taskId, stepId });
|
|
3726
|
+
}
|
|
3727
|
+
const planRiskAudit = readCorrectivePlanningAudit(this.store, task);
|
|
3721
3728
|
if (!planRiskAudit) {
|
|
3722
3729
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
|
|
3723
3730
|
taskId,
|
|
@@ -3750,6 +3757,9 @@ export class WorkflowService {
|
|
|
3750
3757
|
resolution: currentCycle,
|
|
3751
3758
|
});
|
|
3752
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
|
+
}
|
|
3753
3763
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
3754
3764
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
3755
3765
|
}
|
|
@@ -3757,6 +3767,7 @@ export class WorkflowService {
|
|
|
3757
3767
|
const context = this.#mutationContext(repository);
|
|
3758
3768
|
const { identity } = context;
|
|
3759
3769
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3770
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3760
3771
|
assertExpectedRevision(task, expectedRevision);
|
|
3761
3772
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
3762
3773
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Result requires a passing independent review.');
|
|
@@ -3777,6 +3788,7 @@ export class WorkflowService {
|
|
|
3777
3788
|
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
3778
3789
|
const { identity } = this.#mutationContext(repository);
|
|
3779
3790
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
3791
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3780
3792
|
assertExpectedRevision(task, expectedRevision);
|
|
3781
3793
|
if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
|
|
3782
3794
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for final acceptance.`);
|
|
@@ -3823,59 +3835,70 @@ export class WorkflowService {
|
|
|
3823
3835
|
}
|
|
3824
3836
|
syncTaskBase(repository, taskId, expectedRevision) {
|
|
3825
3837
|
const { identity } = this.#mutationContext(repository);
|
|
3826
|
-
|
|
3838
|
+
let task = this.store.readTask(identity.projectId, taskId);
|
|
3827
3839
|
assertExpectedRevision(task, expectedRevision);
|
|
3828
|
-
if (task.
|
|
3829
|
-
|
|
3830
|
-
}
|
|
3831
|
-
const currentBase = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
3832
|
-
if (currentBase === task.baseCommit) {
|
|
3833
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Base branch has not advanced; synchronization is unnecessary.');
|
|
3834
|
-
}
|
|
3835
|
-
const synced = syncBaseIntoTask(identity.repositoryRoot, task.taskBranch, task.baseBranch);
|
|
3836
|
-
const checks = runChecks(identity.repositoryRoot, [...new Set(task.steps.flatMap((step) => step.checks))]);
|
|
3837
|
-
const failed = checks.filter((check) => !check.passed);
|
|
3838
|
-
if (failed.length > 0) {
|
|
3839
|
-
return this.store.writeTask({
|
|
3840
|
-
...task,
|
|
3841
|
-
status: 'needs_fix',
|
|
3842
|
-
baseCommit: synced.baseCommit,
|
|
3843
|
-
systemCommits: [...task.systemCommits, synced.syncCommit],
|
|
3844
|
-
review: emptyReview(),
|
|
3845
|
-
resultHash: null,
|
|
3846
|
-
blockReason: 'Checks failed after synchronizing the base branch.',
|
|
3847
|
-
}, expectedRevision);
|
|
3840
|
+
if (task.pendingBaseSyncValidation !== undefined) {
|
|
3841
|
+
assertPendingBaseSyncValidationReady(task);
|
|
3848
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);
|
|
3849
3886
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
3850
3887
|
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
3851
|
-
taskId: task.id,
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
reason: 'base-sync',
|
|
3855
|
-
checks,
|
|
3856
|
-
steps: task.steps.map((step) => ({ id: step.id, evidence: step.evidence })),
|
|
3857
|
-
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,
|
|
3858
3891
|
}, null, 2));
|
|
3859
|
-
const
|
|
3860
|
-
?
|
|
3861
|
-
|
|
3862
|
-
const saved = this.store.writeTask({
|
|
3863
|
-
...task,
|
|
3864
|
-
status: 'validating',
|
|
3865
|
-
baseCommit: synced.baseCommit,
|
|
3866
|
-
systemCommits: [...task.systemCommits, synced.syncCommit],
|
|
3867
|
-
evidenceHash: stagedEvidence.hash,
|
|
3868
|
-
review: emptyReview(),
|
|
3869
|
-
resultHash: null,
|
|
3870
|
-
authorizations,
|
|
3871
|
-
blockReason: 'Base changed; validation, review, and final acceptance must be repeated.',
|
|
3872
|
-
}, 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);
|
|
3873
3895
|
this.store.publishArtifact(stagedEvidence);
|
|
3874
3896
|
return saved;
|
|
3875
3897
|
}
|
|
3876
3898
|
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
3877
3899
|
const context = this.#mutationContext(repository);
|
|
3878
3900
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
3901
|
+
assertNoPendingBaseSyncValidation(task);
|
|
3879
3902
|
assertExpectedRevision(task, expectedRevision);
|
|
3880
3903
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
3881
3904
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
@@ -4025,137 +4048,6 @@ export class WorkflowService {
|
|
|
4025
4048
|
now: this.now(),
|
|
4026
4049
|
}, actor, confirmationCode);
|
|
4027
4050
|
}
|
|
4028
|
-
pendingReviewUpdatePreflight(repository, taskId) {
|
|
4029
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4030
|
-
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4031
|
-
try {
|
|
4032
|
-
pendingReviewRunnerFingerprint();
|
|
4033
|
-
}
|
|
4034
|
-
catch (error) {
|
|
4035
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4036
|
-
}
|
|
4037
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4038
|
-
let binding = null;
|
|
4039
|
-
if (PACKAGE_VERSION === PENDING_REVIEW_SOURCE_VERSION)
|
|
4040
|
-
blockers.push('The exact target runner must be newer than the supported source.');
|
|
4041
|
-
if (!task)
|
|
4042
|
-
blockers.push('Task not found.');
|
|
4043
|
-
if (task) {
|
|
4044
|
-
try {
|
|
4045
|
-
binding = pendingReviewSourceBinding(snapshot.identity.repositoryRoot, this.store, task);
|
|
4046
|
-
}
|
|
4047
|
-
catch (error) {
|
|
4048
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4049
|
-
}
|
|
4050
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4051
|
-
if ([versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4052
|
-
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PENDING_REVIEW_SOURCE_VERSION)) {
|
|
4053
|
-
blockers.push('All source version surfaces must equal the exact supported beta.13.1.');
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
|
-
const otherRunningSteps = snapshot.tasks.flatMap((candidate) => candidate.steps.filter((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== binding?.stepId)));
|
|
4057
|
-
if (otherRunningSteps.length)
|
|
4058
|
-
blockers.push('No other Step may be running.');
|
|
4059
|
-
const stale = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= this.now().getTime());
|
|
4060
|
-
if (snapshot.leases.some((lease) => !stale.includes(lease)))
|
|
4061
|
-
blockers.push('No live writer lease may exist.');
|
|
4062
|
-
if (stale.some((lease) => lease.entityId !== taskId))
|
|
4063
|
-
blockers.push('No unrelated stale writer lease may exist.');
|
|
4064
|
-
const staleLeaseRepair = blockers.length === 0 && binding
|
|
4065
|
-
? stale.map((lease) => ({ action: 'locks repair', entityId: lease.entityId })) : [];
|
|
4066
|
-
if (stale.length)
|
|
4067
|
-
blockers.push('Repair the exact stale Task lease with the installed source locks repair, then rerun source preflight.');
|
|
4068
|
-
return { readOnly: true, eligible: blockers.length === 0 && binding !== null, binding,
|
|
4069
|
-
bindingHash: binding ? pendingReviewBindingHash(binding) : null, blockers, staleLeaseRepair };
|
|
4070
|
-
}
|
|
4071
|
-
pendingReviewDependencyPreflight(repository, taskId, source) {
|
|
4072
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4073
|
-
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4074
|
-
try {
|
|
4075
|
-
pendingReviewRunnerFingerprint();
|
|
4076
|
-
}
|
|
4077
|
-
catch (error) {
|
|
4078
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4079
|
-
}
|
|
4080
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4081
|
-
const taskHead = headCommit(snapshot.identity.repositoryRoot);
|
|
4082
|
-
let baseTransportCommit = null;
|
|
4083
|
-
if (snapshot.leases.length)
|
|
4084
|
-
blockers.push('No live or stale writer lease may exist.');
|
|
4085
|
-
if (!task)
|
|
4086
|
-
blockers.push('Task not found.');
|
|
4087
|
-
if (task && existsSync(path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)))
|
|
4088
|
-
blockers.push('A pending review update is already recorded.');
|
|
4089
|
-
if (task) {
|
|
4090
|
-
try {
|
|
4091
|
-
// Receipt fields are claims, not authority. Reconstruct every source binding from
|
|
4092
|
-
// canonical Task/sidecars and the exact parents of the candidate Git commits.
|
|
4093
|
-
const parent = runGit(repository, ['rev-parse', 'HEAD^']);
|
|
4094
|
-
baseTransportCommit = runGit(repository, ['rev-parse', task.baseBranch]);
|
|
4095
|
-
const baseParent = runGit(repository, ['rev-parse', `${baseTransportCommit}^`]);
|
|
4096
|
-
const reconstructed = pendingReviewSourceBinding(repository, this.store, task, parent, baseParent);
|
|
4097
|
-
if (canonicalJsonStringify(reconstructed) !== canonicalJsonStringify(source)) {
|
|
4098
|
-
throw new Error('Source receipt does not match reconstructed Task/revision/Plan/HEAD/pending bindings.');
|
|
4099
|
-
}
|
|
4100
|
-
assertPendingReviewTransport(repository, reconstructed, taskHead, baseTransportCommit);
|
|
4101
|
-
if (snapshot.tasks.some((candidate) => candidate.steps.some((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== source.stepId)))) {
|
|
4102
|
-
throw new Error('No other Step may be running.');
|
|
4103
|
-
}
|
|
4104
|
-
const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
|
|
4105
|
-
if ([versions.runtime, versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4106
|
-
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PACKAGE_VERSION)) {
|
|
4107
|
-
throw new Error('All target version surfaces must equal the exact target runner.');
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
catch (error) {
|
|
4111
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
return { readOnly: true, eligible: blockers.length === 0, blockers, taskHead, baseTransportCommit };
|
|
4115
|
-
}
|
|
4116
|
-
recoverPendingReviewDependency(repository, taskId, expectedRevision, source, actor, reason) {
|
|
4117
|
-
if (!actor.trim() || !reason.trim())
|
|
4118
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Actor and reason are required.');
|
|
4119
|
-
const initial = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4120
|
-
if (!initial.eligible)
|
|
4121
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight failed.', initial);
|
|
4122
|
-
const context = this.#mutationContext(repository);
|
|
4123
|
-
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
4124
|
-
assertExpectedRevision(task, expectedRevision);
|
|
4125
|
-
const preflight = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4126
|
-
if (!preflight.eligible || !preflight.baseTransportCommit) {
|
|
4127
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight changed.', preflight);
|
|
4128
|
-
}
|
|
4129
|
-
const now = this.now();
|
|
4130
|
-
const nextTask = { ...task, revision: task.revision + 1, updatedAt: now.toISOString(),
|
|
4131
|
-
systemCommits: [...task.systemCommits, preflight.taskHead],
|
|
4132
|
-
dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
|
|
4133
|
-
commitSha: preflight.taskHead, parentCommitSha: source.completionCommit,
|
|
4134
|
-
packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, files: ['package-lock.json', 'package.json'],
|
|
4135
|
-
actor: actor.trim(), reason: reason.trim(), recordedAt: now.toISOString(),
|
|
4136
|
-
}] };
|
|
4137
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
4138
|
-
const event = preparePendingReviewUpdateEvent({ source, baseTransportCommit: preflight.baseTransportCommit }, preflight.taskHead, now.toISOString());
|
|
4139
|
-
applyProjectTransaction({ projectRoot, projectId: task.projectId,
|
|
4140
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
4141
|
-
kind: 'pending-review-dependency-recovery', now,
|
|
4142
|
-
targets: [{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')).split(path.sep).join('/'),
|
|
4143
|
-
content: `${JSON.stringify(nextTask, null, 2)}\n` },
|
|
4144
|
-
{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)).split(path.sep).join('/'),
|
|
4145
|
-
content: `${canonicalJsonStringify(event)}\n` }],
|
|
4146
|
-
validate: () => {
|
|
4147
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
4148
|
-
if (saved.revision !== nextTask.revision || !verifiedPendingReviewTransport(repository, saved, source.stepId, this.store.taskRoot(task.projectId, task.id))) {
|
|
4149
|
-
throw new WorkflowError('STATE_CORRUPT', 'Pending review dependency recovery readback failed.');
|
|
4150
|
-
}
|
|
4151
|
-
const cycle = buildCurrentStrictStepReviewCycle(this.store, saved, source.stepId);
|
|
4152
|
-
if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== source.pendingEventHash) {
|
|
4153
|
-
throw new WorkflowError('STATE_CORRUPT', 'Pending review changed during dependency recovery.');
|
|
4154
|
-
}
|
|
4155
|
-
},
|
|
4156
|
-
});
|
|
4157
|
-
return this.store.readTask(task.projectId, task.id);
|
|
4158
|
-
}
|
|
4159
4051
|
updatePreflight(repository) {
|
|
4160
4052
|
const snapshot = this.observationSnapshot(repository);
|
|
4161
4053
|
const tasks = snapshot.tasks;
|
|
@@ -4237,6 +4129,69 @@ export class WorkflowService {
|
|
|
4237
4129
|
observation: snapshot.observation,
|
|
4238
4130
|
};
|
|
4239
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
|
+
}
|
|
4240
4195
|
dependencyProvenanceRecoveryPreflight(repository, taskId) {
|
|
4241
4196
|
const snapshot = this.observationSnapshot(repository);
|
|
4242
4197
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -4247,325 +4202,33 @@ export class WorkflowService {
|
|
|
4247
4202
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4248
4203
|
if (!task)
|
|
4249
4204
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4259
|
-
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
|
+
});
|
|
4260
4213
|
}
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
...(preflight.dependencyCommits.length !== 1
|
|
4266
|
-
? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
|
|
4267
|
-
...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
|
|
4268
|
-
? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
|
|
4269
|
-
...(preflight.parentCommitSha
|
|
4270
|
-
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
|
|
4271
|
-
? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
|
|
4272
|
-
...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
|
|
4273
|
-
? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
|
|
4274
|
-
...(!baseDependency
|
|
4275
|
-
|| baseDependency.commitSha !== baseTip
|
|
4276
|
-
|| baseDependency.packageVersion !== PACKAGE_VERSION
|
|
4277
|
-
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
|
|
4278
|
-
!== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
|
|
4279
|
-
? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
|
|
4280
|
-
];
|
|
4281
|
-
return bridgeBlockers.length === 0
|
|
4282
|
-
? preflight
|
|
4283
|
-
: {
|
|
4284
|
-
...preflight,
|
|
4285
|
-
eligible: false,
|
|
4286
|
-
action: null,
|
|
4287
|
-
commitSha: null,
|
|
4288
|
-
parentCommitSha: null,
|
|
4289
|
-
blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
|
|
4290
|
-
};
|
|
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);
|
|
4291
4218
|
}
|
|
4292
|
-
|
|
4219
|
+
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
4293
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
|
+
}
|
|
4294
4226
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4295
4227
|
if (!task)
|
|
4296
4228
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
const sourceSurfaces = {
|
|
4301
|
-
declared: versions.declared,
|
|
4302
|
-
locked: versions.locked,
|
|
4303
|
-
installed: versions.installed,
|
|
4304
|
-
currentBranch: versions.currentBranch,
|
|
4305
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4306
|
-
};
|
|
4307
|
-
const c1 = readTaskC1Posture(this.store, task);
|
|
4308
|
-
if (c1.state !== 'claimed') {
|
|
4309
|
-
return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
|
|
4310
|
-
}
|
|
4311
|
-
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4312
|
-
const now = this.now().getTime();
|
|
4313
|
-
const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
4314
|
-
const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4315
|
-
const transactionCount = snapshot.observation.pendingProjectTransactions.length
|
|
4316
|
-
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4317
|
-
+ snapshot.observation.pendingTaskTransactions.length;
|
|
4318
|
-
const authorizationBlockers = [];
|
|
4319
|
-
if (assessment.eligible) {
|
|
4320
|
-
try {
|
|
4321
|
-
this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
|
|
4322
|
-
}
|
|
4323
|
-
catch (error) {
|
|
4324
|
-
authorizationBlockers.push(error instanceof Error ? error.message : String(error));
|
|
4325
|
-
}
|
|
4326
|
-
}
|
|
4327
|
-
const baseBlockers = [
|
|
4328
|
-
...assessment.blockers,
|
|
4329
|
-
...authorizationBlockers,
|
|
4330
|
-
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4331
|
-
...Object.entries(sourceSurfaces)
|
|
4332
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4333
|
-
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4334
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4335
|
-
...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
|
|
4336
|
-
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4337
|
-
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4338
|
-
...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
|
|
4339
|
-
? ['The exact original Worker C1 claim must remain current.'] : []),
|
|
4340
|
-
...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
|
|
4341
|
-
...(staleLeases.some((lease) => lease.entityId !== task.id
|
|
4342
|
-
|| c1.state !== 'claimed' || lease.owner !== c1.claimant
|
|
4343
|
-
|| createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
|
|
4344
|
-
? ['No unrelated or differently owned stale writer lease may exist.'] : []),
|
|
4345
|
-
...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
|
|
4346
|
-
];
|
|
4347
|
-
const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
|
|
4348
|
-
? staleLeases[0]
|
|
4349
|
-
: null;
|
|
4350
|
-
const blockers = [
|
|
4351
|
-
...baseBlockers,
|
|
4352
|
-
...(repairableStale
|
|
4353
|
-
? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
|
|
4354
|
-
: []),
|
|
4355
|
-
];
|
|
4356
|
-
const compatibility = assessment.compatibility;
|
|
4357
|
-
return {
|
|
4358
|
-
readOnly: true,
|
|
4359
|
-
eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
|
|
4360
|
-
action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
|
|
4361
|
-
? 'update corrective-carryover-transport'
|
|
4362
|
-
: null,
|
|
4363
|
-
projectId: task.projectId,
|
|
4364
|
-
taskId: task.id,
|
|
4365
|
-
taskRevision: task.revision,
|
|
4366
|
-
stepId: compatibility?.stepId ?? null,
|
|
4367
|
-
sourceVersion,
|
|
4368
|
-
targetVersion,
|
|
4369
|
-
sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
|
|
4370
|
-
taskBranch: task.taskBranch,
|
|
4371
|
-
baseBranch: task.baseBranch,
|
|
4372
|
-
dirtyFiles: compatibility?.dirtyFiles ?? [],
|
|
4373
|
-
dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
|
|
4374
|
-
compatibilityHash: compatibility?.compatibilityHash ?? null,
|
|
4375
|
-
c1Posture: c1.state === 'claimed' ? {
|
|
4376
|
-
state: 'claimed',
|
|
4377
|
-
claimant: c1.claimant,
|
|
4378
|
-
claimEventId: compatibility?.c1ClaimEventId ?? null,
|
|
4379
|
-
claimEventHash: compatibility?.c1ClaimEventHash ?? null,
|
|
4380
|
-
} : null,
|
|
4381
|
-
staleLeaseRepair: repairableStale ? [{
|
|
4382
|
-
action: 'locks repair',
|
|
4383
|
-
entityId: task.id,
|
|
4384
|
-
owner: repairableStale.owner,
|
|
4385
|
-
expiresAt: repairableStale.expiresAt,
|
|
4386
|
-
}] : [],
|
|
4387
|
-
requiredTransport: [
|
|
4388
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4389
|
-
'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
|
|
4390
|
-
'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
|
|
4391
|
-
],
|
|
4392
|
-
blockers,
|
|
4393
|
-
};
|
|
4394
|
-
}
|
|
4395
|
-
consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
|
|
4396
|
-
const repositoryRoot = snapshot.identity.repositoryRoot;
|
|
4397
|
-
const sourceVersion = '2.0.0-beta.13.6';
|
|
4398
|
-
const targetVersion = '2.0.0-beta.13.8';
|
|
4399
|
-
const c1 = readTaskC1Posture(this.store, task);
|
|
4400
|
-
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
|
|
4401
|
-
const currentHead = headCommit(repositoryRoot);
|
|
4402
|
-
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
4403
|
-
const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
4404
|
-
const navigation = this.next(repositoryRoot, task.id);
|
|
4405
|
-
const action = String(navigation.action);
|
|
4406
|
-
const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
|
|
4407
|
-
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
4408
|
-
const terminal = readRemediationEvents(this.store, task)
|
|
4409
|
-
.filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
|
|
4410
|
-
const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
|
|
4411
|
-
const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
|
|
4412
|
-
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
|
|
4413
|
-
const missingClaimOnly = assessment.blockers.length === 1
|
|
4414
|
-
&& assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
|
|
4415
|
-
const rollingWave = navigation.rollingWave;
|
|
4416
|
-
const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
|
|
4417
|
-
&& rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
|
|
4418
|
-
|| (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
|
|
4419
|
-
|| (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
|
|
4420
|
-
&& c1.state === 'pending' && c1.targetActor === yielded?.actor);
|
|
4421
|
-
const blockers = [
|
|
4422
|
-
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4423
|
-
...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
|
|
4424
|
-
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4425
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4426
|
-
...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
|
|
4427
|
-
...(snapshot.observation.pendingProjectTransactions.length
|
|
4428
|
-
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4429
|
-
+ snapshot.observation.pendingTaskTransactions.length
|
|
4430
|
-
+ snapshot.observation.corruptTaskTransactions.length
|
|
4431
|
-
+ snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
|
|
4432
|
-
...(!recorded || recorded.stepId === stepId
|
|
4433
|
-
|| task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
|
|
4434
|
-
? ['A different, verified completed Step must own the historical carryover binding.'] : []),
|
|
4435
|
-
...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
|
|
4436
|
-
...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
|
|
4437
|
-
|| task.workspaceOwner !== 'local'
|
|
4438
|
-
|| currentBranch(repositoryRoot) !== task.taskBranch
|
|
4439
|
-
|| task.steps.some((candidate) => candidate.status === 'in_progress')
|
|
4440
|
-
? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
|
|
4441
|
-
...(!step || dirtyFiles.length === 0
|
|
4442
|
-
|| dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
|
|
4443
|
-
? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
|
|
4444
|
-
...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
|
|
4445
|
-
|| terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
|
|
4446
|
-
? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
|
|
4447
|
-
...(!yielded || !terminal || yielded.planHash === null
|
|
4448
|
-
|| !Number.isFinite(Date.parse(yielded.recordedAt))
|
|
4449
|
-
|| Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
|
|
4450
|
-
? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
|
|
4451
|
-
];
|
|
4452
|
-
let candidateBinding = null;
|
|
4453
|
-
if (action === 'task plan-set') {
|
|
4454
|
-
if (!candidate) {
|
|
4455
|
-
blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
|
|
4456
|
-
}
|
|
4457
|
-
else {
|
|
4458
|
-
try {
|
|
4459
|
-
validatePlan(candidate.plan);
|
|
4460
|
-
const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
|
|
4461
|
-
if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
|
|
4462
|
-
throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
|
|
4463
|
-
}
|
|
4464
|
-
const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
|
|
4465
|
-
if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
|
|
4466
|
-
throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
|
|
4467
|
-
}
|
|
4468
|
-
if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
|
|
4469
|
-
throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
|
|
4470
|
-
}
|
|
4471
|
-
if (candidate.plan.objective !== task.planObjective
|
|
4472
|
-
|| canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
|
|
4473
|
-
|| canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
|
|
4474
|
-
throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
|
|
4475
|
-
}
|
|
4476
|
-
const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
|
|
4477
|
-
this.validateKnowledgeTargets(map, candidate.plan);
|
|
4478
|
-
const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
|
|
4479
|
-
if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
|
|
4480
|
-
=== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
|
|
4481
|
-
throw new Error('Candidate must change the failed Plan semantics before source recovery.');
|
|
4482
|
-
}
|
|
4483
|
-
if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
|
|
4484
|
-
throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
|
|
4485
|
-
}
|
|
4486
|
-
const auditTask = {
|
|
4487
|
-
...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
|
|
4488
|
-
knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
|
|
4489
|
-
knowledgeMapHash: candidate.plan.knowledgeMapHash,
|
|
4490
|
-
steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
|
|
4491
|
-
...candidateStep, status: 'planned', evidence: null, failurePause: null,
|
|
4492
|
-
}),
|
|
4493
|
-
};
|
|
4494
|
-
validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
|
|
4495
|
-
candidateBinding = {
|
|
4496
|
-
candidatePlanHash: prepared.planHash,
|
|
4497
|
-
candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
|
|
4498
|
-
riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
|
|
4499
|
-
};
|
|
4500
|
-
}
|
|
4501
|
-
catch (error) {
|
|
4502
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
}
|
|
4506
|
-
try {
|
|
4507
|
-
validateTaskHistory(repositoryRoot, task);
|
|
4508
|
-
this.assertTaskKnowledgeBinding(repositoryRoot, task);
|
|
4509
|
-
const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
|
|
4510
|
-
if (!sourceAudit || !yielded?.planHash || !task.planHash
|
|
4511
|
-
|| normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
|
|
4512
|
-
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
|
|
4513
|
-
throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
|
|
4514
|
-
}
|
|
4515
|
-
}
|
|
4516
|
-
catch (error) {
|
|
4517
|
-
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4518
|
-
}
|
|
4519
|
-
const binding = {
|
|
4520
|
-
taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
|
|
4521
|
-
planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
|
|
4522
|
-
sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
|
|
4523
|
-
completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
|
|
4524
|
-
terminalRemediationEventHash: terminal?.eventHash ?? null,
|
|
4525
|
-
correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
|
|
4526
|
-
c1Posture: c1.state,
|
|
4527
|
-
candidateBinding,
|
|
4528
|
-
};
|
|
4529
|
-
return {
|
|
4530
|
-
readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
|
|
4531
|
-
action: null, transportAllowed: false,
|
|
4532
|
-
...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
|
|
4533
|
-
compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
|
|
4534
|
-
sourceRecovery: blockers.length === 0 ? {
|
|
4535
|
-
eligible: true, action,
|
|
4536
|
-
...(action === 'task plan-set' ? { ...candidateBinding } : {}),
|
|
4537
|
-
...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
|
|
4538
|
-
...(action === 'task claim' ? { actor: yielded.actor } : {}),
|
|
4539
|
-
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.',
|
|
4540
|
-
} : null,
|
|
4541
|
-
blockers,
|
|
4542
|
-
};
|
|
4543
|
-
}
|
|
4544
|
-
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
4545
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4546
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4547
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
|
|
4548
|
-
observation: snapshot.observation,
|
|
4549
|
-
});
|
|
4550
|
-
}
|
|
4551
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4552
|
-
if (!task)
|
|
4553
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4554
|
-
return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
|
|
4555
|
-
}
|
|
4556
|
-
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
4557
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4558
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4559
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
|
|
4560
|
-
observation: snapshot.observation,
|
|
4561
|
-
});
|
|
4562
|
-
}
|
|
4563
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4564
|
-
if (!task)
|
|
4565
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4566
|
-
let candidateParent = null;
|
|
4567
|
-
try {
|
|
4568
|
-
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4229
|
+
let candidateParent = null;
|
|
4230
|
+
try {
|
|
4231
|
+
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4569
4232
|
}
|
|
4570
4233
|
catch {
|
|
4571
4234
|
candidateParent = null;
|
|
@@ -4610,7 +4273,7 @@ export class WorkflowService {
|
|
|
4610
4273
|
])],
|
|
4611
4274
|
};
|
|
4612
4275
|
}
|
|
4613
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null,
|
|
4276
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, {
|
|
4614
4277
|
activeStepId: assessment.activeStepId,
|
|
4615
4278
|
planHash: task.planHash,
|
|
4616
4279
|
dirtyFiles: assessment.dirtyFiles,
|
|
@@ -4709,289 +4372,15 @@ export class WorkflowService {
|
|
|
4709
4372
|
invalidatedStepIds: assessment.invalidatedSteps.map((step) => step.stepId),
|
|
4710
4373
|
});
|
|
4711
4374
|
}
|
|
4712
|
-
downstreamProofReplanUpdatePreflight(repository, taskId) {
|
|
4713
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4714
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4715
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan update preflight requires a coherent observation boundary.', {
|
|
4716
|
-
observation: snapshot.observation,
|
|
4717
|
-
});
|
|
4718
|
-
}
|
|
4719
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4720
|
-
if (!task)
|
|
4721
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4722
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
4723
|
-
const assessment = assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
4724
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4725
|
-
const sourceVersion = '2.0.0-beta.12.6';
|
|
4726
|
-
const versionSurfaces = {
|
|
4727
|
-
declared: versions.declared,
|
|
4728
|
-
locked: versions.locked,
|
|
4729
|
-
installed: versions.installed,
|
|
4730
|
-
currentBranch: versions.currentBranch,
|
|
4731
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4732
|
-
};
|
|
4733
|
-
const blockers = [
|
|
4734
|
-
...assessment.blockers,
|
|
4735
|
-
...(String(PACKAGE_VERSION) === sourceVersion ? ['Recovery runner must be newer than the beta.12.6 source package.'] : []),
|
|
4736
|
-
...Object.entries(versionSurfaces)
|
|
4737
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4738
|
-
.map(([surface]) => `${surface} Workflow version must equal the stranded source ${sourceVersion}.`),
|
|
4739
|
-
...(snapshot.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4740
|
-
...(snapshot.observation.pendingProjectTransactions.length > 0
|
|
4741
|
-
|| snapshot.observation.pendingMilestoneTransactions.length > 0
|
|
4742
|
-
|| snapshot.observation.pendingTaskTransactions.length > 0
|
|
4743
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
4744
|
-
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4745
|
-
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4746
|
-
];
|
|
4747
|
-
return {
|
|
4748
|
-
readOnly: true,
|
|
4749
|
-
eligible: assessment.applicable && assessment.eligible && blockers.length === 0,
|
|
4750
|
-
action: assessment.applicable && assessment.eligible && blockers.length === 0
|
|
4751
|
-
? 'update downstream-proof-replan-transport'
|
|
4752
|
-
: null,
|
|
4753
|
-
projectId: task.projectId,
|
|
4754
|
-
taskId: task.id,
|
|
4755
|
-
taskRevision: task.revision,
|
|
4756
|
-
sourceVersion,
|
|
4757
|
-
targetVersion: PACKAGE_VERSION,
|
|
4758
|
-
sourceHeadCommit: assessment.invalidation?.preservedHeadCommit ?? null,
|
|
4759
|
-
taskBranch: task.taskBranch,
|
|
4760
|
-
baseBranch: task.baseBranch,
|
|
4761
|
-
dirtyFiles: assessment.compatibility?.dirtyFiles ?? [],
|
|
4762
|
-
dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
|
|
4763
|
-
requiredTransport: [
|
|
4764
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4765
|
-
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
4766
|
-
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
4767
|
-
],
|
|
4768
|
-
blockers,
|
|
4769
|
-
};
|
|
4770
|
-
}
|
|
4771
|
-
planIntegrityUpdatePreflight(repository, taskId) {
|
|
4772
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4773
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4774
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
|
|
4775
|
-
observation: snapshot.observation,
|
|
4776
|
-
});
|
|
4777
|
-
}
|
|
4778
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4779
|
-
if (!task)
|
|
4780
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4781
|
-
const assessments = task.steps
|
|
4782
|
-
.filter((step) => step.status === 'failed')
|
|
4783
|
-
.map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
|
|
4784
|
-
.filter((assessment) => assessment.applicable);
|
|
4785
|
-
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4786
|
-
const adoptedCompatibility = assessment
|
|
4787
|
-
? null
|
|
4788
|
-
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4789
|
-
const dirtyCarryoverAssessment = assessment || adoptedCompatibility
|
|
4790
|
-
? null
|
|
4791
|
-
: this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4792
|
-
const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
|
|
4793
|
-
&& dirtyCarryoverAssessment.compatibility
|
|
4794
|
-
? {
|
|
4795
|
-
...dirtyCarryoverAssessment.compatibility,
|
|
4796
|
-
planHash: task.planHash,
|
|
4797
|
-
mode: 'post-rebind-dirty-carryover',
|
|
4798
|
-
}
|
|
4799
|
-
: null;
|
|
4800
|
-
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4801
|
-
const versionSurfaces = {
|
|
4802
|
-
declared: versions.declared,
|
|
4803
|
-
locked: versions.locked,
|
|
4804
|
-
installed: versions.installed,
|
|
4805
|
-
currentBranch: versions.currentBranch,
|
|
4806
|
-
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4807
|
-
};
|
|
4808
|
-
const sourceVersion = versionSurfaces.declared;
|
|
4809
|
-
const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
|
|
4810
|
-
const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
|
|
4811
|
-
const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
|
|
4812
|
-
&& sourceBetaVersion.patch !== null
|
|
4813
|
-
&& sourceBetaVersion.patch >= 11;
|
|
4814
|
-
const targetIsNewer = sourceBetaVersion !== null
|
|
4815
|
-
&& targetBetaVersion !== null
|
|
4816
|
-
&& (targetBetaVersion.train > sourceBetaVersion.train
|
|
4817
|
-
|| (targetBetaVersion.train === sourceBetaVersion.train
|
|
4818
|
-
&& targetBetaVersion.patch !== null
|
|
4819
|
-
&& sourceBetaVersion.patch !== null
|
|
4820
|
-
&& targetBetaVersion.patch > sourceBetaVersion.patch));
|
|
4821
|
-
const blockers = [
|
|
4822
|
-
...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
|
|
4823
|
-
...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
|
|
4824
|
-
? []
|
|
4825
|
-
: (dirtyCarryoverAssessment?.blockers.length
|
|
4826
|
-
? dirtyCarryoverAssessment.blockers
|
|
4827
|
-
: ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
|
|
4828
|
-
...(!sourceIsEligibleBeta12 || !targetIsNewer
|
|
4829
|
-
? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
|
|
4830
|
-
: []),
|
|
4831
|
-
...Object.entries(versionSurfaces)
|
|
4832
|
-
.filter(([, version]) => version !== sourceVersion)
|
|
4833
|
-
.map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
|
|
4834
|
-
...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4835
|
-
...observationPreflightBlockers(snapshot.observation),
|
|
4836
|
-
];
|
|
4837
|
-
return {
|
|
4838
|
-
readOnly: true,
|
|
4839
|
-
eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
|
|
4840
|
-
action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
|
|
4841
|
-
? 'update plan-integrity-transport'
|
|
4842
|
-
: null,
|
|
4843
|
-
projectId: task.projectId,
|
|
4844
|
-
taskId: task.id,
|
|
4845
|
-
taskRevision: task.revision,
|
|
4846
|
-
stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
|
|
4847
|
-
sourceVersion,
|
|
4848
|
-
targetVersion: PACKAGE_VERSION,
|
|
4849
|
-
taskBranch: task.taskBranch,
|
|
4850
|
-
baseBranch: task.baseBranch,
|
|
4851
|
-
dirtyFiles: assessment?.evidence?.changedFiles
|
|
4852
|
-
?? adoptedCompatibility?.dirtyFiles
|
|
4853
|
-
?? dirtyCarryoverCompatibility?.dirtyFiles
|
|
4854
|
-
?? [],
|
|
4855
|
-
conflictHash: assessment?.evidence?.conflictHash
|
|
4856
|
-
?? adoptedCompatibility?.conflictHash
|
|
4857
|
-
?? dirtyCarryoverCompatibility?.conflictHash
|
|
4858
|
-
?? null,
|
|
4859
|
-
compatibilityMode: adoptedCompatibility?.mode
|
|
4860
|
-
?? dirtyCarryoverCompatibility?.mode
|
|
4861
|
-
?? 'failed-step-conflict',
|
|
4862
|
-
requiredTransport: [
|
|
4863
|
-
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4864
|
-
'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
|
|
4865
|
-
'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
|
|
4866
|
-
],
|
|
4867
|
-
blockers,
|
|
4868
|
-
};
|
|
4869
|
-
}
|
|
4870
|
-
planIntegrityDependencyRecoveryPreflight(repository, taskId) {
|
|
4871
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4872
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4873
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
|
|
4874
|
-
observation: snapshot.observation,
|
|
4875
|
-
});
|
|
4876
|
-
}
|
|
4877
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4878
|
-
if (!task)
|
|
4879
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4880
|
-
const assessmentBlockers = [];
|
|
4881
|
-
const assessments = task.steps
|
|
4882
|
-
.filter((step) => step.status === 'failed')
|
|
4883
|
-
.flatMap((step) => {
|
|
4884
|
-
try {
|
|
4885
|
-
return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
|
|
4886
|
-
}
|
|
4887
|
-
catch (error) {
|
|
4888
|
-
if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
|
|
4889
|
-
assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
|
|
4890
|
-
return [];
|
|
4891
|
-
}
|
|
4892
|
-
throw error;
|
|
4893
|
-
}
|
|
4894
|
-
})
|
|
4895
|
-
.filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
|
|
4896
|
-
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4897
|
-
const adoptedCompatibility = assessment?.evidence
|
|
4898
|
-
? null
|
|
4899
|
-
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4900
|
-
const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
|
|
4901
|
-
? null
|
|
4902
|
-
: this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
|
|
4903
|
-
const carriedCompatibility = !assessment?.evidence && task.planHash
|
|
4904
|
-
? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
|
|
4905
|
-
: null;
|
|
4906
|
-
if ((!assessment?.evidence
|
|
4907
|
-
&& !carriedCompatibility
|
|
4908
|
-
&& !adoptedCompatibility
|
|
4909
|
-
&& !dirtyCarryoverCompatibility) || !task.planHash) {
|
|
4910
|
-
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4911
|
-
return {
|
|
4912
|
-
...ordinary,
|
|
4913
|
-
eligible: false,
|
|
4914
|
-
action: null,
|
|
4915
|
-
planIntegrity: null,
|
|
4916
|
-
blockers: [...new Set([
|
|
4917
|
-
...ordinary.blockers,
|
|
4918
|
-
...assessmentBlockers,
|
|
4919
|
-
'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
|
|
4920
|
-
])],
|
|
4921
|
-
};
|
|
4922
|
-
}
|
|
4923
|
-
const compatibility = carriedCompatibility
|
|
4924
|
-
?? adoptedCompatibility
|
|
4925
|
-
?? dirtyCarryoverCompatibility
|
|
4926
|
-
?? {
|
|
4927
|
-
stepId: assessment.stepId,
|
|
4928
|
-
planHash: task.planHash,
|
|
4929
|
-
conflictHash: assessment.evidence.conflictHash,
|
|
4930
|
-
dirtyFiles: [...assessment.evidence.changedFiles],
|
|
4931
|
-
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
|
|
4932
|
-
remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
4933
|
-
? [assessment.evidence.remediationEventId]
|
|
4934
|
-
: assessment.evidence.remediationEvents.map((event) => event.eventId),
|
|
4935
|
-
};
|
|
4936
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
|
|
4937
|
-
}
|
|
4938
|
-
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
4939
|
-
const snapshot = this.observationSnapshot(repository);
|
|
4940
|
-
if (snapshot.observation.kind !== 'assessment') {
|
|
4941
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan dependency recovery requires a coherent observation boundary.', {
|
|
4942
|
-
observation: snapshot.observation,
|
|
4943
|
-
});
|
|
4944
|
-
}
|
|
4945
|
-
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4946
|
-
if (!task)
|
|
4947
|
-
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4948
|
-
let candidateParent = null;
|
|
4949
|
-
try {
|
|
4950
|
-
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
4951
|
-
}
|
|
4952
|
-
catch {
|
|
4953
|
-
candidateParent = null;
|
|
4954
|
-
}
|
|
4955
|
-
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
4956
|
-
const assessment = candidateParent
|
|
4957
|
-
? assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, {
|
|
4958
|
-
historyBoundary: 'HEAD^',
|
|
4959
|
-
preservedHeadCommit: candidateParent,
|
|
4960
|
-
currentPlanRiskAuditExists,
|
|
4961
|
-
})
|
|
4962
|
-
: null;
|
|
4963
|
-
if (!assessment?.applicable || !assessment.eligible || !assessment.compatibility) {
|
|
4964
|
-
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4965
|
-
return {
|
|
4966
|
-
...ordinary,
|
|
4967
|
-
eligible: false,
|
|
4968
|
-
action: null,
|
|
4969
|
-
activeDownstreamProof: null,
|
|
4970
|
-
downstreamProofReplan: null,
|
|
4971
|
-
blockers: [...new Set([
|
|
4972
|
-
...ordinary.blockers,
|
|
4973
|
-
...(assessment?.blockers ?? []),
|
|
4974
|
-
'The Task must preserve the exact beta.12.6 downstream-proof invalidation and accidental knowledge-rebind boundary.',
|
|
4975
|
-
])],
|
|
4976
|
-
};
|
|
4977
|
-
}
|
|
4978
|
-
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, assessment.compatibility);
|
|
4979
|
-
}
|
|
4980
4375
|
recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4981
4376
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, false);
|
|
4982
4377
|
}
|
|
4983
4378
|
recoverActiveDownstreamProofDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4984
4379
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, true);
|
|
4985
4380
|
}
|
|
4986
|
-
recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4987
|
-
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
|
|
4988
|
-
}
|
|
4989
4381
|
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4990
4382
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
4991
4383
|
}
|
|
4992
|
-
recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4993
|
-
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
|
|
4994
|
-
}
|
|
4995
4384
|
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
4996
4385
|
const normalizedActor = actor.trim();
|
|
4997
4386
|
const normalizedReason = reason.trim();
|
|
@@ -5004,15 +4393,9 @@ export class WorkflowService {
|
|
|
5004
4393
|
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
5005
4394
|
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead
|
|
5006
4395
|
&& record.packageVersion === PACKAGE_VERSION
|
|
5007
|
-
&& (recoveryMode === '
|
|
5008
|
-
? Boolean(record.
|
|
5009
|
-
:
|
|
5010
|
-
? Boolean(record.historicalStepProvenance)
|
|
5011
|
-
: recoveryMode === 'plan-integrity'
|
|
5012
|
-
? Boolean(record.planIntegrity)
|
|
5013
|
-
: recoveryMode === false && record.rootCauseReplan
|
|
5014
|
-
? true
|
|
5015
|
-
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
4396
|
+
&& (recoveryMode === 'historical-step'
|
|
4397
|
+
? Boolean(record.historicalStepProvenance)
|
|
4398
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
5016
4399
|
if (prior && current.systemCommits.includes(currentHead))
|
|
5017
4400
|
return current;
|
|
5018
4401
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -5020,34 +4403,17 @@ export class WorkflowService {
|
|
|
5020
4403
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
5021
4404
|
if (!task)
|
|
5022
4405
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
5023
|
-
const preflight = recoveryMode === '
|
|
5024
|
-
? this.
|
|
5025
|
-
: recoveryMode
|
|
5026
|
-
? this.
|
|
5027
|
-
:
|
|
5028
|
-
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
5029
|
-
: recoveryMode
|
|
5030
|
-
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
5031
|
-
: 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);
|
|
5032
4411
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
5033
4412
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
5034
4413
|
taskId,
|
|
5035
4414
|
blockers: preflight.blockers,
|
|
5036
4415
|
});
|
|
5037
4416
|
}
|
|
5038
|
-
if (preflight.rootCauseReplan) {
|
|
5039
|
-
const posture = readTaskC1Posture(this.store, task);
|
|
5040
|
-
if (posture.state !== 'claimed'
|
|
5041
|
-
|| posture.claimant !== preflight.rootCauseReplan.claimant
|
|
5042
|
-
|| normalizedActor !== posture.claimant) {
|
|
5043
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
|
|
5044
|
-
taskId,
|
|
5045
|
-
actor: normalizedActor,
|
|
5046
|
-
requiredActor: preflight.rootCauseReplan.claimant,
|
|
5047
|
-
posture: posture.state,
|
|
5048
|
-
});
|
|
5049
|
-
}
|
|
5050
|
-
}
|
|
5051
4417
|
const now = this.now();
|
|
5052
4418
|
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
5053
4419
|
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
@@ -5064,18 +4430,9 @@ export class WorkflowService {
|
|
|
5064
4430
|
...(isTerminalCommit && preflight.activeDownstreamProof
|
|
5065
4431
|
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
5066
4432
|
: {}),
|
|
5067
|
-
...(isTerminalCommit && preflight.downstreamProofReplan
|
|
5068
|
-
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
5069
|
-
: {}),
|
|
5070
4433
|
...(isTerminalCommit && preflight.historicalStepProvenance
|
|
5071
4434
|
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
5072
4435
|
: {}),
|
|
5073
|
-
...(isTerminalCommit && preflight.planIntegrity
|
|
5074
|
-
? { planIntegrity: preflight.planIntegrity }
|
|
5075
|
-
: {}),
|
|
5076
|
-
...(isTerminalCommit && preflight.rootCauseReplan
|
|
5077
|
-
? { rootCauseReplan: preflight.rootCauseReplan }
|
|
5078
|
-
: {}),
|
|
5079
4436
|
};
|
|
5080
4437
|
});
|
|
5081
4438
|
const nextTask = {
|
|
@@ -5091,62 +4448,7 @@ export class WorkflowService {
|
|
|
5091
4448
|
...recoveredRecords,
|
|
5092
4449
|
],
|
|
5093
4450
|
};
|
|
5094
|
-
|
|
5095
|
-
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
5096
|
-
}
|
|
5097
|
-
const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
|
|
5098
|
-
const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
|
|
5099
|
-
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
5100
|
-
let refreshedKnowledgeMap = null;
|
|
5101
|
-
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
5102
|
-
const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
|
|
5103
|
-
if (!knowledgeRefresh.eligible) {
|
|
5104
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
|
|
5105
|
-
taskId,
|
|
5106
|
-
dependencyKnowledgeRefresh: knowledgeRefresh,
|
|
5107
|
-
});
|
|
5108
|
-
}
|
|
5109
|
-
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
5110
|
-
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
5111
|
-
if (!approved.approval) {
|
|
5112
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
|
|
5113
|
-
}
|
|
5114
|
-
refreshedKnowledgeMap = {
|
|
5115
|
-
...approved,
|
|
5116
|
-
revision: currentKnowledgeMap.revision + 2,
|
|
5117
|
-
createdAt: currentKnowledgeMap.createdAt,
|
|
5118
|
-
updatedAt: now.toISOString(),
|
|
5119
|
-
approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
|
|
5120
|
-
};
|
|
5121
|
-
}
|
|
5122
|
-
const projectRoot = this.store.projectRoot(task.projectId);
|
|
5123
|
-
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
5124
|
-
applyProjectTransaction({
|
|
5125
|
-
projectRoot,
|
|
5126
|
-
projectId: task.projectId,
|
|
5127
|
-
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
5128
|
-
kind: 'plan-integrity-dependency-recovery',
|
|
5129
|
-
now,
|
|
5130
|
-
targets: [
|
|
5131
|
-
...(refreshedKnowledgeMap
|
|
5132
|
-
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
5133
|
-
: []),
|
|
5134
|
-
{
|
|
5135
|
-
path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
|
|
5136
|
-
content: `${JSON.stringify(nextTask, null, 2)}\n`,
|
|
5137
|
-
},
|
|
5138
|
-
],
|
|
5139
|
-
validate: () => {
|
|
5140
|
-
const saved = this.store.readTask(task.projectId, task.id);
|
|
5141
|
-
if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
|
|
5142
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
|
|
5143
|
-
}
|
|
5144
|
-
if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
|
|
5145
|
-
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
|
|
5146
|
-
}
|
|
5147
|
-
},
|
|
5148
|
-
});
|
|
5149
|
-
return this.store.readTask(task.projectId, task.id);
|
|
4451
|
+
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
5150
4452
|
}
|
|
5151
4453
|
milestoneProgress(repository, milestoneId) {
|
|
5152
4454
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -5214,109 +4516,6 @@ export class WorkflowService {
|
|
|
5214
4516
|
tasks,
|
|
5215
4517
|
};
|
|
5216
4518
|
}
|
|
5217
|
-
alpha6StrictReviewRescuePreflight(repository) {
|
|
5218
|
-
const context = this.#mutationContext(repository);
|
|
5219
|
-
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
5220
|
-
const tasks = context.store.listTasks(context.identity.projectId);
|
|
5221
|
-
const leases = context.locks.list();
|
|
5222
|
-
const now = this.now().getTime();
|
|
5223
|
-
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
5224
|
-
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
5225
|
-
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
5226
|
-
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
5227
|
-
const pendingCandidates = [];
|
|
5228
|
-
const malformedCandidates = [];
|
|
5229
|
-
for (const task of tasks) {
|
|
5230
|
-
if (task.status !== 'in_progress')
|
|
5231
|
-
continue;
|
|
5232
|
-
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
5233
|
-
if (!planRiskAudit)
|
|
5234
|
-
continue;
|
|
5235
|
-
for (const step of task.steps) {
|
|
5236
|
-
if (step.status !== 'in_progress' || !step.evidence)
|
|
5237
|
-
continue;
|
|
5238
|
-
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
5239
|
-
continue;
|
|
5240
|
-
try {
|
|
5241
|
-
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
5242
|
-
if (cycle.resolution !== 'pending')
|
|
5243
|
-
continue;
|
|
5244
|
-
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
5245
|
-
let knowledgeBindingStale = false;
|
|
5246
|
-
try {
|
|
5247
|
-
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
5248
|
-
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
5249
|
-
knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
5250
|
-
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
5251
|
-
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
5252
|
-
}
|
|
5253
|
-
catch {
|
|
5254
|
-
knowledgeBindingStale = true;
|
|
5255
|
-
}
|
|
5256
|
-
pendingCandidates.push({
|
|
5257
|
-
taskId: task.id,
|
|
5258
|
-
taskRevision: task.revision,
|
|
5259
|
-
stepId: step.id,
|
|
5260
|
-
completionCommit: step.evidence.commitSha,
|
|
5261
|
-
currentHead,
|
|
5262
|
-
knowledgeBindingStale,
|
|
5263
|
-
});
|
|
5264
|
-
}
|
|
5265
|
-
catch (error) {
|
|
5266
|
-
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
5267
|
-
}
|
|
5268
|
-
}
|
|
5269
|
-
}
|
|
5270
|
-
const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
|
|
5271
|
-
const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
|
|
5272
|
-
const blockers = [
|
|
5273
|
-
...(dependency.declared !== '2.0.0-alpha.6'
|
|
5274
|
-
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
5275
|
-
: []),
|
|
5276
|
-
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
5277
|
-
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
5278
|
-
...(pendingCandidates.length !== 1
|
|
5279
|
-
? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
|
|
5280
|
-
: []),
|
|
5281
|
-
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
5282
|
-
? ['HEAD does not match the pending strict review completion commit.']
|
|
5283
|
-
: []),
|
|
5284
|
-
...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
|
|
5285
|
-
? ['A stale writer lease exists outside the rescue Task.']
|
|
5286
|
-
: []),
|
|
5287
|
-
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
5288
|
-
];
|
|
5289
|
-
const eligible = blockers.length === 0 && candidate !== null;
|
|
5290
|
-
const requiredActions = eligible
|
|
5291
|
-
? [
|
|
5292
|
-
...staleLeases.map((lease) => ({
|
|
5293
|
-
action: 'locks repair',
|
|
5294
|
-
entityId: lease.entityId,
|
|
5295
|
-
})),
|
|
5296
|
-
{
|
|
5297
|
-
action: 'task step-review',
|
|
5298
|
-
taskId: candidate.taskId,
|
|
5299
|
-
stepId: candidate.stepId,
|
|
5300
|
-
expectedRevision: candidate.taskRevision,
|
|
5301
|
-
},
|
|
5302
|
-
]
|
|
5303
|
-
: [];
|
|
5304
|
-
return {
|
|
5305
|
-
eligible,
|
|
5306
|
-
mutationFree: true,
|
|
5307
|
-
runnerPackageVersion: PACKAGE_VERSION,
|
|
5308
|
-
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
5309
|
-
declaredProjectPackageVersion: dependency.declared,
|
|
5310
|
-
projectId: context.identity.projectId,
|
|
5311
|
-
repositoryRoot: context.identity.repositoryRoot,
|
|
5312
|
-
clean: dirty.length === 0,
|
|
5313
|
-
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
5314
|
-
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
5315
|
-
candidate,
|
|
5316
|
-
blockers,
|
|
5317
|
-
requiredActions,
|
|
5318
|
-
};
|
|
5319
|
-
}
|
|
5320
4519
|
scanKnowledgeMap(repository) {
|
|
5321
4520
|
const { identity } = this.#mutationContext(repository);
|
|
5322
4521
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
@@ -5383,10 +4582,13 @@ export class WorkflowService {
|
|
|
5383
4582
|
return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
5384
4583
|
}
|
|
5385
4584
|
graphStatus(repository, graphKind, mode = 'moderate') {
|
|
5386
|
-
|
|
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() });
|
|
5387
4589
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
5388
4590
|
const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
5389
|
-
return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
|
|
4591
|
+
return { request, binding: binding ? inspectGraphBinding(binding, request) : null, observation };
|
|
5390
4592
|
}
|
|
5391
4593
|
bindGraph(repository, expectedRevision, request, evidence) {
|
|
5392
4594
|
const { identity } = this.#mutationContext(repository);
|
|
@@ -5415,10 +4617,25 @@ export class WorkflowService {
|
|
|
5415
4617
|
return explainChange(input, repository, effectiveTask, navigation);
|
|
5416
4618
|
}
|
|
5417
4619
|
next(repository, taskId = null) {
|
|
5418
|
-
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);
|
|
5419
4621
|
}
|
|
5420
4622
|
nextFromStatus(repository, taskId, status) {
|
|
5421
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
|
+
}
|
|
5422
4639
|
return {
|
|
5423
4640
|
scope: 'project',
|
|
5424
4641
|
projectId: status.projectId,
|
|
@@ -5426,7 +4643,16 @@ export class WorkflowService {
|
|
|
5426
4643
|
action: 'blocked',
|
|
5427
4644
|
observation: status.observation,
|
|
5428
4645
|
navigation: {
|
|
5429
|
-
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
|
+
} : {}),
|
|
5430
4656
|
reasonCodes: status.observation.reasonCodes,
|
|
5431
4657
|
availableRepairTransitionId: null,
|
|
5432
4658
|
},
|
|
@@ -5563,6 +4789,46 @@ export class WorkflowService {
|
|
|
5563
4789
|
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
5564
4790
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
5565
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
|
+
}
|
|
5566
4832
|
const requestedTask = taskId === null
|
|
5567
4833
|
? null
|
|
5568
4834
|
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
@@ -5604,23 +4870,27 @@ export class WorkflowService {
|
|
|
5604
4870
|
? [task]
|
|
5605
4871
|
: [];
|
|
5606
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
|
+
}
|
|
5607
4889
|
const activeTask = requestedTask
|
|
4890
|
+
?? prioritizedRefreshTask
|
|
5608
4891
|
?? prioritizedRemediationTask
|
|
5609
4892
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
5610
4893
|
if (activeTask) {
|
|
5611
|
-
let pendingReviewDependencyRecovery = null;
|
|
5612
|
-
try {
|
|
5613
|
-
const reconstructed = pendingReviewSourceBinding(repositoryRoot, this.store, activeTask, runGit(repositoryRoot, ['rev-parse', 'HEAD^']), runGit(repositoryRoot, ['rev-parse', `${activeTask.baseBranch}^`]));
|
|
5614
|
-
pendingReviewDependencyRecovery = this.pendingReviewDependencyPreflight(repositoryRoot, activeTask.id, reconstructed);
|
|
5615
|
-
}
|
|
5616
|
-
catch { /* A nonmatching shape follows ordinary navigation without a compatibility bypass. */ }
|
|
5617
|
-
if (pendingReviewDependencyRecovery?.eligible) {
|
|
5618
|
-
return withAdoption({ scope: 'task', id: activeTask.id, status: activeTask.status,
|
|
5619
|
-
action: 'update pending-review-dependency-recover', revision: activeTask.revision,
|
|
5620
|
-
pendingReviewDependencyRecovery,
|
|
5621
|
-
sourceReceiptRequired: true,
|
|
5622
|
-
});
|
|
5623
|
-
}
|
|
5624
4894
|
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5625
4895
|
if (historicalStepProvenanceRecovery.eligible) {
|
|
5626
4896
|
return withAdoption({
|
|
@@ -5648,22 +4918,6 @@ export class WorkflowService {
|
|
|
5648
4918
|
},
|
|
5649
4919
|
});
|
|
5650
4920
|
}
|
|
5651
|
-
const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5652
|
-
if (planIntegrityDependencyRecovery.eligible) {
|
|
5653
|
-
return withAdoption({
|
|
5654
|
-
scope: 'task',
|
|
5655
|
-
id: activeTask.id,
|
|
5656
|
-
status: activeTask.status,
|
|
5657
|
-
action: 'update plan-integrity-dependency-recover',
|
|
5658
|
-
revision: activeTask.revision,
|
|
5659
|
-
dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
|
|
5660
|
-
compatibilityBoundary: {
|
|
5661
|
-
preservesWorktree: true,
|
|
5662
|
-
prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
|
|
5663
|
-
continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
|
|
5664
|
-
},
|
|
5665
|
-
});
|
|
5666
|
-
}
|
|
5667
4921
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5668
4922
|
if (dependencyRecovery.eligible) {
|
|
5669
4923
|
return withAdoption({
|
|
@@ -5675,6 +4929,18 @@ export class WorkflowService {
|
|
|
5675
4929
|
dependencyProvenanceRecovery: dependencyRecovery,
|
|
5676
4930
|
});
|
|
5677
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
|
+
}
|
|
5678
4944
|
const activeDownstreamProofDependencyRecovery = this.activeDownstreamProofDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5679
4945
|
if (activeDownstreamProofDependencyRecovery.eligible) {
|
|
5680
4946
|
return withAdoption({
|
|
@@ -5691,22 +4957,6 @@ export class WorkflowService {
|
|
|
5691
4957
|
},
|
|
5692
4958
|
});
|
|
5693
4959
|
}
|
|
5694
|
-
const downstreamProofReplanDependencyRecovery = this.downstreamProofReplanDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
5695
|
-
if (downstreamProofReplanDependencyRecovery.eligible) {
|
|
5696
|
-
return withAdoption({
|
|
5697
|
-
scope: 'task',
|
|
5698
|
-
id: activeTask.id,
|
|
5699
|
-
status: activeTask.status,
|
|
5700
|
-
action: 'update downstream-proof-replan-dependency-recover',
|
|
5701
|
-
revision: activeTask.revision,
|
|
5702
|
-
dependencyProvenanceRecovery: downstreamProofReplanDependencyRecovery,
|
|
5703
|
-
compatibilityBoundary: {
|
|
5704
|
-
preservesWorktree: true,
|
|
5705
|
-
prerequisite: 'A newer package preflight approved the exact beta.12.6 stranded replan transport before dependency-only commits were created.',
|
|
5706
|
-
continuation: 'Register this dependency commit, then run task downstream-proof-replan-recover.',
|
|
5707
|
-
},
|
|
5708
|
-
});
|
|
5709
|
-
}
|
|
5710
4960
|
const activeTaskMilestone = activeTask.milestoneId
|
|
5711
4961
|
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
5712
4962
|
: null;
|
|
@@ -5730,61 +4980,13 @@ export class WorkflowService {
|
|
|
5730
4980
|
},
|
|
5731
4981
|
}
|
|
5732
4982
|
: value);
|
|
5733
|
-
const currentPlanRiskAuditExists = activeTask.planHash
|
|
5734
|
-
? readPlanRiskAuditEvents(this.store, activeTask).some((event) => event.taskRevision === activeTask.revision && event.planHash === activeTask.planHash)
|
|
5735
|
-
: false;
|
|
5736
|
-
const downstreamReplanRecovery = assessDownstreamProofReplanRecovery(this.store, repositoryRoot, activeTask, { currentPlanRiskAuditExists });
|
|
5737
|
-
if (downstreamReplanRecovery.applicable) {
|
|
5738
|
-
const environmentBlockers = [
|
|
5739
|
-
...(!downstreamReplanRecovery.dependencyRecoveryCommit
|
|
5740
|
-
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
5741
|
-
: []),
|
|
5742
|
-
...(status.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
5743
|
-
...(status.observation.pendingProjectTransactions.length > 0
|
|
5744
|
-
|| status.observation.pendingMilestoneTransactions.length > 0
|
|
5745
|
-
|| status.observation.pendingTaskTransactions.length > 0
|
|
5746
|
-
? ['No pending Workflow transaction may exist.'] : []),
|
|
5747
|
-
...(status.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
5748
|
-
...(status.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
5749
|
-
];
|
|
5750
|
-
return withAdoption(withMilestoneCancellation(downstreamReplanRecovery.eligible && environmentBlockers.length === 0
|
|
5751
|
-
? {
|
|
5752
|
-
scope: 'task',
|
|
5753
|
-
id: activeTask.id,
|
|
5754
|
-
status: activeTask.status,
|
|
5755
|
-
action: 'task downstream-proof-replan-recover',
|
|
5756
|
-
revision: activeTask.revision,
|
|
5757
|
-
downstreamProofReplanRecovery: {
|
|
5758
|
-
state: 'eligible',
|
|
5759
|
-
invalidationEventId: downstreamReplanRecovery.invalidation.eventId,
|
|
5760
|
-
sourcePlanHash: downstreamReplanRecovery.invalidation.planHash,
|
|
5761
|
-
reboundPlanHash: activeTask.planHash,
|
|
5762
|
-
dirtyFiles: downstreamReplanRecovery.compatibility.dirtyFiles,
|
|
5763
|
-
dirtyWorktreeHash: downstreamReplanRecovery.compatibility.dirtyWorktreeHash,
|
|
5764
|
-
dependencyRecoveryCommit: downstreamReplanRecovery.dependencyRecoveryCommit,
|
|
5765
|
-
boundedEffect: 'restore-needs-fix-and-return-same-task-to-plan-set',
|
|
5766
|
-
},
|
|
5767
|
-
}
|
|
5768
|
-
: {
|
|
5769
|
-
scope: 'task',
|
|
5770
|
-
id: activeTask.id,
|
|
5771
|
-
status: activeTask.status,
|
|
5772
|
-
action: 'doctor',
|
|
5773
|
-
blockedAction: 'task downstream-proof-replan-recover',
|
|
5774
|
-
revision: activeTask.revision,
|
|
5775
|
-
downstreamProofReplanRecovery: {
|
|
5776
|
-
state: 'blocked',
|
|
5777
|
-
blockers: [...downstreamReplanRecovery.blockers, ...environmentBlockers],
|
|
5778
|
-
},
|
|
5779
|
-
}));
|
|
5780
|
-
}
|
|
5781
4983
|
let activePlanRiskAudit = null;
|
|
5782
4984
|
let planRiskAuditRecovery = null;
|
|
5783
4985
|
const requiresReplacementPlan = activeTask.status === 'needs_fix'
|
|
5784
4986
|
&& !activeTask.steps.some((step) => step.status === 'failed');
|
|
5785
4987
|
if (activeTask.planHash && !requiresReplacementPlan) {
|
|
5786
4988
|
try {
|
|
5787
|
-
activePlanRiskAudit =
|
|
4989
|
+
activePlanRiskAudit = readCorrectivePlanningAudit(this.store, activeTask);
|
|
5788
4990
|
}
|
|
5789
4991
|
catch (error) {
|
|
5790
4992
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
@@ -5814,6 +5016,9 @@ export class WorkflowService {
|
|
|
5814
5016
|
: new Map();
|
|
5815
5017
|
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
5816
5018
|
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
5019
|
+
&& activeTask.planHash !== null
|
|
5020
|
+
&& activeTask.workspaceOwner === null
|
|
5021
|
+
&& activeTask.steps.every(step => step.status === 'planned')
|
|
5817
5022
|
&& remediationMilestone?.status === 'active'
|
|
5818
5023
|
&& remediationMembership?.disposition === 'required';
|
|
5819
5024
|
const remediationAncestorIds = new Set();
|
|
@@ -5990,9 +5195,8 @@ export class WorkflowService {
|
|
|
5990
5195
|
if (taskNext?.action === 'task step-complete'
|
|
5991
5196
|
|| taskNext?.action === 'task step-review'
|
|
5992
5197
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
5993
|
-
|| taskNext?.action === 'task remediation-mode-recover'
|
|
5994
5198
|
|| (taskNext?.action === 'task writer-credential-replace'
|
|
5995
|
-
&& ['task step-complete', 'task step-review'
|
|
5199
|
+
&& ['task step-complete', 'task step-review'].includes(String(taskNext?.blockedAction)))
|
|
5996
5200
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
5997
5201
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
5998
5202
|
|| taskNext?.action === 'task corrective-yield'
|
|
@@ -6068,6 +5272,7 @@ export class WorkflowService {
|
|
|
6068
5272
|
contextRefresh: {
|
|
6069
5273
|
action: 'task context-refresh',
|
|
6070
5274
|
mode: refreshAssessment.mode,
|
|
5275
|
+
authority: readFailedStepPlanningRecovery(this.store, activeTask) ? 'planning-only' : 'execution',
|
|
6071
5276
|
addedSources: refreshAssessment.addedSources,
|
|
6072
5277
|
options: contextRefreshOptions,
|
|
6073
5278
|
},
|
|
@@ -6172,6 +5377,24 @@ export class WorkflowService {
|
|
|
6172
5377
|
milestoneInitialAssembly: initialAssembly,
|
|
6173
5378
|
});
|
|
6174
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
|
+
}
|
|
6175
5398
|
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
6176
5399
|
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
6177
5400
|
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
@@ -6213,6 +5436,33 @@ export class WorkflowService {
|
|
|
6213
5436
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6214
5437
|
});
|
|
6215
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';
|
|
6216
5466
|
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
6217
5467
|
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
6218
5468
|
: [];
|
|
@@ -6220,7 +5470,24 @@ export class WorkflowService {
|
|
|
6220
5470
|
scope: 'milestone',
|
|
6221
5471
|
id: activeMilestone.id,
|
|
6222
5472
|
status: activeMilestone.status,
|
|
6223
|
-
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
|
+
} : {}),
|
|
6224
5491
|
revision: activeMilestone.revision,
|
|
6225
5492
|
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
6226
5493
|
? {
|
|
@@ -6424,7 +5691,11 @@ export class WorkflowService {
|
|
|
6424
5691
|
return { milestone, authorization, scopeChange };
|
|
6425
5692
|
}
|
|
6426
5693
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
6427
|
-
|
|
5694
|
+
if (task.pendingBaseSyncValidation !== undefined)
|
|
5695
|
+
return this.taskGitNavigationOverride(task);
|
|
5696
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
5697
|
+
// A stopped rebound Plan can expose planning recovery, never an approval route.
|
|
5698
|
+
const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
|
|
6428
5699
|
? 'task.execution_authorize'
|
|
6429
5700
|
: task.status === 'awaiting_final_acceptance'
|
|
6430
5701
|
? 'task.final_accept'
|
|
@@ -6438,49 +5709,44 @@ export class WorkflowService {
|
|
|
6438
5709
|
const correctiveDerivedAuthority = transition && correctiveDerivedActor
|
|
6439
5710
|
? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
|
|
6440
5711
|
: null;
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
if (planRiskAudit) {
|
|
6444
|
-
try {
|
|
6445
|
-
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
6446
|
-
}
|
|
6447
|
-
catch (error) {
|
|
6448
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
6449
|
-
if (!remediationModeRecovery)
|
|
6450
|
-
throw error;
|
|
6451
|
-
}
|
|
6452
|
-
}
|
|
6453
|
-
let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
|
|
6454
|
-
? guardedCorrectivePosture
|
|
5712
|
+
const guardedCorrectivePosture = planRiskAudit
|
|
5713
|
+
? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
|
|
6455
5714
|
: null;
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
catch (error) {
|
|
6461
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
6462
|
-
if (!remediationModeRecovery)
|
|
6463
|
-
throw error;
|
|
6464
|
-
}
|
|
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));
|
|
6465
5719
|
}
|
|
6466
5720
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
6467
5721
|
const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
6468
5722
|
const rootCauseStepId = nextStepId
|
|
6469
5723
|
?? task.steps.find((candidate) => candidate.status === 'planned')?.id
|
|
6470
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);
|
|
6471
5728
|
const downstreamProofCarryover = next.action === 'task run'
|
|
6472
5729
|
&& nextStepId
|
|
6473
|
-
&&
|
|
5730
|
+
&& nextStartPhase === 'planned-dirty'
|
|
6474
5731
|
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
6475
5732
|
: null;
|
|
6476
5733
|
const planIntegrityDirtyCarryover = next.action === 'task run'
|
|
6477
5734
|
&& nextStepId
|
|
6478
|
-
&&
|
|
5735
|
+
&& nextStartPhase === 'planned-dirty'
|
|
6479
5736
|
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
6480
5737
|
: null;
|
|
6481
|
-
|
|
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))
|
|
6482
5747
|
&& rootCauseStepId
|
|
6483
|
-
|
|
5748
|
+
// Match runStep: historical carryover admits a replacement start, never a failed retry.
|
|
5749
|
+
&& rootCauseStartPhase === 'planned-dirty'
|
|
6484
5750
|
? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
|
|
6485
5751
|
: null;
|
|
6486
5752
|
if (rootCauseReplanDirtyCarryover?.eligible) {
|
|
@@ -6546,14 +5812,14 @@ export class WorkflowService {
|
|
|
6546
5812
|
}
|
|
6547
5813
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
6548
5814
|
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
6549
|
-
const failedStepRemediations = failedStep
|
|
5815
|
+
const failedStepRemediations = failedStep
|
|
6550
5816
|
? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
|
|
6551
5817
|
: [];
|
|
6552
5818
|
const failedStepReviewRouting = failedStep
|
|
6553
5819
|
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
6554
5820
|
: null;
|
|
6555
|
-
const planIntegrityAssessment =
|
|
6556
|
-
?
|
|
5821
|
+
const planIntegrityAssessment = planRiskAudit && failedStep
|
|
5822
|
+
? this.assessPlanIntegrityRecoveryAdmission(repositoryRoot, task, failedStep.id)
|
|
6557
5823
|
: null;
|
|
6558
5824
|
const downstreamProofAssessment = assessDownstreamProofRecovery(this.store, repositoryRoot, task);
|
|
6559
5825
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -6711,28 +5977,10 @@ export class WorkflowService {
|
|
|
6711
5977
|
},
|
|
6712
5978
|
};
|
|
6713
5979
|
}
|
|
6714
|
-
if (
|
|
6715
|
-
guardedCorrectiveOverlay = {
|
|
6716
|
-
action: 'task remediation-mode-recover',
|
|
6717
|
-
stepId: remediationModeRecovery.remediation.stepId,
|
|
6718
|
-
remediationModeRecovery: {
|
|
6719
|
-
state: 'required',
|
|
6720
|
-
remediationEventId: remediationModeRecovery.remediation.eventId,
|
|
6721
|
-
remediationEventHash: remediationModeRecovery.remediation.eventHash,
|
|
6722
|
-
correctiveDecisionEventId: remediationModeRecovery.decision.eventId,
|
|
6723
|
-
correctiveDecisionRecoveryEventId: remediationModeRecovery.correctiveRecovery.eventId,
|
|
6724
|
-
attemptOrdinal: remediationModeRecovery.remediation.attemptOrdinal,
|
|
6725
|
-
fromMode: 'ordinary',
|
|
6726
|
-
toMode: 'corrective',
|
|
6727
|
-
requiredActor: remediationModeRecovery.actor,
|
|
6728
|
-
},
|
|
6729
|
-
};
|
|
6730
|
-
}
|
|
6731
|
-
if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
5980
|
+
if (guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
6732
5981
|
guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
|
|
6733
5982
|
}
|
|
6734
|
-
if (
|
|
6735
|
-
&& guardedCorrectivePosture
|
|
5983
|
+
if (guardedCorrectivePosture
|
|
6736
5984
|
&& (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
|
|
6737
5985
|
switch (guardedCorrectivePosture.posture) {
|
|
6738
5986
|
case 'decision-required':
|
|
@@ -6913,62 +6161,42 @@ export class WorkflowService {
|
|
|
6913
6161
|
retainedImplementation: 'journal-recoverable-1-to-1',
|
|
6914
6162
|
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
6915
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
|
+
: [],
|
|
6916
6176
|
incidentDependencies,
|
|
6917
6177
|
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
6918
6178
|
id: discovery.id,
|
|
6919
6179
|
revision: discovery.revision,
|
|
6920
6180
|
goal: discovery.goal,
|
|
6921
6181
|
})),
|
|
6922
|
-
requiredAction: '
|
|
6182
|
+
requiredAction: 'The owner must decide whether to stop this Task or revise the Milestone scope through supported transitions; structural replacement is unavailable.',
|
|
6923
6183
|
},
|
|
6924
6184
|
};
|
|
6925
6185
|
}
|
|
6926
6186
|
break;
|
|
6927
6187
|
case 'stop-escalate':
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
delegatedApprovalAllowed: false,
|
|
6941
|
-
},
|
|
6942
|
-
stopOverride: {
|
|
6943
|
-
state: 'eligible',
|
|
6944
|
-
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
6945
|
-
prepareCommand: 'task stop-override-prepare',
|
|
6946
|
-
applyCommand: 'task stop-override-apply',
|
|
6947
|
-
expectedRevision: task.revision,
|
|
6948
|
-
appendOnly: true,
|
|
6949
|
-
preservesOriginalDecision: true,
|
|
6950
|
-
requiredAction: 'Prepare with an explicit human actor and reason, then apply the exact confirmation code in a later user turn.',
|
|
6951
|
-
},
|
|
6952
|
-
};
|
|
6953
|
-
}
|
|
6954
|
-
catch (error) {
|
|
6955
|
-
guardedCorrectiveOverlay = {
|
|
6956
|
-
action: 'doctor',
|
|
6957
|
-
blockedAction: 'task stop-override-prepare',
|
|
6958
|
-
stepId: guardedCorrectivePosture.stepId,
|
|
6959
|
-
correctiveDecisionGate: {
|
|
6960
|
-
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
6961
|
-
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
6962
|
-
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
6963
|
-
humanApprovalRequired: true,
|
|
6964
|
-
},
|
|
6965
|
-
stopOverride: {
|
|
6966
|
-
state: 'blocked',
|
|
6967
|
-
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
6968
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
6969
|
-
},
|
|
6970
|
-
};
|
|
6971
|
-
}
|
|
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
|
+
};
|
|
6972
6200
|
break;
|
|
6973
6201
|
default:
|
|
6974
6202
|
break;
|
|
@@ -7119,6 +6347,7 @@ export class WorkflowService {
|
|
|
7119
6347
|
}
|
|
7120
6348
|
: {}),
|
|
7121
6349
|
...mechanicalFeasibilityOverlay,
|
|
6350
|
+
...navigationDirtyOverlay,
|
|
7122
6351
|
...(downstreamProofCarryover?.applicable
|
|
7123
6352
|
? downstreamProofCarryover.eligible
|
|
7124
6353
|
? {
|
|
@@ -7187,10 +6416,11 @@ export class WorkflowService {
|
|
|
7187
6416
|
rootCauseReplanDirtyCarryover: {
|
|
7188
6417
|
state: 'awaiting-c1',
|
|
7189
6418
|
stepId: rootCauseStepId,
|
|
7190
|
-
requiredActor: defaultTaskWorkerActor(task.id),
|
|
6419
|
+
requiredActor: rootCauseReplanDirtyCarryover.requiredActor ?? defaultTaskWorkerActor(task.id),
|
|
7191
6420
|
},
|
|
7192
6421
|
}
|
|
7193
6422
|
: handoffPosture.state === 'pending'
|
|
6423
|
+
&& (!rootCauseReplanDirtyCarryover.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)
|
|
7194
6424
|
? {}
|
|
7195
6425
|
: {
|
|
7196
6426
|
action: 'doctor',
|
|
@@ -7231,15 +6461,15 @@ export class WorkflowService {
|
|
|
7231
6461
|
// fix review. Advertise the existing append-only decision route without turning
|
|
7232
6462
|
// every ordinary repair into a mandatory replan or rewriting that review.
|
|
7233
6463
|
const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
|
|
7234
|
-
const optionalCorrectiveDecision = result.action === 'task run'
|
|
7235
|
-
&& ['needs_fix', 'ready'].includes(task.status)
|
|
6464
|
+
const optionalCorrectiveDecision = (result.action === 'task run' || planningRecovery !== null)
|
|
6465
|
+
&& (['needs_fix', 'ready'].includes(task.status) || planningRecovery !== null)
|
|
7236
6466
|
&& failedStep && task.planHash && planRiskAudit
|
|
7237
6467
|
&& planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
|
|
7238
6468
|
&& !task.steps.some(step => step.status === 'in_progress')
|
|
7239
6469
|
&& failedStepRemediations.length >= 2
|
|
7240
6470
|
&& handoffPosture.state !== 'pending'
|
|
7241
6471
|
&& !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
|
|
7242
|
-
&&
|
|
6472
|
+
&& correctiveDecisionCheckoutEligible(this.store, repositoryRoot, task, failedStep.id)
|
|
7243
6473
|
? {
|
|
7244
6474
|
command: 'task corrective-decision',
|
|
7245
6475
|
state: 'eligible',
|
|
@@ -7255,7 +6485,8 @@ export class WorkflowService {
|
|
|
7255
6485
|
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
7256
6486
|
auditorMustDifferFromLatestStrictReviewer: true,
|
|
7257
6487
|
requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
|
|
7258
|
-
decisions: ['
|
|
6488
|
+
decisions: planningRecovery ? ['replan-required', 'split-required', 'stop-escalate']
|
|
6489
|
+
: ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
|
|
7259
6490
|
authority: 'none',
|
|
7260
6491
|
preservesOriginalReview: true,
|
|
7261
6492
|
humanApprovalRequiredForDecision: false,
|
|
@@ -7264,11 +6495,17 @@ export class WorkflowService {
|
|
|
7264
6495
|
}
|
|
7265
6496
|
: null;
|
|
7266
6497
|
const resultWithCorrectiveOption = optionalCorrectiveDecision
|
|
7267
|
-
? { ...result, correctiveDecisionOption: optionalCorrectiveDecision
|
|
6498
|
+
? { ...result, correctiveDecisionOption: optionalCorrectiveDecision,
|
|
6499
|
+
...(planningRecovery ? { action: 'task corrective-decision', blockedAction: 'task authorize',
|
|
6500
|
+
planningRecovery: { authority: 'planning-only', obstructionAuditHash: planningRecovery.current.audit.eventHash,
|
|
6501
|
+
baselineAuditHash: planningRecovery.baseline.eventHash } } : {}) }
|
|
6502
|
+
: planningRecovery && result.action === 'task authorize'
|
|
6503
|
+
? { ...result, action: 'doctor', blockedAction: 'task authorize' } : result;
|
|
7268
6504
|
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
7269
6505
|
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
7270
6506
|
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
7271
|
-
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim
|
|
6507
|
+
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim
|
|
6508
|
+
&& (!rootCauseReplanDirtyCarryover?.requiredActor || handoffPosture.targetActor === rootCauseReplanDirtyCarryover.requiredActor)) {
|
|
7272
6509
|
return this.withTaskNavigationContracts(task, {
|
|
7273
6510
|
...resultWithCorrectiveOption,
|
|
7274
6511
|
action: 'task claim',
|
|
@@ -7301,28 +6538,43 @@ export class WorkflowService {
|
|
|
7301
6538
|
},
|
|
7302
6539
|
};
|
|
7303
6540
|
}
|
|
7304
|
-
|
|
6541
|
+
taskContextChangeBlocker(task, operation) {
|
|
6542
|
+
const blocked = (error) => ({ error, activeStep: false });
|
|
7305
6543
|
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
7306
|
-
|
|
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' }));
|
|
7307
6545
|
}
|
|
7308
6546
|
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
7309
|
-
|
|
6547
|
+
return blocked(new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
7310
6548
|
taskId: task.id,
|
|
7311
6549
|
requiredAction: 'Run task plan-set with the full unfinished remainder before requesting authorization.',
|
|
7312
|
-
});
|
|
6550
|
+
}));
|
|
7313
6551
|
}
|
|
7314
6552
|
const posture = this.taskTerminalCorrectivePosture(task);
|
|
7315
|
-
if (
|
|
7316
|
-
return;
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
}
|
|
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;
|
|
7326
6578
|
}
|
|
7327
6579
|
stopOverrideContextRebindDecisionId(task, stepId, repositoryRoot) {
|
|
7328
6580
|
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
@@ -7346,7 +6598,15 @@ export class WorkflowService {
|
|
|
7346
6598
|
taskTerminalCorrectivePosture(task) {
|
|
7347
6599
|
if (!task.planHash)
|
|
7348
6600
|
return null;
|
|
7349
|
-
|
|
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;
|
|
6609
|
+
const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
|
|
7350
6610
|
if (!planRiskAudit)
|
|
7351
6611
|
return null;
|
|
7352
6612
|
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
@@ -7354,42 +6614,6 @@ export class WorkflowService {
|
|
|
7354
6614
|
? posture
|
|
7355
6615
|
: null;
|
|
7356
6616
|
}
|
|
7357
|
-
assertStopOverrideRepositoryBoundary(identity, task, observedLeases) {
|
|
7358
|
-
const blockers = [];
|
|
7359
|
-
const dirty = changedFiles(identity.repositoryRoot);
|
|
7360
|
-
if (dirty.length > 0)
|
|
7361
|
-
blockers.push('Repository checkout must be clean.');
|
|
7362
|
-
if (task.workspaceOwner !== 'local')
|
|
7363
|
-
blockers.push('Stop override is limited to a local Task workspace.');
|
|
7364
|
-
if (!task.taskBranch || currentBranch(identity.repositoryRoot) !== task.taskBranch) {
|
|
7365
|
-
blockers.push('The current branch must be the recorded Task branch.');
|
|
7366
|
-
}
|
|
7367
|
-
if (task.steps.some((step) => step.status === 'in_progress'))
|
|
7368
|
-
blockers.push('No Step may be in progress.');
|
|
7369
|
-
const leases = observedLeases
|
|
7370
|
-
?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
|
|
7371
|
-
if (leases.some((lease) => lease.entityId === task.id))
|
|
7372
|
-
blockers.push('No active or stale Task writer lease may exist.');
|
|
7373
|
-
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
7374
|
-
const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
|
|
7375
|
-
const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
|
|
7376
|
-
if (replanTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
7377
|
-
|| replanTransactions.corrupt.some((transaction) => transaction.taskId === task.id)
|
|
7378
|
-
|| yieldTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
7379
|
-
|| yieldTransactions.corrupt.some((transaction) => transaction.taskId === task.id)) {
|
|
7380
|
-
blockers.push('No pending or corrupt Task transaction may exist.');
|
|
7381
|
-
}
|
|
7382
|
-
if (listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root)
|
|
7383
|
-
.some((operation) => operation.entityId === task.id)) {
|
|
7384
|
-
blockers.push('No Core Task operation may exist.');
|
|
7385
|
-
}
|
|
7386
|
-
if (blockers.length > 0) {
|
|
7387
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override repository preconditions are not satisfied.', {
|
|
7388
|
-
taskId: task.id,
|
|
7389
|
-
blockers,
|
|
7390
|
-
});
|
|
7391
|
-
}
|
|
7392
|
-
}
|
|
7393
6617
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
7394
6618
|
return this.listDelegations(projectId)
|
|
7395
6619
|
.filter((grant) => {
|
|
@@ -7413,6 +6637,19 @@ export class WorkflowService {
|
|
|
7413
6637
|
}));
|
|
7414
6638
|
}
|
|
7415
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 [];
|
|
7416
6653
|
return this.listDelegations(projectId)
|
|
7417
6654
|
.filter((grant) => {
|
|
7418
6655
|
try {
|
|
@@ -7432,8 +6669,8 @@ export class WorkflowService {
|
|
|
7432
6669
|
grantId: grant.id,
|
|
7433
6670
|
principal: grant.principal,
|
|
7434
6671
|
delegate: grant.delegate,
|
|
7435
|
-
transition: 'task.execution_authorize',
|
|
7436
|
-
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'],
|
|
7437
6674
|
scope: grant.scope,
|
|
7438
6675
|
expiresAt: grant.expiresAt,
|
|
7439
6676
|
policyHash: grant.policyHash,
|
|
@@ -7443,16 +6680,13 @@ export class WorkflowService {
|
|
|
7443
6680
|
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
7444
6681
|
for (const task of tasks) {
|
|
7445
6682
|
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
7446
|
-
|| !task.planHash
|
|
7447
|
-
|| !taskCanRebindKnowledge(task)
|
|
7448
|
-
|| (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed'))
|
|
7449
|
-
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
6683
|
+
|| !task.planHash)
|
|
7450
6684
|
continue;
|
|
7451
6685
|
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
7452
6686
|
if (!assessment.eligible)
|
|
7453
6687
|
continue;
|
|
7454
6688
|
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
7455
|
-
if (options.length > 0)
|
|
6689
|
+
if (options.length > 0 && this.taskContextChangeBlocker(task, 'refresh') === null)
|
|
7456
6690
|
return { task, assessment, options };
|
|
7457
6691
|
}
|
|
7458
6692
|
return null;
|
|
@@ -7560,12 +6794,31 @@ export class WorkflowService {
|
|
|
7560
6794
|
: inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
7561
6795
|
}
|
|
7562
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
|
+
}
|
|
7563
6806
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
|
|
7564
6807
|
return null;
|
|
7565
6808
|
}
|
|
7566
6809
|
const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
7567
6810
|
if (currentBase === task.baseCommit)
|
|
7568
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
|
+
};
|
|
7569
6822
|
return {
|
|
7570
6823
|
scope: 'task',
|
|
7571
6824
|
id: task.id,
|
|
@@ -7579,43 +6832,6 @@ export class WorkflowService {
|
|
|
7579
6832
|
},
|
|
7580
6833
|
};
|
|
7581
6834
|
}
|
|
7582
|
-
carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
|
|
7583
|
-
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
7584
|
-
let dependencyCommitCount = 0;
|
|
7585
|
-
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
7586
|
-
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
7587
|
-
if (!dependencyCommit)
|
|
7588
|
-
return null;
|
|
7589
|
-
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
7590
|
-
dependencyCommitCount += 1;
|
|
7591
|
-
}
|
|
7592
|
-
if (dependencyCommitCount === 0)
|
|
7593
|
-
return null;
|
|
7594
|
-
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
7595
|
-
const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
|
|
7596
|
-
const compatibility = record.planIntegrity;
|
|
7597
|
-
if (!compatibility
|
|
7598
|
-
|| record.commitSha !== boundaryCommit
|
|
7599
|
-
|| !task.systemCommits.includes(record.commitSha)
|
|
7600
|
-
|| compatibility.planHash !== task.planHash
|
|
7601
|
-
|| canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
|
|
7602
|
-
|| compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
|
|
7603
|
-
return false;
|
|
7604
|
-
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
|
|
7605
|
-
if (!step || step.status !== 'failed')
|
|
7606
|
-
return false;
|
|
7607
|
-
const remediationEventIds = readRemediationEvents(this.store, task)
|
|
7608
|
-
.filter((event) => event.stepId === compatibility.stepId)
|
|
7609
|
-
.map((event) => event.eventId);
|
|
7610
|
-
if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
|
|
7611
|
-
return false;
|
|
7612
|
-
return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
|
|
7613
|
-
&& event.stepId === compatibility.stepId
|
|
7614
|
-
&& event.decision === 'replan-required')
|
|
7615
|
-
&& readTaskC1Posture(this.store, task).state === 'none';
|
|
7616
|
-
});
|
|
7617
|
-
return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
|
|
7618
|
-
}
|
|
7619
6835
|
postRebindCheckSupportCompatibility(snapshot, task) {
|
|
7620
6836
|
if (task.status !== 'awaiting_execution_authorization'
|
|
7621
6837
|
|| !task.planHash
|
|
@@ -7790,9 +7006,12 @@ export class WorkflowService {
|
|
|
7790
7006
|
blockers,
|
|
7791
7007
|
};
|
|
7792
7008
|
}
|
|
7793
|
-
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null,
|
|
7009
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, historicalStepProvenance = null) {
|
|
7794
7010
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
7795
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;
|
|
7796
7015
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
7797
7016
|
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
7798
7017
|
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
@@ -7810,7 +7029,7 @@ export class WorkflowService {
|
|
|
7810
7029
|
currentBranch: versions.currentBranch,
|
|
7811
7030
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
7812
7031
|
},
|
|
7813
|
-
}, activeDownstreamProof,
|
|
7032
|
+
}, activeDownstreamProof, historicalStepProvenance, navigationCarryover?.eligible ? navigationCarryover.compatibility : null);
|
|
7814
7033
|
}
|
|
7815
7034
|
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
|
|
7816
7035
|
const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
|
|
@@ -7876,10 +7095,19 @@ export class WorkflowService {
|
|
|
7876
7095
|
}
|
|
7877
7096
|
const candidates = task.steps
|
|
7878
7097
|
.filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
|
|
7879
|
-
.flatMap((step) =>
|
|
7880
|
-
|
|
7881
|
-
.
|
|
7882
|
-
|
|
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
|
+
})
|
|
7883
7111
|
.filter((assessment) => assessment.applicable);
|
|
7884
7112
|
const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
|
|
7885
7113
|
if (eligible.length === 1)
|
|
@@ -7888,6 +7116,7 @@ export class WorkflowService {
|
|
|
7888
7116
|
applicable: candidates.length > 0,
|
|
7889
7117
|
eligible: false,
|
|
7890
7118
|
compatibility: null,
|
|
7119
|
+
...(candidates.length === 1 && candidates[0].requiredActor ? { requiredActor: candidates[0].requiredActor } : {}),
|
|
7891
7120
|
blockers: [...new Set([
|
|
7892
7121
|
...candidates.flatMap((assessment) => assessment.blockers),
|
|
7893
7122
|
...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
|
|
@@ -8863,7 +8092,6 @@ function writerTokenContractForAction(action, claimedC1Posture = false, existing
|
|
|
8863
8092
|
'task review-launch',
|
|
8864
8093
|
'task review-sealed-record',
|
|
8865
8094
|
'task corrective-decision-recover',
|
|
8866
|
-
'task remediation-mode-recover',
|
|
8867
8095
|
'task plan-integrity-recover',
|
|
8868
8096
|
'task corrective-decision',
|
|
8869
8097
|
'task corrective-yield',
|