codex-workflow-v2 2.0.0-beta.12.8 → 2.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -16
- package/dist/reviewer-runtime-build.json +1092 -0
- package/dist/src/alpha6/check-support-anchor.d.ts +24 -0
- package/dist/src/alpha6/check-support-anchor.js +102 -0
- package/dist/src/alpha6/check-support-anchor.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +28 -0
- package/dist/src/alpha6/component-owner.js +95 -0
- package/dist/src/alpha6/component-owner.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.js +4 -2
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.js +42 -12
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -1
- package/dist/src/alpha6/milestone.d.ts +6 -2
- package/dist/src/alpha6/milestone.js +164 -10
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/npm-check-contract.d.ts +11 -0
- package/dist/src/alpha6/npm-check-contract.js +36 -0
- package/dist/src/alpha6/npm-check-contract.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +157 -27
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +3 -1
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation-cause.d.ts +13 -0
- package/dist/src/alpha6/remediation-cause.js +77 -0
- package/dist/src/alpha6/remediation-cause.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +15 -4
- package/dist/src/alpha6/remediation.js +430 -67
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +6 -0
- package/dist/src/alpha6/review.js +46 -4
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/artifacts.js +6 -0
- package/dist/src/artifacts.js.map +1 -1
- package/dist/src/cli-actions.d.ts +25 -0
- package/dist/src/cli-actions.js +109 -0
- package/dist/src/cli-actions.js.map +1 -0
- package/dist/src/cli.js +244 -253
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +202 -5
- package/dist/src/credential-transport.d.ts +38 -0
- package/dist/src/credential-transport.js +156 -0
- package/dist/src/credential-transport.js.map +1 -0
- package/dist/src/dependency-provenance.d.ts +23 -2
- package/dist/src/dependency-provenance.js +239 -36
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/completed-step-carryover.d.ts +13 -0
- package/dist/src/domain/completed-step-carryover.js +71 -0
- package/dist/src/domain/completed-step-carryover.js.map +1 -0
- package/dist/src/domain/validation.d.ts +9 -1
- package/dist/src/domain/validation.js +86 -0
- package/dist/src/domain/validation.js.map +1 -1
- package/dist/src/gateway-handshake.d.ts +11 -0
- package/dist/src/gateway-handshake.js +92 -0
- package/dist/src/gateway-handshake.js.map +1 -0
- package/dist/src/git.js +1 -0
- package/dist/src/git.js.map +1 -1
- package/dist/src/historical-step-provenance.d.ts +20 -0
- package/dist/src/historical-step-provenance.js +111 -0
- package/dist/src/historical-step-provenance.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +1 -1
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/lifecycle/semantic-registry.js +1 -1
- package/dist/src/observation.js +1 -2
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.d.ts +114 -0
- package/dist/src/observed-routes.js +545 -0
- package/dist/src/observed-routes.js.map +1 -0
- package/dist/src/operational-contract.d.ts +26 -0
- package/dist/src/operational-contract.js +94 -0
- package/dist/src/operational-contract.js.map +1 -0
- package/dist/src/reviewer.d.ts +218 -8
- package/dist/src/reviewer.js +927 -25
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.js +23 -15
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +35 -10
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.js +12 -4
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.js +11 -2
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +24 -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.d.ts +70 -4
- package/dist/src/workflow.js +1888 -152
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +41 -14
- package/docs/delegated-approval.md +6 -1
- package/docs/development-flow.md +42 -12
- 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/sources/codex-workflow-v2-architecture-ru.md +191 -22
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +163 -45
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +346 -52
- package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
- package/docs/problem-briefs/README.md +11 -11
- package/docs/release.md +89 -9
- package/docs/updating-existing-project.md +235 -0
- package/docs/validation-report.md +156 -98
- package/package.json +12 -3
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +9 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +67 -1
- package/schemas/authorization-event.schema.json +10 -3
- package/schemas/corrective-decision-event.schema.json +164 -9
- package/schemas/corrective-replan-credentials.private.schema.json +4 -2
- package/schemas/milestone-scope-change-event.schema.json +25 -1
- package/schemas/remediation-event.schema.json +17 -1
- package/schemas/review-result.schema.json +1 -0
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/task.schema.json +104 -1
- package/schemas/transition-definition.schema.json +8 -0
package/dist/src/workflow.js
CHANGED
|
@@ -2,24 +2,28 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
|
-
import { inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
5
|
+
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
6
|
+
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
6
7
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
7
8
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
8
9
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
10
|
+
import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
|
|
9
11
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
10
12
|
import { WorkflowError } from './errors.js';
|
|
13
|
+
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
11
14
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
12
15
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
13
16
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
14
|
-
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, } from './alpha6/downstream-proof.js';
|
|
17
|
+
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
15
18
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
16
19
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
17
|
-
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision,
|
|
20
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
18
21
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
22
|
+
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
19
23
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
20
|
-
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
24
|
+
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
21
25
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
22
|
-
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
26
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
|
|
23
27
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
24
28
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
25
29
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
@@ -27,7 +31,7 @@ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
|
27
31
|
import { ensureDirectory } from './fs-utils.js';
|
|
28
32
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
29
33
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
30
|
-
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
34
|
+
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
31
35
|
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
32
36
|
import { FileStateStore } from './state/store.js';
|
|
33
37
|
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
@@ -600,6 +604,353 @@ export class WorkflowService {
|
|
|
600
604
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
601
605
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
602
606
|
}
|
|
607
|
+
materializeAuditedRemediationTask(repository, milestoneId, expectedMilestoneRevision, blockedTaskId, expectedBlockedTaskRevision, input) {
|
|
608
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
609
|
+
const observation = this.observationSnapshot(repository).observation;
|
|
610
|
+
if (observation.kind !== 'assessment'
|
|
611
|
+
|| observation.leases.length > 0
|
|
612
|
+
|| observation.coreTaskOperations.length > 0
|
|
613
|
+
|| observation.pendingProjectTransactions.length > 0
|
|
614
|
+
|| observation.pendingMilestoneTransactions.length > 0
|
|
615
|
+
|| observation.pendingTaskTransactions.length > 0
|
|
616
|
+
|| observation.corruptTaskTransactions.length > 0) {
|
|
617
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires a coherent, idle observation boundary.', { observation });
|
|
618
|
+
}
|
|
619
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
620
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Audited remediation materialization requires a clean Milestone base checkout.', { changedFiles: changedFiles(identity.repositoryRoot) });
|
|
621
|
+
}
|
|
622
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
623
|
+
assertExpectedRevision(milestone, expectedMilestoneRevision);
|
|
624
|
+
if (milestone.status !== 'active' || currentBranch(identity.repositoryRoot) !== milestone.baseBranch) {
|
|
625
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires the active Milestone base branch.', {
|
|
626
|
+
milestoneStatus: milestone.status,
|
|
627
|
+
expectedBranch: milestone.baseBranch,
|
|
628
|
+
actualBranch: currentBranch(identity.repositoryRoot),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
632
|
+
const blockedTask = this.store.readTask(identity.projectId, blockedTaskId);
|
|
633
|
+
assertExpectedRevision(blockedTask, expectedBlockedTaskRevision);
|
|
634
|
+
if (blockedTask.milestoneId !== milestone.id
|
|
635
|
+
|| blockedTask.status !== 'awaiting_execution_authorization'
|
|
636
|
+
|| !blockedTask.planHash
|
|
637
|
+
|| blockedTask.workspaceOwner !== null
|
|
638
|
+
|| blockedTask.steps.some((step) => step.status !== 'planned')) {
|
|
639
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation is available only for an unstarted Milestone Task with a rejected current Plan.', {
|
|
640
|
+
taskId: blockedTask.id,
|
|
641
|
+
taskStatus: blockedTask.status,
|
|
642
|
+
milestoneId: blockedTask.milestoneId,
|
|
643
|
+
workspaceOwner: blockedTask.workspaceOwner,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const blockedMembership = milestone.memberships.find((membership) => membership.taskId === blockedTask.id);
|
|
647
|
+
if (blockedMembership?.disposition !== 'required') {
|
|
648
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The blocked Task must remain a required Milestone member.', { taskId: blockedTask.id, disposition: blockedMembership?.disposition ?? null });
|
|
649
|
+
}
|
|
650
|
+
const audit = readPlanRiskAuditEvents(this.store, blockedTask).at(-1);
|
|
651
|
+
if (!audit
|
|
652
|
+
|| audit.decision !== 'stop-escalate'
|
|
653
|
+
|| audit.taskRevision !== blockedTask.revision
|
|
654
|
+
|| audit.briefHash !== blockedTask.briefHash
|
|
655
|
+
|| audit.planHash !== blockedTask.planHash) {
|
|
656
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation requires the current stop-escalate Plan Risk Audit.', {
|
|
657
|
+
taskId: blockedTask.id,
|
|
658
|
+
currentTaskRevision: blockedTask.revision,
|
|
659
|
+
currentBriefHash: blockedTask.briefHash,
|
|
660
|
+
currentPlanHash: blockedTask.planHash,
|
|
661
|
+
latestAudit: audit ?? null,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const duplicate = readMilestoneScopeChangeEvents(this.store, identity.projectId, milestone.id)
|
|
665
|
+
.find((event) => event.authorizationMode === 'audited-remediation'
|
|
666
|
+
&& event.remediationEvidence?.planRiskAuditEventHash === audit.eventHash);
|
|
667
|
+
if (duplicate) {
|
|
668
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'This blocking Plan Risk Audit already has a materialized remediation Task.', {
|
|
669
|
+
planRiskAuditEventId: audit.eventId,
|
|
670
|
+
remediationTaskId: duplicate.remediationEvidence?.remediationTaskId ?? null,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const discovery = this.store.readDiscovery(identity.projectId, input.discoveryId);
|
|
674
|
+
assertExpectedRevision(discovery, input.expectedDiscoveryRevision);
|
|
675
|
+
const readiness = assessDiscovery(discovery);
|
|
676
|
+
if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
|
|
677
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation Discovery is not ready to materialize.', {
|
|
678
|
+
discoveryId: discovery.id,
|
|
679
|
+
missing: readiness.missing,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const title = input.title.trim();
|
|
683
|
+
if (!title)
|
|
684
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Remediation Task title is required.');
|
|
685
|
+
const predecessorTaskIds = [...new Set(input.predecessorTaskIds.map((taskId) => taskId.trim()))].sort();
|
|
686
|
+
if (predecessorTaskIds.length === 0 || predecessorTaskIds.some((taskId) => !taskId)) {
|
|
687
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Audited remediation requires at least one explicit merged predecessor Task.');
|
|
688
|
+
}
|
|
689
|
+
const requiredPredecessorTaskIds = [...(blockedMembership.dependsOnTaskIds ?? [])].sort();
|
|
690
|
+
if (canonicalJsonStringify(predecessorTaskIds) !== canonicalJsonStringify(requiredPredecessorTaskIds)) {
|
|
691
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation must inherit the blocked Task exact direct predecessor frontier.', {
|
|
692
|
+
blockedTaskId: blockedTask.id,
|
|
693
|
+
requiredPredecessorTaskIds,
|
|
694
|
+
actualPredecessorTaskIds: predecessorTaskIds,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const membershipsByTaskId = new Map(milestone.memberships.map((membership) => [membership.taskId, membership]));
|
|
698
|
+
const ancestorTaskIds = new Set();
|
|
699
|
+
const visitAncestors = (taskId) => {
|
|
700
|
+
const membership = membershipsByTaskId.get(taskId);
|
|
701
|
+
for (const dependencyId of membership?.dependsOnTaskIds ?? []) {
|
|
702
|
+
if (ancestorTaskIds.has(dependencyId))
|
|
703
|
+
continue;
|
|
704
|
+
ancestorTaskIds.add(dependencyId);
|
|
705
|
+
visitAncestors(dependencyId);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
visitAncestors(blockedTask.id);
|
|
709
|
+
for (const predecessorTaskId of predecessorTaskIds) {
|
|
710
|
+
const predecessorMembership = membershipsByTaskId.get(predecessorTaskId);
|
|
711
|
+
const predecessor = this.store.readTask(identity.projectId, predecessorTaskId);
|
|
712
|
+
if (!ancestorTaskIds.has(predecessorTaskId)
|
|
713
|
+
|| predecessorMembership?.disposition !== 'required'
|
|
714
|
+
|| predecessor.status !== 'merged') {
|
|
715
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Every remediation predecessor must be a merged required ancestor of the blocked Task.', {
|
|
716
|
+
predecessorTaskId,
|
|
717
|
+
isAncestor: ancestorTaskIds.has(predecessorTaskId),
|
|
718
|
+
disposition: predecessorMembership?.disposition ?? null,
|
|
719
|
+
status: predecessor.status,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const now = this.now();
|
|
724
|
+
const timestamp = now.toISOString();
|
|
725
|
+
const actor = `agent:milestone-remediation:${milestone.id}`;
|
|
726
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
727
|
+
const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
|
|
728
|
+
const inspectedKnowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scannedKnowledgeMap);
|
|
729
|
+
let dependencyKnowledgeRefresh = null;
|
|
730
|
+
let refreshedKnowledgeMap = null;
|
|
731
|
+
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
732
|
+
dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(identity.repositoryRoot, milestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
|
|
733
|
+
if (!dependencyKnowledgeRefresh.eligible) {
|
|
734
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation cannot bypass unrelated or unproven Project Knowledge drift.', { dependencyKnowledgeRefresh });
|
|
735
|
+
}
|
|
736
|
+
const reconciled = reconcileKnowledgeMap(storedKnowledgeMap, scannedKnowledgeMap);
|
|
737
|
+
const approved = applyKnowledgeSelections(reconciled, [], actor, now);
|
|
738
|
+
if (!approved.approval) {
|
|
739
|
+
throw new WorkflowError('STATE_CORRUPT', 'Dependency-only Knowledge refresh did not produce approval evidence.');
|
|
740
|
+
}
|
|
741
|
+
refreshedKnowledgeMap = {
|
|
742
|
+
...approved,
|
|
743
|
+
revision: storedKnowledgeMap.revision + 2,
|
|
744
|
+
createdAt: storedKnowledgeMap.createdAt,
|
|
745
|
+
updatedAt: timestamp,
|
|
746
|
+
approval: {
|
|
747
|
+
...approved.approval,
|
|
748
|
+
authorizationMode: 'corrective-derived',
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const remediationTaskId = createEntityId('TASK', now.getTime());
|
|
753
|
+
const briefContent = renderBrief(discovery, title);
|
|
754
|
+
const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
|
|
755
|
+
const briefHash = sha256Hex(normalizedBriefContent);
|
|
756
|
+
const remediationTask = {
|
|
757
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
758
|
+
revision: 1,
|
|
759
|
+
id: remediationTaskId,
|
|
760
|
+
projectId: identity.projectId,
|
|
761
|
+
createdAt: timestamp,
|
|
762
|
+
updatedAt: timestamp,
|
|
763
|
+
kind: 'task',
|
|
764
|
+
status: 'planning',
|
|
765
|
+
title,
|
|
766
|
+
slug: slugify(title),
|
|
767
|
+
discoveryId: discovery.id,
|
|
768
|
+
milestoneId: milestone.id,
|
|
769
|
+
repositoryRoot: identity.repositoryRoot,
|
|
770
|
+
baseBranch: milestone.baseBranch,
|
|
771
|
+
baseCommit: null,
|
|
772
|
+
taskBranch: null,
|
|
773
|
+
workspaceOwner: null,
|
|
774
|
+
briefHash,
|
|
775
|
+
planHash: null,
|
|
776
|
+
planObjective: null,
|
|
777
|
+
resultHash: null,
|
|
778
|
+
evidenceHash: null,
|
|
779
|
+
requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
|
|
780
|
+
acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
|
|
781
|
+
risks: discovery.assumptions,
|
|
782
|
+
knowledgeImpact: null,
|
|
783
|
+
knowledgeImpactReason: null,
|
|
784
|
+
knowledgeTargets: [],
|
|
785
|
+
knowledgeMapRevision: null,
|
|
786
|
+
knowledgeMapHash: null,
|
|
787
|
+
graphUse: null,
|
|
788
|
+
steps: [],
|
|
789
|
+
authorizations: [],
|
|
790
|
+
knowledgeRebinds: [],
|
|
791
|
+
review: emptyReview(),
|
|
792
|
+
blockReason: null,
|
|
793
|
+
mergedCommit: null,
|
|
794
|
+
systemCommits: [],
|
|
795
|
+
};
|
|
796
|
+
const consumedDiscovery = {
|
|
797
|
+
...discovery,
|
|
798
|
+
revision: discovery.revision + 1,
|
|
799
|
+
updatedAt: timestamp,
|
|
800
|
+
status: 'materialized',
|
|
801
|
+
materializedEntityId: remediationTaskId,
|
|
802
|
+
};
|
|
803
|
+
const remediationEvidence = {
|
|
804
|
+
blockedTaskId: blockedTask.id,
|
|
805
|
+
blockedTaskRevision: blockedTask.revision,
|
|
806
|
+
blockedBriefHash: blockedTask.briefHash,
|
|
807
|
+
blockedPlanHash: blockedTask.planHash,
|
|
808
|
+
planRiskAuditEventId: audit.eventId,
|
|
809
|
+
planRiskAuditEventHash: audit.eventHash,
|
|
810
|
+
remediationTaskId,
|
|
811
|
+
predecessorTaskIds,
|
|
812
|
+
};
|
|
813
|
+
const reason = `Audited remediation for ${blockedTask.id} after stop-escalate ${audit.eventId}.`;
|
|
814
|
+
const plan = {
|
|
815
|
+
outcome: milestone.outcome,
|
|
816
|
+
successSignal: milestone.successSignal,
|
|
817
|
+
acceptance: [...milestone.acceptance],
|
|
818
|
+
checks: [...milestone.checks],
|
|
819
|
+
memberships: [
|
|
820
|
+
...milestone.memberships.map((membership) => membership.taskId === blockedTask.id
|
|
821
|
+
? {
|
|
822
|
+
...membership,
|
|
823
|
+
dependsOnTaskIds: [...new Set([...(membership.dependsOnTaskIds ?? []), remediationTaskId])],
|
|
824
|
+
}
|
|
825
|
+
: { ...membership, dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])] }),
|
|
826
|
+
{
|
|
827
|
+
taskId: remediationTaskId,
|
|
828
|
+
disposition: 'required',
|
|
829
|
+
reason,
|
|
830
|
+
dependsOnTaskIds: predecessorTaskIds,
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
};
|
|
834
|
+
const readCandidateTask = (taskId) => taskId === remediationTaskId
|
|
835
|
+
? remediationTask
|
|
836
|
+
: this.store.readTask(identity.projectId, taskId);
|
|
837
|
+
const preparedCandidate = prepareMilestoneScopeChangeCandidate({
|
|
838
|
+
milestone,
|
|
839
|
+
plan,
|
|
840
|
+
readTask: readCandidateTask,
|
|
841
|
+
actor,
|
|
842
|
+
auditedRemediationEvidence: remediationEvidence,
|
|
843
|
+
});
|
|
844
|
+
const sourceAuthorization = [...milestone.authorizations]
|
|
845
|
+
.reverse()
|
|
846
|
+
.find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
|
|
847
|
+
if (!sourceAuthorization) {
|
|
848
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation has no current Milestone execution authority.');
|
|
849
|
+
}
|
|
850
|
+
const derivedAuthorization = {
|
|
851
|
+
kind: 'execution',
|
|
852
|
+
decision: 'approved',
|
|
853
|
+
actor,
|
|
854
|
+
recordedAt: timestamp,
|
|
855
|
+
briefHash: null,
|
|
856
|
+
planHash: preparedCandidate.candidatePlanHash,
|
|
857
|
+
resultHash: null,
|
|
858
|
+
evidenceHash: sha256Hex(canonicalJsonStringify({
|
|
859
|
+
sourcePlanHash: sourceAuthorization.planHash,
|
|
860
|
+
sourceAuthorizationMode: sourceAuthorization.authorizationMode ?? 'human',
|
|
861
|
+
planRiskAuditEventHash: audit.eventHash,
|
|
862
|
+
remediationEvidence,
|
|
863
|
+
})),
|
|
864
|
+
headCommit: headCommit(identity.repositoryRoot),
|
|
865
|
+
reason: 'Derived from the current Milestone execution authority and exact blocking Plan Risk Audit.',
|
|
866
|
+
authorizationMode: 'corrective-derived',
|
|
867
|
+
};
|
|
868
|
+
const prepared = {
|
|
869
|
+
...preparedCandidate,
|
|
870
|
+
authorizations: [...preparedCandidate.authorizations, derivedAuthorization],
|
|
871
|
+
};
|
|
872
|
+
const bundle = prepareMilestoneScopeChangeBundle({
|
|
873
|
+
store: this.store,
|
|
874
|
+
milestone,
|
|
875
|
+
prepared,
|
|
876
|
+
now,
|
|
877
|
+
readTask: readCandidateTask,
|
|
878
|
+
});
|
|
879
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
880
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
881
|
+
applyProjectTransaction({
|
|
882
|
+
projectRoot,
|
|
883
|
+
projectId: identity.projectId,
|
|
884
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
885
|
+
kind: 'audited-remediation-materialization',
|
|
886
|
+
now,
|
|
887
|
+
targets: [
|
|
888
|
+
...(refreshedKnowledgeMap
|
|
889
|
+
? [{
|
|
890
|
+
path: relative(path.join(projectRoot, 'knowledge-map.json')),
|
|
891
|
+
content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n`,
|
|
892
|
+
}]
|
|
893
|
+
: []),
|
|
894
|
+
{
|
|
895
|
+
path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')),
|
|
896
|
+
content: `${JSON.stringify(consumedDiscovery, null, 2)}\n`,
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'brief.md')),
|
|
900
|
+
content: normalizedBriefContent,
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'state.json')),
|
|
904
|
+
content: `${JSON.stringify(remediationTask, null, 2)}\n`,
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')),
|
|
908
|
+
content: bundle.planContent,
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')),
|
|
912
|
+
content: bundle.sidecarContent,
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')),
|
|
916
|
+
content: bundle.stateContent,
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
validate: () => {
|
|
920
|
+
const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
|
|
921
|
+
const savedRemediationTask = this.store.readTask(identity.projectId, remediationTaskId);
|
|
922
|
+
const savedMilestone = this.readMilestoneCurrent(identity.projectId, milestone.id);
|
|
923
|
+
const savedBlockedMembership = savedMilestone.memberships
|
|
924
|
+
.find((membership) => membership.taskId === blockedTask.id);
|
|
925
|
+
const savedRemediationMembership = savedMilestone.memberships
|
|
926
|
+
.find((membership) => membership.taskId === remediationTaskId);
|
|
927
|
+
const savedAuthorization = [...savedMilestone.authorizations]
|
|
928
|
+
.reverse()
|
|
929
|
+
.find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
|
|
930
|
+
const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
931
|
+
if (savedDiscovery.materializedEntityId !== remediationTaskId
|
|
932
|
+
|| savedRemediationTask.milestoneId !== milestone.id
|
|
933
|
+
|| !savedBlockedMembership?.dependsOnTaskIds?.includes(remediationTaskId)
|
|
934
|
+
|| savedRemediationMembership?.disposition !== 'required'
|
|
935
|
+
|| savedAuthorization?.authorizationMode !== 'corrective-derived'
|
|
936
|
+
|| savedMilestone.status !== 'active'
|
|
937
|
+
|| (refreshedKnowledgeMap !== null
|
|
938
|
+
&& (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
|
|
939
|
+
|| savedKnowledgeMap.status !== 'active'
|
|
940
|
+
|| savedKnowledgeMap.approval?.authorizationMode !== 'corrective-derived'))
|
|
941
|
+
|| locks.inspect(blockedTask.id) !== null) {
|
|
942
|
+
throw new WorkflowError('STATE_CORRUPT', 'Audited remediation transaction readback failed.');
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
return {
|
|
947
|
+
discovery: this.store.readDiscovery(identity.projectId, discovery.id),
|
|
948
|
+
remediationTask: this.store.readTask(identity.projectId, remediationTaskId),
|
|
949
|
+
blockedTask: this.store.readTask(identity.projectId, blockedTask.id),
|
|
950
|
+
milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
|
|
951
|
+
remediationEvidence,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
603
954
|
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
604
955
|
const { identity, locks } = this.#mutationContext(repository);
|
|
605
956
|
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
@@ -1265,11 +1616,14 @@ export class WorkflowService {
|
|
|
1265
1616
|
|| (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
|
|
1266
1617
|
const downstreamReplanRecovered = readDownstreamProofReplanRecoveries(this.store, task)
|
|
1267
1618
|
.some((event) => event.resultTaskRevision === task.revision && event.reboundPlanHash === task.planHash);
|
|
1268
|
-
const
|
|
1619
|
+
const currentPlanRiskAuditExists = task.planHash !== null && readPlanRiskAuditEvents(this.store, task).some((event) => event.briefHash === task.briefHash && event.planHash === task.planHash);
|
|
1620
|
+
const currentPlanRiskAudit = (task.status === 'needs_fix'
|
|
1621
|
+
|| (task.steps.some((step) => step.status === 'failed') && currentPlanRiskAuditExists)
|
|
1622
|
+
|| currentReplanDecisions.length > 0)
|
|
1269
1623
|
&& !downstreamReplanRecovered
|
|
1270
1624
|
? readCurrentPlanRiskAudit(this.store, task)
|
|
1271
1625
|
: null;
|
|
1272
|
-
const classifiedCorrectivePosture =
|
|
1626
|
+
const classifiedCorrectivePosture = currentPlanRiskAudit
|
|
1273
1627
|
? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
|
|
1274
1628
|
: null;
|
|
1275
1629
|
const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
|
|
@@ -1299,7 +1653,10 @@ export class WorkflowService {
|
|
|
1299
1653
|
: 'Use task corrective-replan-prepare, confirm, and execute.',
|
|
1300
1654
|
});
|
|
1301
1655
|
}
|
|
1302
|
-
else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)
|
|
1656
|
+
else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)
|
|
1657
|
+
&& !(task.status === 'ready'
|
|
1658
|
+
&& correctiveReplanPosture?.posture === 'replan-required'
|
|
1659
|
+
&& correctiveReplanPosture.correctiveDecision === null)) {
|
|
1303
1660
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
1304
1661
|
}
|
|
1305
1662
|
if (currentHardBlockingDecisions.length > 0) {
|
|
@@ -1369,7 +1726,8 @@ export class WorkflowService {
|
|
|
1369
1726
|
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
|
|
1370
1727
|
}
|
|
1371
1728
|
}
|
|
1372
|
-
if (task.planHash && task.status === 'needs_fix'
|
|
1729
|
+
if (task.planHash && (task.status === 'needs_fix'
|
|
1730
|
+
|| correctiveReplanPosture?.posture === 'replan-required')) {
|
|
1373
1731
|
if (task.planObjective !== null && plan.objective !== task.planObjective) {
|
|
1374
1732
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
|
|
1375
1733
|
taskId: task.id,
|
|
@@ -1391,29 +1749,82 @@ export class WorkflowService {
|
|
|
1391
1749
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
1392
1750
|
? { ...authorization, decision: 'superseded' }
|
|
1393
1751
|
: authorization);
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1752
|
+
const retainedCompletedSteps = deriveCompletedStepCarryover(task, plan.steps);
|
|
1753
|
+
const retainedCompletedIds = [...retainedCompletedSteps.keys()];
|
|
1754
|
+
const plannedSteps = plan.steps.map((candidate) => {
|
|
1755
|
+
const step = normalizePlanStep(candidate);
|
|
1756
|
+
return retainedCompletedSteps.get(step.id) ?? ({
|
|
1757
|
+
...step,
|
|
1758
|
+
status: dependenciesComplete(step, retainedCompletedIds) ? 'planned' : 'blocked',
|
|
1759
|
+
evidence: null,
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
const invalidatedStepCommits = [...new Set([
|
|
1763
|
+
...(task.invalidatedStepCommits ?? []),
|
|
1764
|
+
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1765
|
+
])];
|
|
1402
1766
|
if (task.status === 'needs_fix' &&
|
|
1403
1767
|
task.review.status === 'failed' &&
|
|
1404
1768
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
1405
1769
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
1406
1770
|
}
|
|
1407
1771
|
if (correctiveReplanPosture?.posture === 'replan-required') {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
});
|
|
1772
|
+
if (correctiveReplanPosture.correctiveDecision === null) {
|
|
1773
|
+
const currentStep = task.steps.find((step) => step.id === correctiveReplanPosture.stepId) ?? null;
|
|
1774
|
+
const candidateStep = plan.steps.find((step) => step.id === correctiveReplanPosture.stepId) ?? null;
|
|
1775
|
+
const currentClassification = currentPlanRiskAudit?.audit.stepClassifications
|
|
1776
|
+
.find((classification) => classification.stepId === correctiveReplanPosture.stepId) ?? null;
|
|
1777
|
+
const candidateClassification = planRiskAudit?.stepClassifications
|
|
1778
|
+
.find((classification) => classification.stepId === correctiveReplanPosture.stepId) ?? null;
|
|
1779
|
+
const priorProductionEdges = task.productionEdges ?? [];
|
|
1780
|
+
const candidateProductionEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => `${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`));
|
|
1781
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
1782
|
+
const rawWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
1783
|
+
const blockingGuardIds = [
|
|
1784
|
+
...(task.steps.some((step) => step.status === 'in_progress')
|
|
1785
|
+
? ['task.rolling-replan.no-step-in-progress']
|
|
1786
|
+
: []),
|
|
1787
|
+
...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
|
|
1788
|
+
...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
|
|
1789
|
+
...(preparedPlan.planHash === task.planHash
|
|
1790
|
+
? ['task.rolling-replan.semantic-plan-change']
|
|
1791
|
+
: []),
|
|
1792
|
+
...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
|
|
1793
|
+
? ['task.rolling-replan.guarded-step-retained']
|
|
1794
|
+
: []),
|
|
1795
|
+
...(!planRiskAudit || !candidateClassification || !currentClassification
|
|
1796
|
+
|| !candidateClassification.reviewRequired
|
|
1797
|
+
|| currentClassification.categories.some((category) => !candidateClassification.categories.includes(category))
|
|
1798
|
+
? ['task.rolling-replan.guarded-review-classification-preserved']
|
|
1799
|
+
: []),
|
|
1800
|
+
...(currentStep && candidateStep
|
|
1801
|
+
&& currentStep.forbiddenScope.some((scope) => !candidateStep.forbiddenScope.includes(scope))
|
|
1802
|
+
? ['task.rolling-replan.forbidden-scope-preserved']
|
|
1803
|
+
: []),
|
|
1804
|
+
...((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
|
|
1805
|
+
|| priorProductionEdges.some((edge) => !candidateProductionEdgeKeys.has(`${edge.ownerStepId}\0${edge.consumerStepId}\0${edge.checkRef}`))
|
|
1806
|
+
? ['task.rolling-replan.production-edge-boundary-preserved']
|
|
1807
|
+
: []),
|
|
1808
|
+
];
|
|
1809
|
+
if (blockingGuardIds.length > 0) {
|
|
1810
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Rolling-wave root-cause replan rejected the candidate Plan for Task ${task.id}.`, {
|
|
1811
|
+
transitionId: 'task.plan.rolling-root-cause-replan',
|
|
1812
|
+
evaluationPhase: 'execution',
|
|
1813
|
+
blockingGuardIds,
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
else {
|
|
1818
|
+
const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
|
|
1819
|
+
if (!executionEvaluation.allowed) {
|
|
1820
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
|
|
1821
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1822
|
+
evaluationPhase: executionEvaluation.phase,
|
|
1823
|
+
blockingGuardIds: executionEvaluation.blockingGuardIds,
|
|
1824
|
+
pendingGuardIds: executionEvaluation.pendingGuardIds,
|
|
1825
|
+
guardResults: executionEvaluation.guardResults,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1417
1828
|
}
|
|
1418
1829
|
}
|
|
1419
1830
|
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
@@ -1436,7 +1847,10 @@ export class WorkflowService {
|
|
|
1436
1847
|
knowledgeMapRevision: plan.knowledgeMapRevision,
|
|
1437
1848
|
knowledgeMapHash: plan.knowledgeMapHash,
|
|
1438
1849
|
graphUse: plan.graphUse,
|
|
1850
|
+
productionEdges: structuredClone(plan.productionEdges ?? []),
|
|
1851
|
+
requiresProductionEdges: plan.requiresProductionEdges === true,
|
|
1439
1852
|
steps: plannedSteps,
|
|
1853
|
+
invalidatedStepCommits,
|
|
1440
1854
|
authorizations,
|
|
1441
1855
|
review: emptyReview(),
|
|
1442
1856
|
resultHash: null,
|
|
@@ -1672,7 +2086,12 @@ export class WorkflowService {
|
|
|
1672
2086
|
requiredAction: 'task plan-set',
|
|
1673
2087
|
});
|
|
1674
2088
|
}
|
|
1675
|
-
const
|
|
2089
|
+
const correctiveAuthority = delegationGrantId === null
|
|
2090
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
2091
|
+
: null;
|
|
2092
|
+
const delegation = correctiveAuthority
|
|
2093
|
+
? null
|
|
2094
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1676
2095
|
const event = {
|
|
1677
2096
|
kind: 'execution',
|
|
1678
2097
|
decision: 'approved',
|
|
@@ -1684,7 +2103,11 @@ export class WorkflowService {
|
|
|
1684
2103
|
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1685
2104
|
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1686
2105
|
reason,
|
|
1687
|
-
authorizationMode:
|
|
2106
|
+
authorizationMode: correctiveAuthority
|
|
2107
|
+
? 'corrective-derived'
|
|
2108
|
+
: delegation
|
|
2109
|
+
? 'delegated'
|
|
2110
|
+
: 'human',
|
|
1688
2111
|
...(delegation ? { delegation } : {}),
|
|
1689
2112
|
mechanicalFeasibility,
|
|
1690
2113
|
};
|
|
@@ -1869,7 +2292,7 @@ export class WorkflowService {
|
|
|
1869
2292
|
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1870
2293
|
const action = String(navigation.action);
|
|
1871
2294
|
const currentLease = context.locks.inspect(taskId);
|
|
1872
|
-
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
2295
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
1873
2296
|
const requiredActor = writerTokenContract
|
|
1874
2297
|
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1875
2298
|
: null;
|
|
@@ -1883,9 +2306,7 @@ export class WorkflowService {
|
|
|
1883
2306
|
nextAction: action,
|
|
1884
2307
|
});
|
|
1885
2308
|
}
|
|
1886
|
-
const lease =
|
|
1887
|
-
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1888
|
-
: context.locks.acquire(taskId, normalizedActor);
|
|
2309
|
+
const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
|
|
1889
2310
|
if (posture.state === 'claimed') {
|
|
1890
2311
|
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1891
2312
|
}
|
|
@@ -2004,6 +2425,7 @@ export class WorkflowService {
|
|
|
2004
2425
|
}
|
|
2005
2426
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2006
2427
|
const step = requireStep(task, stepId);
|
|
2428
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2007
2429
|
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
2008
2430
|
if (!['planned', 'failed'].includes(step.status)) {
|
|
2009
2431
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
@@ -2038,18 +2460,25 @@ export class WorkflowService {
|
|
|
2038
2460
|
const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
|
|
2039
2461
|
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2040
2462
|
: null;
|
|
2463
|
+
const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2464
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2465
|
+
: null;
|
|
2041
2466
|
if (step.status === 'planned'
|
|
2042
2467
|
&& dirty.length > 0
|
|
2043
2468
|
&& !resumesCorrectiveReplan
|
|
2044
|
-
&& !downstreamProofCarryover?.eligible
|
|
2469
|
+
&& !downstreamProofCarryover?.eligible
|
|
2470
|
+
&& !planIntegrityDirtyCarryover?.eligible) {
|
|
2045
2471
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2046
2472
|
changedFiles: dirty,
|
|
2047
2473
|
...(downstreamProofCarryover?.applicable
|
|
2048
2474
|
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2049
2475
|
: {}),
|
|
2476
|
+
...(planIntegrityDirtyCarryover?.applicable
|
|
2477
|
+
? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
|
|
2478
|
+
: {}),
|
|
2050
2479
|
});
|
|
2051
2480
|
}
|
|
2052
|
-
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file,
|
|
2481
|
+
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
2053
2482
|
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
2054
2483
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
2055
2484
|
changedFiles: dirtyOutsideStep,
|
|
@@ -2096,7 +2525,9 @@ export class WorkflowService {
|
|
|
2096
2525
|
return context.locks.acquire(taskId, actor.trim() || 'worker');
|
|
2097
2526
|
})();
|
|
2098
2527
|
}
|
|
2099
|
-
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2528
|
+
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2529
|
+
? { ...candidate, status: 'in_progress', evidence: null, failurePause: null }
|
|
2530
|
+
: candidate);
|
|
2100
2531
|
let saved;
|
|
2101
2532
|
try {
|
|
2102
2533
|
saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
@@ -2120,7 +2551,7 @@ export class WorkflowService {
|
|
|
2120
2551
|
path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
|
|
2121
2552
|
],
|
|
2122
2553
|
allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
|
|
2123
|
-
allowedWrites
|
|
2554
|
+
allowedWrites,
|
|
2124
2555
|
commands: step.checks,
|
|
2125
2556
|
forbiddenScope: [
|
|
2126
2557
|
...step.forbiddenScope,
|
|
@@ -2239,6 +2670,7 @@ export class WorkflowService {
|
|
|
2239
2670
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
2240
2671
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2241
2672
|
const step = requireStep(task, stepId);
|
|
2673
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2242
2674
|
if (task.status !== 'in_progress' || step.status !== 'in_progress') {
|
|
2243
2675
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
|
|
2244
2676
|
}
|
|
@@ -2271,18 +2703,114 @@ export class WorkflowService {
|
|
|
2271
2703
|
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
2272
2704
|
const failed = checks.filter((check) => !check.passed);
|
|
2273
2705
|
if (failed.length > 0) {
|
|
2274
|
-
|
|
2275
|
-
|
|
2706
|
+
const now = this.now();
|
|
2707
|
+
const normalizedFailures = normalizeMechanicalFailures(failed, context.identity.repositoryRoot);
|
|
2708
|
+
// Bind useful uncommitted work to this failure without treating it as
|
|
2709
|
+
// completed-Step authority.
|
|
2710
|
+
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
2711
|
+
const failurePause = {
|
|
2712
|
+
recordedAt: now.toISOString(),
|
|
2713
|
+
dirtyFiles,
|
|
2714
|
+
dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
|
|
2715
|
+
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2716
|
+
failedChecks: normalizedFailures,
|
|
2717
|
+
};
|
|
2718
|
+
const preparedRemediation = reviewRequired && planRiskAudit
|
|
2719
|
+
? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
2276
2720
|
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
2277
2721
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2278
2722
|
reviewEventHash: null,
|
|
2279
2723
|
reviewerAttestationHash: null,
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2724
|
+
executionTaskRevision: task.revision,
|
|
2725
|
+
dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
|
|
2726
|
+
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
|
|
2727
|
+
: null;
|
|
2728
|
+
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2729
|
+
? { ...candidate, status: 'failed', evidence: null, failurePause }
|
|
2730
|
+
: candidate);
|
|
2731
|
+
const paused = {
|
|
2732
|
+
...task,
|
|
2733
|
+
revision: task.revision + 1,
|
|
2734
|
+
updatedAt: now.toISOString(),
|
|
2735
|
+
status: 'needs_fix',
|
|
2736
|
+
steps,
|
|
2737
|
+
blockReason: `${stepId} checks failed.`,
|
|
2738
|
+
};
|
|
2739
|
+
const stepHistory = readRemediationEvents(this.store, task)
|
|
2740
|
+
.filter((event) => event.stepId === stepId);
|
|
2741
|
+
const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
|
|
2742
|
+
? stepHistory
|
|
2743
|
+
: [...stepHistory, preparedRemediation.event];
|
|
2744
|
+
const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
|
|
2745
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2746
|
+
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
|
|
2747
|
+
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
2748
|
+
actor: c1Posture.claimant,
|
|
2749
|
+
writerToken: lease.token,
|
|
2750
|
+
reason: `Yield execution authority after repeated mechanical cause ${preparedRemediation.event.eventId}.`,
|
|
2751
|
+
eventId: `HOF-${sha256Hex([
|
|
2752
|
+
'checks-failed-root-cause-replan-pause-v1',
|
|
2753
|
+
task.id,
|
|
2754
|
+
String(task.revision),
|
|
2755
|
+
preparedRemediation.event.eventHash,
|
|
2756
|
+
c1Posture.claimant,
|
|
2757
|
+
].join('\0'))}`,
|
|
2758
|
+
}, now)
|
|
2759
|
+
: null;
|
|
2760
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2761
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2762
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2763
|
+
const failureHead = headCommit(context.identity.repositoryRoot);
|
|
2764
|
+
applyProjectTransaction({
|
|
2765
|
+
projectRoot,
|
|
2766
|
+
projectId: task.projectId,
|
|
2767
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2768
|
+
kind: rollingCauseDecision === 'root-cause-replan'
|
|
2769
|
+
? 'failed-step-root-cause-replan-pause'
|
|
2770
|
+
: 'failed-step-continuation',
|
|
2771
|
+
now,
|
|
2772
|
+
targets: [
|
|
2773
|
+
...(preparedRemediation?.postimage
|
|
2774
|
+
? [{
|
|
2775
|
+
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
2776
|
+
content: preparedRemediation.postimage,
|
|
2777
|
+
}]
|
|
2778
|
+
: []),
|
|
2779
|
+
...(preparedYield
|
|
2780
|
+
? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
|
|
2781
|
+
: []),
|
|
2782
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
|
|
2783
|
+
{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
|
|
2784
|
+
],
|
|
2785
|
+
validate: () => {
|
|
2786
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2787
|
+
const savedStep = requireStep(saved, stepId);
|
|
2788
|
+
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
2789
|
+
const remediationHash = preparedRemediation
|
|
2790
|
+
? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
|
|
2791
|
+
: null;
|
|
2792
|
+
if (saved.revision !== paused.revision
|
|
2793
|
+
|| saved.status !== 'needs_fix'
|
|
2794
|
+
|| savedStep.status !== 'failed'
|
|
2795
|
+
|| savedStep.failurePause?.dirtyWorktreeHash !== failurePause.dirtyWorktreeHash
|
|
2796
|
+
|| savedStep.failurePause?.checksHash !== failurePause.checksHash
|
|
2797
|
+
|| remediationHash !== (preparedRemediation?.event.eventHash ?? null)
|
|
2798
|
+
|| context.locks.inspect(task.id) !== null
|
|
2799
|
+
|| (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
|
|
2800
|
+
|| headCommit(context.identity.repositoryRoot) !== failureHead
|
|
2801
|
+
|| JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
|
|
2802
|
+
|| hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== failurePause.dirtyWorktreeHash) {
|
|
2803
|
+
throw new WorkflowError('STATE_CORRUPT', 'Failed-Step continuation transaction readback failed.', {
|
|
2804
|
+
taskId,
|
|
2805
|
+
stepId,
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
2809
|
+
},
|
|
2810
|
+
});
|
|
2811
|
+
return this.store.readTask(task.projectId, task.id);
|
|
2284
2812
|
}
|
|
2285
|
-
const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title,
|
|
2813
|
+
const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, allowedWrites);
|
|
2286
2814
|
const evidence = {
|
|
2287
2815
|
recordedAt: this.now().toISOString(),
|
|
2288
2816
|
commitSha: committed.commitSha,
|
|
@@ -2321,7 +2849,7 @@ export class WorkflowService {
|
|
|
2321
2849
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2322
2850
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2323
2851
|
assertExpectedRevision(task, expectedRevision);
|
|
2324
|
-
this.
|
|
2852
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2325
2853
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
2326
2854
|
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2327
2855
|
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
@@ -2392,18 +2920,14 @@ export class WorkflowService {
|
|
|
2392
2920
|
resolution: currentCycle.resolution,
|
|
2393
2921
|
});
|
|
2394
2922
|
}
|
|
2395
|
-
|
|
2396
|
-
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
2397
|
-
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2398
|
-
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
2399
|
-
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
2400
|
-
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2923
|
+
return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, currentCycle.reviewRecordedEvent, currentCycle.reviewerAttestationEvent.eventHash, planRiskAudit, undefined, reviewLease);
|
|
2401
2924
|
}
|
|
2402
2925
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
2403
2926
|
}
|
|
2404
2927
|
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2405
2928
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2406
|
-
const
|
|
2929
|
+
const reviewTask = taskWithEffectiveStepAllowedWrites(this.store, task, stepId);
|
|
2930
|
+
const launch = launchStrictStepReviewer(context.identity.repositoryRoot, reviewTask, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner, reviewScopeAugmentations(this.store, task, stepId));
|
|
2407
2931
|
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2408
2932
|
review: launch.review,
|
|
2409
2933
|
reviewerOutputHash: launch.reviewerOutputHash,
|
|
@@ -2420,16 +2944,11 @@ export class WorkflowService {
|
|
|
2420
2944
|
resolution: recorded.resolution,
|
|
2421
2945
|
});
|
|
2422
2946
|
}
|
|
2423
|
-
|
|
2424
|
-
completionCommit: reviewRecordedEvent.completionCommit,
|
|
2425
|
-
checksHash: reviewRecordedEvent.checksHash,
|
|
2426
|
-
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
2427
|
-
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
2428
|
-
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2947
|
+
return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewRecordedEvent, reviewerAttestationEvent.eventHash, planRiskAudit, launch.review.summary, reviewLease);
|
|
2429
2948
|
}
|
|
2430
2949
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2431
2950
|
}
|
|
2432
|
-
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2951
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
|
|
2433
2952
|
const identity = resolveRepositoryIdentity(repository);
|
|
2434
2953
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2435
2954
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2449,15 +2968,18 @@ export class WorkflowService {
|
|
|
2449
2968
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2450
2969
|
}
|
|
2451
2970
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2452
|
-
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
|
|
2971
|
+
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, taskWithEffectiveStepAllowedWrites(this.store, task, stepId), this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, reviewScopeAugmentations(this.store, task, stepId), reviewMode);
|
|
2453
2972
|
}
|
|
2454
2973
|
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2455
2974
|
const context = this.#mutationContext(repository);
|
|
2456
2975
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2457
2976
|
assertExpectedRevision(task, expectedRevision);
|
|
2458
|
-
this.
|
|
2977
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2459
2978
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2460
|
-
|
|
2979
|
+
if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
|
|
2980
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'External Step Review requires reviewMode ordinary or security.');
|
|
2981
|
+
}
|
|
2982
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision, input.reviewMode);
|
|
2461
2983
|
let review;
|
|
2462
2984
|
try {
|
|
2463
2985
|
review = validateExternalStrictStepReviewInput(input, packet);
|
|
@@ -2492,23 +3014,131 @@ export class WorkflowService {
|
|
|
2492
3014
|
isolationResult: 'verified',
|
|
2493
3015
|
}, this.now());
|
|
2494
3016
|
if (recorded.resolution === 'failed') {
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
3017
|
+
const saved = this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, recorded.reviewRecordedEvent, recorded.reviewerAttestationEvent.eventHash, readCurrentPlanRiskAudit(this.store, task), review.summary, reviewLease);
|
|
3018
|
+
return {
|
|
3019
|
+
mode: 'external-sealed',
|
|
3020
|
+
reviewMode: packet.reviewMode,
|
|
3021
|
+
packetHash: packet.packetHash,
|
|
3022
|
+
repositorySealHash: packet.repositorySealHash,
|
|
3023
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
3024
|
+
task: saved,
|
|
3025
|
+
};
|
|
2501
3026
|
}
|
|
2502
3027
|
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2503
3028
|
return {
|
|
2504
3029
|
mode: 'external-sealed',
|
|
3030
|
+
reviewMode: packet.reviewMode,
|
|
2505
3031
|
packetHash: packet.packetHash,
|
|
2506
3032
|
repositorySealHash: packet.repositorySealHash,
|
|
2507
3033
|
reviewerThreadId: input.reviewerThreadId,
|
|
2508
3034
|
task: saved,
|
|
2509
3035
|
};
|
|
2510
3036
|
}
|
|
2511
|
-
|
|
3037
|
+
finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewEvent, reviewerAttestationHash, planRiskAudit, summary, reviewLease) {
|
|
3038
|
+
const now = this.now();
|
|
3039
|
+
const preparedRemediation = prepareRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
3040
|
+
completionCommit: reviewEvent.completionCommit,
|
|
3041
|
+
checksHash: reviewEvent.checksHash,
|
|
3042
|
+
reviewEventHash: reviewEvent.eventHash,
|
|
3043
|
+
reviewerAttestationHash,
|
|
3044
|
+
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), reviewCauseBindings(reviewEvent));
|
|
3045
|
+
const stepHistory = readRemediationEvents(this.store, task)
|
|
3046
|
+
.filter((event) => event.stepId === stepId);
|
|
3047
|
+
const resultingHistory = preparedRemediation.postimage === null
|
|
3048
|
+
? stepHistory
|
|
3049
|
+
: [...stepHistory, preparedRemediation.event];
|
|
3050
|
+
const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
|
|
3051
|
+
const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
|
|
3052
|
+
const paused = {
|
|
3053
|
+
...resolved,
|
|
3054
|
+
revision: task.revision + 1,
|
|
3055
|
+
updatedAt: now.toISOString(),
|
|
3056
|
+
};
|
|
3057
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
3058
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
3059
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
3060
|
+
const failureHead = headCommit(context.identity.repositoryRoot);
|
|
3061
|
+
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3062
|
+
const dirtyWorktreeHash = hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles);
|
|
3063
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
3064
|
+
if (rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && !reviewLease) {
|
|
3065
|
+
throw new WorkflowError('STATE_CONFLICT', 'Root-cause replan pause requires the claimed writer authority it will release.', {
|
|
3066
|
+
taskId: task.id,
|
|
3067
|
+
stepId,
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && reviewLease
|
|
3071
|
+
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
3072
|
+
actor: c1Posture.claimant,
|
|
3073
|
+
writerToken: reviewLease.token,
|
|
3074
|
+
reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
|
|
3075
|
+
eventId: `HOF-${sha256Hex([
|
|
3076
|
+
'strict-review-root-cause-replan-pause-v1',
|
|
3077
|
+
task.id,
|
|
3078
|
+
String(task.revision),
|
|
3079
|
+
preparedRemediation.event.eventHash,
|
|
3080
|
+
c1Posture.claimant,
|
|
3081
|
+
].join('\0'))}`,
|
|
3082
|
+
}, now)
|
|
3083
|
+
: null;
|
|
3084
|
+
if (reviewLease) {
|
|
3085
|
+
const currentLease = context.locks.inspect(task.id);
|
|
3086
|
+
if (!currentLease || currentLease.token !== reviewLease.token || currentLease.owner !== reviewLease.owner) {
|
|
3087
|
+
throw new WorkflowError('STATE_CONFLICT', 'Strict Step Review writer authority changed before the failure pause could be recorded.', {
|
|
3088
|
+
taskId: task.id,
|
|
3089
|
+
stepId,
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
applyProjectTransaction({
|
|
3094
|
+
projectRoot,
|
|
3095
|
+
projectId: task.projectId,
|
|
3096
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
3097
|
+
kind: rollingCauseDecision === 'root-cause-replan'
|
|
3098
|
+
? 'strict-review-root-cause-replan-pause'
|
|
3099
|
+
: 'strict-review-failed-step-pause',
|
|
3100
|
+
now,
|
|
3101
|
+
targets: [
|
|
3102
|
+
...(preparedRemediation.postimage
|
|
3103
|
+
? [{
|
|
3104
|
+
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
3105
|
+
content: preparedRemediation.postimage,
|
|
3106
|
+
}]
|
|
3107
|
+
: []),
|
|
3108
|
+
...(preparedYield
|
|
3109
|
+
? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
|
|
3110
|
+
: []),
|
|
3111
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
|
|
3112
|
+
...(reviewLease
|
|
3113
|
+
? [{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null }]
|
|
3114
|
+
: []),
|
|
3115
|
+
],
|
|
3116
|
+
validate: () => {
|
|
3117
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
3118
|
+
const savedStep = requireStep(saved, stepId);
|
|
3119
|
+
const savedRemediation = readRemediationEvents(this.store, saved)
|
|
3120
|
+
.find((event) => event.eventId === preparedRemediation.event.eventId) ?? null;
|
|
3121
|
+
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3122
|
+
if (saved.revision !== paused.revision
|
|
3123
|
+
|| saved.status !== 'needs_fix'
|
|
3124
|
+
|| savedStep.status !== 'failed'
|
|
3125
|
+
|| savedRemediation?.eventHash !== preparedRemediation.event.eventHash
|
|
3126
|
+
|| context.locks.inspect(task.id) !== null
|
|
3127
|
+
|| (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
|
|
3128
|
+
|| headCommit(context.identity.repositoryRoot) !== failureHead
|
|
3129
|
+
|| JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
|
|
3130
|
+
|| hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== dirtyWorktreeHash) {
|
|
3131
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review failure-pause transaction readback failed.', {
|
|
3132
|
+
taskId: task.id,
|
|
3133
|
+
stepId,
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
3137
|
+
},
|
|
3138
|
+
});
|
|
3139
|
+
return this.store.readTask(task.projectId, task.id);
|
|
3140
|
+
}
|
|
3141
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
|
|
2512
3142
|
const identity = resolveRepositoryIdentity(repository);
|
|
2513
3143
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2514
3144
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2525,7 +3155,7 @@ export class WorkflowService {
|
|
|
2525
3155
|
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2526
3156
|
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2527
3157
|
}
|
|
2528
|
-
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
3158
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'), task.steps.flatMap((step) => reviewScopeAugmentations(this.store, task, step.id)), reviewMode);
|
|
2529
3159
|
}
|
|
2530
3160
|
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2531
3161
|
const context = this.#mutationContext(repository);
|
|
@@ -2533,7 +3163,10 @@ export class WorkflowService {
|
|
|
2533
3163
|
assertExpectedRevision(task, expectedRevision);
|
|
2534
3164
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2535
3165
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2536
|
-
|
|
3166
|
+
if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
|
|
3167
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'External Task Review requires reviewMode ordinary or security.');
|
|
3168
|
+
}
|
|
3169
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision, input.reviewMode);
|
|
2537
3170
|
let review;
|
|
2538
3171
|
try {
|
|
2539
3172
|
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
@@ -2547,6 +3180,7 @@ export class WorkflowService {
|
|
|
2547
3180
|
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2548
3181
|
return {
|
|
2549
3182
|
mode: 'external-sealed',
|
|
3183
|
+
reviewMode: packet.reviewMode,
|
|
2550
3184
|
packetHash: packet.packetHash,
|
|
2551
3185
|
repositorySealHash: packet.repositorySealHash,
|
|
2552
3186
|
reviewerThreadId: input.reviewerThreadId,
|
|
@@ -2651,8 +3285,6 @@ export class WorkflowService {
|
|
|
2651
3285
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2652
3286
|
assertExpectedRevision(task, expectedRevision);
|
|
2653
3287
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2654
|
-
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2655
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2656
3288
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2657
3289
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2658
3290
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
@@ -2684,6 +3316,20 @@ export class WorkflowService {
|
|
|
2684
3316
|
blockers: assessment.blockers,
|
|
2685
3317
|
});
|
|
2686
3318
|
}
|
|
3319
|
+
const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
3320
|
+
? [assessment.evidence.remediationEventId]
|
|
3321
|
+
: assessment.evidence.remediationEvents.map((event) => event.eventId);
|
|
3322
|
+
const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
|
|
3323
|
+
&& record.packageVersion === PACKAGE_VERSION
|
|
3324
|
+
&& task.systemCommits.includes(record.commitSha)
|
|
3325
|
+
&& record.planIntegrity?.stepId === stepId
|
|
3326
|
+
&& record.planIntegrity.planHash === task.planHash
|
|
3327
|
+
&& canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
|
|
3328
|
+
&& record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
|
|
3329
|
+
if (!compatibility) {
|
|
3330
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
3331
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3332
|
+
}
|
|
2687
3333
|
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2688
3334
|
return { assessment, event };
|
|
2689
3335
|
}
|
|
@@ -2769,7 +3415,12 @@ export class WorkflowService {
|
|
|
2769
3415
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
2770
3416
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
2771
3417
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
2772
|
-
const
|
|
3418
|
+
const correctiveAuthority = delegationGrantId === null
|
|
3419
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
3420
|
+
: null;
|
|
3421
|
+
const delegation = correctiveAuthority
|
|
3422
|
+
? null
|
|
3423
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
|
|
2773
3424
|
const event = {
|
|
2774
3425
|
kind: 'final_acceptance',
|
|
2775
3426
|
decision: 'approved',
|
|
@@ -2781,7 +3432,11 @@ export class WorkflowService {
|
|
|
2781
3432
|
evidenceHash,
|
|
2782
3433
|
headCommit: headCommit(identity.repositoryRoot),
|
|
2783
3434
|
reason: '',
|
|
2784
|
-
authorizationMode:
|
|
3435
|
+
authorizationMode: correctiveAuthority
|
|
3436
|
+
? 'corrective-derived'
|
|
3437
|
+
: delegation
|
|
3438
|
+
? 'delegated'
|
|
3439
|
+
: 'human',
|
|
2785
3440
|
...(delegation ? { delegation } : {}),
|
|
2786
3441
|
};
|
|
2787
3442
|
return this.store.writeTask({
|
|
@@ -2884,7 +3539,9 @@ export class WorkflowService {
|
|
|
2884
3539
|
return saved;
|
|
2885
3540
|
}
|
|
2886
3541
|
status(repository) {
|
|
2887
|
-
|
|
3542
|
+
return this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
3543
|
+
}
|
|
3544
|
+
statusFromSnapshot(snapshot) {
|
|
2888
3545
|
return {
|
|
2889
3546
|
projectId: snapshot.identity.projectId,
|
|
2890
3547
|
discoveries: snapshot.discoveries,
|
|
@@ -2898,6 +3555,73 @@ export class WorkflowService {
|
|
|
2898
3555
|
observation: snapshot.observation,
|
|
2899
3556
|
};
|
|
2900
3557
|
}
|
|
3558
|
+
compactStatus(repository) {
|
|
3559
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3560
|
+
const status = this.statusFromSnapshot(snapshot);
|
|
3561
|
+
const next = this.nextFromStatus(repository, null, status);
|
|
3562
|
+
const routedId = typeof next.id === 'string'
|
|
3563
|
+
? next.id
|
|
3564
|
+
: typeof next.taskId === 'string'
|
|
3565
|
+
? next.taskId
|
|
3566
|
+
: null;
|
|
3567
|
+
const activeTask = routedId === null
|
|
3568
|
+
? null
|
|
3569
|
+
: snapshot.tasks.find((task) => task.id === routedId) ?? null;
|
|
3570
|
+
const routedStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
3571
|
+
const activeStep = activeTask === null
|
|
3572
|
+
? null
|
|
3573
|
+
: routedStepId === null
|
|
3574
|
+
? activeTask.steps.find((step) => step.status === 'in_progress')
|
|
3575
|
+
?? activeTask.steps.find((step) => step.status === 'failed')
|
|
3576
|
+
?? null
|
|
3577
|
+
: activeTask.steps.find((step) => step.id === routedStepId) ?? null;
|
|
3578
|
+
const action = typeof next.action === 'string' ? next.action : 'blocked';
|
|
3579
|
+
const blockedAction = typeof next.blockedAction === 'string' ? next.blockedAction : null;
|
|
3580
|
+
const navigation = next.navigation && typeof next.navigation === 'object' && !Array.isArray(next.navigation)
|
|
3581
|
+
? next.navigation
|
|
3582
|
+
: null;
|
|
3583
|
+
const route = {
|
|
3584
|
+
scope: typeof next.scope === 'string' ? next.scope : null,
|
|
3585
|
+
status: typeof next.status === 'string' ? next.status : null,
|
|
3586
|
+
entityId: routedId,
|
|
3587
|
+
navigationKind: navigation && typeof navigation.kind === 'string' ? navigation.kind : null,
|
|
3588
|
+
transitionId: navigation && typeof navigation.transitionId === 'string' ? navigation.transitionId : null,
|
|
3589
|
+
};
|
|
3590
|
+
const routeFingerprint = sha256Hex(canonicalJsonStringify({
|
|
3591
|
+
projectId: snapshot.identity.projectId,
|
|
3592
|
+
taskId: activeTask?.id ?? null,
|
|
3593
|
+
taskRevision: activeTask?.revision ?? null,
|
|
3594
|
+
stepId: activeStep?.id ?? routedStepId,
|
|
3595
|
+
action,
|
|
3596
|
+
blockedAction,
|
|
3597
|
+
route,
|
|
3598
|
+
observationKind: snapshot.observation.kind,
|
|
3599
|
+
observationReasonCodes: snapshot.observation.reasonCodes,
|
|
3600
|
+
}));
|
|
3601
|
+
return {
|
|
3602
|
+
projection: 'codex-workflow-compact-status-v1',
|
|
3603
|
+
bounded: true,
|
|
3604
|
+
projectId: snapshot.identity.projectId,
|
|
3605
|
+
counts: {
|
|
3606
|
+
discoveries: snapshot.discoveries.length,
|
|
3607
|
+
tasks: snapshot.tasks.length,
|
|
3608
|
+
milestones: snapshot.milestones.length,
|
|
3609
|
+
delegations: snapshot.delegations.length,
|
|
3610
|
+
},
|
|
3611
|
+
activeTask: activeTask
|
|
3612
|
+
? { id: activeTask.id, revision: activeTask.revision, status: activeTask.status }
|
|
3613
|
+
: null,
|
|
3614
|
+
activeStep: activeStep ? { id: activeStep.id, status: activeStep.status } : null,
|
|
3615
|
+
action,
|
|
3616
|
+
blockedAction,
|
|
3617
|
+
routeFingerprint,
|
|
3618
|
+
route,
|
|
3619
|
+
observation: {
|
|
3620
|
+
kind: snapshot.observation.kind,
|
|
3621
|
+
reasonCodes: [...snapshot.observation.reasonCodes],
|
|
3622
|
+
},
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
2901
3625
|
recoverMilestoneTransactions(repository) {
|
|
2902
3626
|
const { identity } = this.#mutationContext(repository);
|
|
2903
3627
|
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
@@ -3012,6 +3736,137 @@ export class WorkflowService {
|
|
|
3012
3736
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3013
3737
|
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3014
3738
|
}
|
|
3739
|
+
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
3740
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3741
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3742
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
|
|
3743
|
+
observation: snapshot.observation,
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3747
|
+
if (!task)
|
|
3748
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3749
|
+
return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
|
|
3750
|
+
}
|
|
3751
|
+
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
3752
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3753
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3754
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
|
|
3755
|
+
observation: snapshot.observation,
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3759
|
+
if (!task)
|
|
3760
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3761
|
+
let candidateParent = null;
|
|
3762
|
+
try {
|
|
3763
|
+
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
3764
|
+
}
|
|
3765
|
+
catch {
|
|
3766
|
+
candidateParent = null;
|
|
3767
|
+
}
|
|
3768
|
+
const assessment = candidateParent
|
|
3769
|
+
? this.inspectHistoricalStepProvenanceCandidate(snapshot, task, {
|
|
3770
|
+
historyBoundary: 'HEAD^',
|
|
3771
|
+
preservedHeadCommit: candidateParent,
|
|
3772
|
+
})
|
|
3773
|
+
: null;
|
|
3774
|
+
if (!assessment?.eligible
|
|
3775
|
+
|| !assessment.activeStepId
|
|
3776
|
+
|| !assessment.commitSha
|
|
3777
|
+
|| !task.planHash) {
|
|
3778
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3779
|
+
return {
|
|
3780
|
+
...ordinary,
|
|
3781
|
+
eligible: false,
|
|
3782
|
+
action: null,
|
|
3783
|
+
historicalStepProvenance: null,
|
|
3784
|
+
blockers: [...new Set([
|
|
3785
|
+
...ordinary.blockers,
|
|
3786
|
+
...(assessment?.blockers ?? []),
|
|
3787
|
+
'The dependency candidate parent must contain exactly one accepted and strictly reviewed historical Step commit omitted by the current corrective Plan.',
|
|
3788
|
+
])],
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
let dirtyWorktreeHash;
|
|
3792
|
+
try {
|
|
3793
|
+
dirtyWorktreeHash = hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.dirtyFiles);
|
|
3794
|
+
}
|
|
3795
|
+
catch (error) {
|
|
3796
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3797
|
+
return {
|
|
3798
|
+
...ordinary,
|
|
3799
|
+
eligible: false,
|
|
3800
|
+
action: null,
|
|
3801
|
+
historicalStepProvenance: null,
|
|
3802
|
+
blockers: [...new Set([
|
|
3803
|
+
...ordinary.blockers,
|
|
3804
|
+
error instanceof Error ? error.message : String(error),
|
|
3805
|
+
])],
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, {
|
|
3809
|
+
activeStepId: assessment.activeStepId,
|
|
3810
|
+
planHash: task.planHash,
|
|
3811
|
+
dirtyFiles: assessment.dirtyFiles,
|
|
3812
|
+
dirtyWorktreeHash,
|
|
3813
|
+
retainedCommitSha: assessment.commitSha,
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
recoverHistoricalStepProvenance(repository, taskId, expectedRevision, commitSha, actor, reason) {
|
|
3817
|
+
const normalizedActor = actor.trim();
|
|
3818
|
+
const normalizedReason = reason.trim();
|
|
3819
|
+
if (!normalizedActor)
|
|
3820
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires an actor.');
|
|
3821
|
+
if (!normalizedReason)
|
|
3822
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires a reason.');
|
|
3823
|
+
const context = this.#mutationContext(repository);
|
|
3824
|
+
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
3825
|
+
const prior = current.historicalStepProvenanceRecoveries?.find((record) => record.commitSha === commitSha);
|
|
3826
|
+
if (prior && (current.invalidatedStepCommits ?? []).includes(commitSha)) {
|
|
3827
|
+
validateTaskHistory(context.identity.repositoryRoot, current);
|
|
3828
|
+
return current;
|
|
3829
|
+
}
|
|
3830
|
+
assertExpectedRevision(current, expectedRevision);
|
|
3831
|
+
const preflight = this.historicalStepProvenanceRecoveryPreflight(repository, taskId);
|
|
3832
|
+
if (!preflight.eligible || preflight.commitSha !== commitSha || !preflight.sourceFinalAcceptanceRecordedAt) {
|
|
3833
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded historical Step provenance recovery preflight failed.', {
|
|
3834
|
+
taskId,
|
|
3835
|
+
requestedCommit: commitSha,
|
|
3836
|
+
candidateCommit: preflight.commitSha,
|
|
3837
|
+
blockers: preflight.blockers,
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
const sourceAcceptance = current.authorizations.find((authorization) => authorization.kind === 'final_acceptance'
|
|
3841
|
+
&& ['approved', 'superseded'].includes(authorization.decision)
|
|
3842
|
+
&& authorization.headCommit === commitSha
|
|
3843
|
+
&& authorization.recordedAt === preflight.sourceFinalAcceptanceRecordedAt
|
|
3844
|
+
&& authorization.planHash
|
|
3845
|
+
&& authorization.resultHash
|
|
3846
|
+
&& authorization.evidenceHash);
|
|
3847
|
+
if (!sourceAcceptance?.planHash || !sourceAcceptance.resultHash || !sourceAcceptance.evidenceHash) {
|
|
3848
|
+
throw new WorkflowError('STATE_CONFLICT', 'The source final acceptance changed after historical provenance preflight.');
|
|
3849
|
+
}
|
|
3850
|
+
const saved = context.store.writeTask({
|
|
3851
|
+
...current,
|
|
3852
|
+
invalidatedStepCommits: [...new Set([...(current.invalidatedStepCommits ?? []), commitSha])],
|
|
3853
|
+
historicalStepProvenanceRecoveries: [
|
|
3854
|
+
...(current.historicalStepProvenanceRecoveries ?? []),
|
|
3855
|
+
{
|
|
3856
|
+
commitSha,
|
|
3857
|
+
actor: normalizedActor,
|
|
3858
|
+
reason: normalizedReason,
|
|
3859
|
+
recordedAt: this.now().toISOString(),
|
|
3860
|
+
sourceFinalAcceptanceRecordedAt: sourceAcceptance.recordedAt,
|
|
3861
|
+
sourcePlanHash: sourceAcceptance.planHash,
|
|
3862
|
+
sourceResultHash: sourceAcceptance.resultHash,
|
|
3863
|
+
sourceEvidenceHash: sourceAcceptance.evidenceHash,
|
|
3864
|
+
},
|
|
3865
|
+
],
|
|
3866
|
+
}, expectedRevision);
|
|
3867
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
3868
|
+
return saved;
|
|
3869
|
+
}
|
|
3015
3870
|
activeDownstreamProofDependencyRecoveryPreflight(repository, taskId) {
|
|
3016
3871
|
const snapshot = this.observationSnapshot(repository);
|
|
3017
3872
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -3108,6 +3963,173 @@ export class WorkflowService {
|
|
|
3108
3963
|
blockers,
|
|
3109
3964
|
};
|
|
3110
3965
|
}
|
|
3966
|
+
planIntegrityUpdatePreflight(repository, taskId) {
|
|
3967
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3968
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3969
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
|
|
3970
|
+
observation: snapshot.observation,
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3974
|
+
if (!task)
|
|
3975
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3976
|
+
const assessments = task.steps
|
|
3977
|
+
.filter((step) => step.status === 'failed')
|
|
3978
|
+
.map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
|
|
3979
|
+
.filter((assessment) => assessment.applicable);
|
|
3980
|
+
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
3981
|
+
const adoptedCompatibility = assessment
|
|
3982
|
+
? null
|
|
3983
|
+
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
3984
|
+
const dirtyCarryoverAssessment = assessment || adoptedCompatibility
|
|
3985
|
+
? null
|
|
3986
|
+
: this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
3987
|
+
const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
|
|
3988
|
+
&& dirtyCarryoverAssessment.compatibility
|
|
3989
|
+
? {
|
|
3990
|
+
...dirtyCarryoverAssessment.compatibility,
|
|
3991
|
+
planHash: task.planHash,
|
|
3992
|
+
mode: 'post-rebind-dirty-carryover',
|
|
3993
|
+
}
|
|
3994
|
+
: null;
|
|
3995
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
3996
|
+
const versionSurfaces = {
|
|
3997
|
+
declared: versions.declared,
|
|
3998
|
+
locked: versions.locked,
|
|
3999
|
+
installed: versions.installed,
|
|
4000
|
+
currentBranch: versions.currentBranch,
|
|
4001
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4002
|
+
};
|
|
4003
|
+
const sourceVersion = versionSurfaces.declared;
|
|
4004
|
+
const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
|
|
4005
|
+
const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
|
|
4006
|
+
const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
|
|
4007
|
+
&& sourceBetaVersion.patch !== null
|
|
4008
|
+
&& sourceBetaVersion.patch >= 11;
|
|
4009
|
+
const targetIsNewer = sourceBetaVersion !== null
|
|
4010
|
+
&& targetBetaVersion !== null
|
|
4011
|
+
&& (targetBetaVersion.train > sourceBetaVersion.train
|
|
4012
|
+
|| (targetBetaVersion.train === sourceBetaVersion.train
|
|
4013
|
+
&& targetBetaVersion.patch !== null
|
|
4014
|
+
&& sourceBetaVersion.patch !== null
|
|
4015
|
+
&& targetBetaVersion.patch > sourceBetaVersion.patch));
|
|
4016
|
+
const blockers = [
|
|
4017
|
+
...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
|
|
4018
|
+
...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
|
|
4019
|
+
? []
|
|
4020
|
+
: (dirtyCarryoverAssessment?.blockers.length
|
|
4021
|
+
? dirtyCarryoverAssessment.blockers
|
|
4022
|
+
: ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
|
|
4023
|
+
...(!sourceIsEligibleBeta12 || !targetIsNewer
|
|
4024
|
+
? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
|
|
4025
|
+
: []),
|
|
4026
|
+
...Object.entries(versionSurfaces)
|
|
4027
|
+
.filter(([, version]) => version !== sourceVersion)
|
|
4028
|
+
.map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
|
|
4029
|
+
...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4030
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4031
|
+
];
|
|
4032
|
+
return {
|
|
4033
|
+
readOnly: true,
|
|
4034
|
+
eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
|
|
4035
|
+
action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
|
|
4036
|
+
? 'update plan-integrity-transport'
|
|
4037
|
+
: null,
|
|
4038
|
+
projectId: task.projectId,
|
|
4039
|
+
taskId: task.id,
|
|
4040
|
+
taskRevision: task.revision,
|
|
4041
|
+
stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
|
|
4042
|
+
sourceVersion,
|
|
4043
|
+
targetVersion: PACKAGE_VERSION,
|
|
4044
|
+
taskBranch: task.taskBranch,
|
|
4045
|
+
baseBranch: task.baseBranch,
|
|
4046
|
+
dirtyFiles: assessment?.evidence?.changedFiles
|
|
4047
|
+
?? adoptedCompatibility?.dirtyFiles
|
|
4048
|
+
?? dirtyCarryoverCompatibility?.dirtyFiles
|
|
4049
|
+
?? [],
|
|
4050
|
+
conflictHash: assessment?.evidence?.conflictHash
|
|
4051
|
+
?? adoptedCompatibility?.conflictHash
|
|
4052
|
+
?? dirtyCarryoverCompatibility?.conflictHash
|
|
4053
|
+
?? null,
|
|
4054
|
+
compatibilityMode: adoptedCompatibility?.mode
|
|
4055
|
+
?? dirtyCarryoverCompatibility?.mode
|
|
4056
|
+
?? 'failed-step-conflict',
|
|
4057
|
+
requiredTransport: [
|
|
4058
|
+
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4059
|
+
'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
|
|
4060
|
+
'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
|
|
4061
|
+
],
|
|
4062
|
+
blockers,
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
planIntegrityDependencyRecoveryPreflight(repository, taskId) {
|
|
4066
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4067
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
4068
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
|
|
4069
|
+
observation: snapshot.observation,
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4073
|
+
if (!task)
|
|
4074
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4075
|
+
const assessmentBlockers = [];
|
|
4076
|
+
const assessments = task.steps
|
|
4077
|
+
.filter((step) => step.status === 'failed')
|
|
4078
|
+
.flatMap((step) => {
|
|
4079
|
+
try {
|
|
4080
|
+
return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
|
|
4081
|
+
}
|
|
4082
|
+
catch (error) {
|
|
4083
|
+
if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
|
|
4084
|
+
assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
|
|
4085
|
+
return [];
|
|
4086
|
+
}
|
|
4087
|
+
throw error;
|
|
4088
|
+
}
|
|
4089
|
+
})
|
|
4090
|
+
.filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
|
|
4091
|
+
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4092
|
+
const adoptedCompatibility = assessment?.evidence
|
|
4093
|
+
? null
|
|
4094
|
+
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4095
|
+
const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
|
|
4096
|
+
? null
|
|
4097
|
+
: this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
|
|
4098
|
+
const carriedCompatibility = !assessment?.evidence && task.planHash
|
|
4099
|
+
? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
|
|
4100
|
+
: null;
|
|
4101
|
+
if ((!assessment?.evidence
|
|
4102
|
+
&& !carriedCompatibility
|
|
4103
|
+
&& !adoptedCompatibility
|
|
4104
|
+
&& !dirtyCarryoverCompatibility) || !task.planHash) {
|
|
4105
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4106
|
+
return {
|
|
4107
|
+
...ordinary,
|
|
4108
|
+
eligible: false,
|
|
4109
|
+
action: null,
|
|
4110
|
+
planIntegrity: null,
|
|
4111
|
+
blockers: [...new Set([
|
|
4112
|
+
...ordinary.blockers,
|
|
4113
|
+
...assessmentBlockers,
|
|
4114
|
+
'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
|
|
4115
|
+
])],
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
const compatibility = carriedCompatibility
|
|
4119
|
+
?? adoptedCompatibility
|
|
4120
|
+
?? dirtyCarryoverCompatibility
|
|
4121
|
+
?? {
|
|
4122
|
+
stepId: assessment.stepId,
|
|
4123
|
+
planHash: task.planHash,
|
|
4124
|
+
conflictHash: assessment.evidence.conflictHash,
|
|
4125
|
+
dirtyFiles: [...assessment.evidence.changedFiles],
|
|
4126
|
+
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
|
|
4127
|
+
remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
4128
|
+
? [assessment.evidence.remediationEventId]
|
|
4129
|
+
: assessment.evidence.remediationEvents.map((event) => event.eventId),
|
|
4130
|
+
};
|
|
4131
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
|
|
4132
|
+
}
|
|
3111
4133
|
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
3112
4134
|
const snapshot = this.observationSnapshot(repository);
|
|
3113
4135
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -3159,6 +4181,12 @@ export class WorkflowService {
|
|
|
3159
4181
|
recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3160
4182
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
|
|
3161
4183
|
}
|
|
4184
|
+
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4185
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
4186
|
+
}
|
|
4187
|
+
recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4188
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
|
|
4189
|
+
}
|
|
3162
4190
|
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
3163
4191
|
const normalizedActor = actor.trim();
|
|
3164
4192
|
const normalizedReason = reason.trim();
|
|
@@ -3173,7 +4201,11 @@ export class WorkflowService {
|
|
|
3173
4201
|
&& record.packageVersion === PACKAGE_VERSION
|
|
3174
4202
|
&& (recoveryMode === 'downstream-replan'
|
|
3175
4203
|
? Boolean(record.downstreamProofReplan)
|
|
3176
|
-
:
|
|
4204
|
+
: recoveryMode === 'historical-step'
|
|
4205
|
+
? Boolean(record.historicalStepProvenance)
|
|
4206
|
+
: recoveryMode === 'plan-integrity'
|
|
4207
|
+
? Boolean(record.planIntegrity)
|
|
4208
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
3177
4209
|
if (prior && current.systemCommits.includes(currentHead))
|
|
3178
4210
|
return current;
|
|
3179
4211
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -3183,39 +4215,115 @@ export class WorkflowService {
|
|
|
3183
4215
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3184
4216
|
const preflight = recoveryMode === 'downstream-replan'
|
|
3185
4217
|
? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
|
|
3186
|
-
: recoveryMode
|
|
3187
|
-
? this.
|
|
3188
|
-
:
|
|
4218
|
+
: recoveryMode === 'historical-step'
|
|
4219
|
+
? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
|
|
4220
|
+
: recoveryMode === 'plan-integrity'
|
|
4221
|
+
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
4222
|
+
: recoveryMode
|
|
4223
|
+
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4224
|
+
: this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3189
4225
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
3190
4226
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
3191
4227
|
taskId,
|
|
3192
4228
|
blockers: preflight.blockers,
|
|
3193
4229
|
});
|
|
3194
4230
|
}
|
|
3195
|
-
|
|
4231
|
+
const now = this.now();
|
|
4232
|
+
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
4233
|
+
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
4234
|
+
return {
|
|
4235
|
+
commitSha: dependencyCommit.commitSha,
|
|
4236
|
+
parentCommitSha: dependencyCommit.parentCommitSha,
|
|
4237
|
+
packageName: PACKAGE_NAME,
|
|
4238
|
+
packageVersion: dependencyCommit.packageVersion,
|
|
4239
|
+
files: ['package-lock.json', 'package.json'],
|
|
4240
|
+
actor: normalizedActor,
|
|
4241
|
+
reason: normalizedReason,
|
|
4242
|
+
recordedAt: now.toISOString(),
|
|
4243
|
+
retainedHistoricalCommits: isTerminalCommit ? preflight.retainedHistoricalCommits : [],
|
|
4244
|
+
...(isTerminalCommit && preflight.activeDownstreamProof
|
|
4245
|
+
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
4246
|
+
: {}),
|
|
4247
|
+
...(isTerminalCommit && preflight.downstreamProofReplan
|
|
4248
|
+
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
4249
|
+
: {}),
|
|
4250
|
+
...(isTerminalCommit && preflight.historicalStepProvenance
|
|
4251
|
+
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
4252
|
+
: {}),
|
|
4253
|
+
...(isTerminalCommit && preflight.planIntegrity
|
|
4254
|
+
? { planIntegrity: preflight.planIntegrity }
|
|
4255
|
+
: {}),
|
|
4256
|
+
};
|
|
4257
|
+
});
|
|
4258
|
+
const nextTask = {
|
|
3196
4259
|
...task,
|
|
3197
|
-
|
|
4260
|
+
revision: task.revision + 1,
|
|
4261
|
+
updatedAt: now.toISOString(),
|
|
4262
|
+
systemCommits: [
|
|
4263
|
+
...task.systemCommits,
|
|
4264
|
+
...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
|
|
4265
|
+
],
|
|
3198
4266
|
dependencyProvenanceRecoveries: [
|
|
3199
4267
|
...(task.dependencyProvenanceRecoveries ?? []),
|
|
4268
|
+
...recoveredRecords,
|
|
4269
|
+
],
|
|
4270
|
+
};
|
|
4271
|
+
if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
|
|
4272
|
+
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
4273
|
+
}
|
|
4274
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
|
|
4275
|
+
const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
|
|
4276
|
+
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
4277
|
+
let refreshedKnowledgeMap = null;
|
|
4278
|
+
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
4279
|
+
const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
|
|
4280
|
+
if (!knowledgeRefresh.eligible) {
|
|
4281
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
|
|
4282
|
+
taskId,
|
|
4283
|
+
dependencyKnowledgeRefresh: knowledgeRefresh,
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4286
|
+
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
4287
|
+
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
4288
|
+
if (!approved.approval) {
|
|
4289
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
|
|
4290
|
+
}
|
|
4291
|
+
refreshedKnowledgeMap = {
|
|
4292
|
+
...approved,
|
|
4293
|
+
revision: currentKnowledgeMap.revision + 2,
|
|
4294
|
+
createdAt: currentKnowledgeMap.createdAt,
|
|
4295
|
+
updatedAt: now.toISOString(),
|
|
4296
|
+
approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
4300
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
4301
|
+
applyProjectTransaction({
|
|
4302
|
+
projectRoot,
|
|
4303
|
+
projectId: task.projectId,
|
|
4304
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
4305
|
+
kind: 'plan-integrity-dependency-recovery',
|
|
4306
|
+
now,
|
|
4307
|
+
targets: [
|
|
4308
|
+
...(refreshedKnowledgeMap
|
|
4309
|
+
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
4310
|
+
: []),
|
|
3200
4311
|
{
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
packageName: PACKAGE_NAME,
|
|
3204
|
-
packageVersion: PACKAGE_VERSION,
|
|
3205
|
-
files: ['package-lock.json', 'package.json'],
|
|
3206
|
-
actor: normalizedActor,
|
|
3207
|
-
reason: normalizedReason,
|
|
3208
|
-
recordedAt: this.now().toISOString(),
|
|
3209
|
-
retainedHistoricalCommits: preflight.retainedHistoricalCommits,
|
|
3210
|
-
...(preflight.activeDownstreamProof
|
|
3211
|
-
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
3212
|
-
: {}),
|
|
3213
|
-
...(preflight.downstreamProofReplan
|
|
3214
|
-
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
3215
|
-
: {}),
|
|
4312
|
+
path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
|
|
4313
|
+
content: `${JSON.stringify(nextTask, null, 2)}\n`,
|
|
3216
4314
|
},
|
|
3217
4315
|
],
|
|
3218
|
-
|
|
4316
|
+
validate: () => {
|
|
4317
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
4318
|
+
if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
|
|
4319
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
|
|
4320
|
+
}
|
|
4321
|
+
if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
|
|
4322
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
|
|
4323
|
+
}
|
|
4324
|
+
},
|
|
4325
|
+
});
|
|
4326
|
+
return this.store.readTask(task.projectId, task.id);
|
|
3219
4327
|
}
|
|
3220
4328
|
milestoneProgress(repository, milestoneId) {
|
|
3221
4329
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -3472,7 +4580,9 @@ export class WorkflowService {
|
|
|
3472
4580
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
3473
4581
|
}
|
|
3474
4582
|
next(repository, taskId = null) {
|
|
3475
|
-
|
|
4583
|
+
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
4584
|
+
}
|
|
4585
|
+
nextFromStatus(repository, taskId, status) {
|
|
3476
4586
|
if (status.observation.kind === 'blocked') {
|
|
3477
4587
|
return {
|
|
3478
4588
|
scope: 'project',
|
|
@@ -3612,9 +4722,69 @@ export class WorkflowService {
|
|
|
3612
4722
|
...(dependency ? { dependency } : {}),
|
|
3613
4723
|
});
|
|
3614
4724
|
}
|
|
4725
|
+
const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
|
|
4726
|
+
if (milestone.status !== 'active')
|
|
4727
|
+
return [];
|
|
4728
|
+
const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
|
|
4729
|
+
.reverse()
|
|
4730
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
4731
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
4732
|
+
&& candidate.remediationEvidence?.remediationTaskId);
|
|
4733
|
+
if (!event?.remediationEvidence)
|
|
4734
|
+
return [];
|
|
4735
|
+
const task = tasksById.get(event.remediationEvidence.remediationTaskId);
|
|
4736
|
+
return task
|
|
4737
|
+
&& taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4738
|
+
? [task]
|
|
4739
|
+
: [];
|
|
4740
|
+
}).at(0) ?? null;
|
|
3615
4741
|
const activeTask = requestedTask
|
|
4742
|
+
?? prioritizedRemediationTask
|
|
3616
4743
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3617
4744
|
if (activeTask) {
|
|
4745
|
+
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4746
|
+
if (historicalStepProvenanceRecovery.eligible) {
|
|
4747
|
+
return withAdoption({
|
|
4748
|
+
scope: 'task',
|
|
4749
|
+
id: activeTask.id,
|
|
4750
|
+
status: activeTask.status,
|
|
4751
|
+
action: 'task historical-step-provenance-recover',
|
|
4752
|
+
revision: activeTask.revision,
|
|
4753
|
+
historicalStepProvenanceRecovery,
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
const historicalStepDependencyRecovery = this.historicalStepDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4757
|
+
if (historicalStepDependencyRecovery.eligible) {
|
|
4758
|
+
return withAdoption({
|
|
4759
|
+
scope: 'task',
|
|
4760
|
+
id: activeTask.id,
|
|
4761
|
+
status: activeTask.status,
|
|
4762
|
+
action: 'update historical-step-dependency-recover',
|
|
4763
|
+
revision: activeTask.revision,
|
|
4764
|
+
dependencyProvenanceRecovery: historicalStepDependencyRecovery,
|
|
4765
|
+
compatibilityBoundary: {
|
|
4766
|
+
preservesWorktree: true,
|
|
4767
|
+
prerequisite: 'The active Milestone base and Task branch contain only the exact target dependency commits; the candidate parent still binds the reviewed historical Step commit.',
|
|
4768
|
+
continuation: 'Register the dependency commit and historical provenance atomically, then follow fresh next.',
|
|
4769
|
+
},
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4773
|
+
if (planIntegrityDependencyRecovery.eligible) {
|
|
4774
|
+
return withAdoption({
|
|
4775
|
+
scope: 'task',
|
|
4776
|
+
id: activeTask.id,
|
|
4777
|
+
status: activeTask.status,
|
|
4778
|
+
action: 'update plan-integrity-dependency-recover',
|
|
4779
|
+
revision: activeTask.revision,
|
|
4780
|
+
dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
|
|
4781
|
+
compatibilityBoundary: {
|
|
4782
|
+
preservesWorktree: true,
|
|
4783
|
+
prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
|
|
4784
|
+
continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
|
|
4785
|
+
},
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
3618
4788
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3619
4789
|
if (dependencyRecovery.eligible) {
|
|
3620
4790
|
return withAdoption({
|
|
@@ -3741,41 +4911,132 @@ export class WorkflowService {
|
|
|
3741
4911
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3742
4912
|
throw error;
|
|
3743
4913
|
const blockingDecision = error.details.decision;
|
|
3744
|
-
|
|
4914
|
+
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
4915
|
+
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
4916
|
+
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
4917
|
+
.reverse()
|
|
4918
|
+
.find((event) => event.authorizationMode === 'audited-remediation'
|
|
4919
|
+
&& event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
|
|
4920
|
+
: null;
|
|
4921
|
+
const existingRemediationTask = existingRemediationEvent?.remediationEvidence
|
|
4922
|
+
? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
|
|
4923
|
+
: null;
|
|
4924
|
+
const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
|
|
4925
|
+
const remediationMilestone = activeTask.milestoneId
|
|
4926
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
4927
|
+
: null;
|
|
4928
|
+
const remediationMemberships = remediationMilestone
|
|
4929
|
+
? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
|
|
4930
|
+
: new Map();
|
|
4931
|
+
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
4932
|
+
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
4933
|
+
&& remediationMilestone?.status === 'active'
|
|
4934
|
+
&& remediationMembership?.disposition === 'required';
|
|
4935
|
+
const remediationAncestorIds = new Set();
|
|
4936
|
+
const visitRemediationAncestors = (candidateTaskId) => {
|
|
4937
|
+
for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
|
|
4938
|
+
if (remediationAncestorIds.has(predecessorTaskId))
|
|
4939
|
+
continue;
|
|
4940
|
+
remediationAncestorIds.add(predecessorTaskId);
|
|
4941
|
+
visitRemediationAncestors(predecessorTaskId);
|
|
4942
|
+
}
|
|
4943
|
+
};
|
|
4944
|
+
visitRemediationAncestors(activeTask.id);
|
|
4945
|
+
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
4946
|
+
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
4947
|
+
.sort();
|
|
4948
|
+
planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
3745
4949
|
? {
|
|
3746
4950
|
scope: 'task',
|
|
3747
4951
|
id: activeTask.id,
|
|
3748
4952
|
status: activeTask.status,
|
|
3749
|
-
action:
|
|
4953
|
+
action: existingRemediationTask?.status === 'merged'
|
|
4954
|
+
? 'task plan-set'
|
|
4955
|
+
: existingRemediationTask
|
|
4956
|
+
? 'doctor'
|
|
4957
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4958
|
+
? 'milestone remediation-materialize'
|
|
4959
|
+
: remediationDiscovery
|
|
4960
|
+
? 'discovery update'
|
|
4961
|
+
: 'discovery start',
|
|
3750
4962
|
blockedAction: 'task authorize',
|
|
3751
4963
|
revision: activeTask.revision,
|
|
3752
4964
|
planRiskAuditDecision: {
|
|
3753
4965
|
state: 'recorded-blocking',
|
|
3754
4966
|
decision: blockingDecision,
|
|
3755
4967
|
reason: error.message,
|
|
3756
|
-
requiredAction:
|
|
3757
|
-
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
3758
|
-
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
4968
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
3759
4969
|
},
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
4970
|
+
auditedRemediation: {
|
|
4971
|
+
state: existingRemediationTask?.status === 'merged'
|
|
4972
|
+
? 'merged-replan-required'
|
|
4973
|
+
: existingRemediationTask
|
|
4974
|
+
? 'materialized-not-merged'
|
|
4975
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4976
|
+
? 'ready-to-materialize'
|
|
4977
|
+
: 'discovery-required',
|
|
4978
|
+
humanApprovalRequired: false,
|
|
4979
|
+
blockedTaskId: activeTask.id,
|
|
4980
|
+
blockedTaskRevision: activeTask.revision,
|
|
4981
|
+
milestoneId: remediationMilestone.id,
|
|
4982
|
+
milestoneRevision: remediationMilestone.revision,
|
|
4983
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
4984
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
4985
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
4986
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
4987
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
4988
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
4989
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
4990
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
4991
|
+
command: existingRemediationTask?.status === 'merged'
|
|
4992
|
+
? 'task plan-set'
|
|
4993
|
+
: existingRemediationTask
|
|
4994
|
+
? 'doctor'
|
|
4995
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4996
|
+
? 'milestone remediation-materialize'
|
|
4997
|
+
: remediationDiscovery
|
|
4998
|
+
? 'discovery update'
|
|
4999
|
+
: 'discovery start',
|
|
5000
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5001
|
+
preservesMilestoneSemantics: true,
|
|
3777
5002
|
},
|
|
3778
|
-
|
|
5003
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5004
|
+
}
|
|
5005
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5006
|
+
? {
|
|
5007
|
+
scope: 'task',
|
|
5008
|
+
id: activeTask.id,
|
|
5009
|
+
status: activeTask.status,
|
|
5010
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5011
|
+
blockedAction: 'task authorize',
|
|
5012
|
+
revision: activeTask.revision,
|
|
5013
|
+
planRiskAuditDecision: {
|
|
5014
|
+
state: 'recorded-blocking',
|
|
5015
|
+
decision: blockingDecision,
|
|
5016
|
+
reason: error.message,
|
|
5017
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
5018
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5019
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5020
|
+
},
|
|
5021
|
+
...(blockingDecision === 'stop-escalate'
|
|
5022
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
5023
|
+
: {}),
|
|
5024
|
+
}
|
|
5025
|
+
: {
|
|
5026
|
+
scope: 'task',
|
|
5027
|
+
id: activeTask.id,
|
|
5028
|
+
status: activeTask.status,
|
|
5029
|
+
action: 'task plan-risk-audit',
|
|
5030
|
+
revision: activeTask.revision,
|
|
5031
|
+
planRiskAuditRecovery: {
|
|
5032
|
+
state: 'required',
|
|
5033
|
+
reason: error.message,
|
|
5034
|
+
currentBriefHash: activeTask.briefHash,
|
|
5035
|
+
currentPlanHash: activeTask.planHash,
|
|
5036
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
5037
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
5038
|
+
},
|
|
5039
|
+
};
|
|
3779
5040
|
}
|
|
3780
5041
|
}
|
|
3781
5042
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -3784,6 +5045,37 @@ export class WorkflowService {
|
|
|
3784
5045
|
? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
|
|
3785
5046
|
: planRiskAuditRecovery
|
|
3786
5047
|
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
5048
|
+
const auditedRemediationNavigation = taskNext?.auditedRemediation;
|
|
5049
|
+
let auditedDependencyKnowledgeRefreshBlocked = null;
|
|
5050
|
+
if (auditedRemediationNavigation
|
|
5051
|
+
&& activeTaskMilestone
|
|
5052
|
+
&& ['discovery start', 'discovery update', 'milestone remediation-materialize'].includes(String(taskNext?.action))) {
|
|
5053
|
+
try {
|
|
5054
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
|
|
5055
|
+
const inspectedKnowledgeMap = inspectCurrentKnowledgeMap();
|
|
5056
|
+
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
5057
|
+
const dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(repositoryRoot, activeTaskMilestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
|
|
5058
|
+
if (dependencyKnowledgeRefresh.eligible) {
|
|
5059
|
+
return withAdoption(withMilestoneCancellation({
|
|
5060
|
+
...taskNext,
|
|
5061
|
+
auditedRemediation: {
|
|
5062
|
+
...auditedRemediationNavigation,
|
|
5063
|
+
dependencyKnowledgeRefresh: {
|
|
5064
|
+
...dependencyKnowledgeRefresh,
|
|
5065
|
+
humanApprovalRequired: false,
|
|
5066
|
+
application: 'atomic-with-remediation-materialization',
|
|
5067
|
+
},
|
|
5068
|
+
},
|
|
5069
|
+
}));
|
|
5070
|
+
}
|
|
5071
|
+
auditedDependencyKnowledgeRefreshBlocked = dependencyKnowledgeRefresh;
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
catch (error) {
|
|
5075
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
5076
|
+
throw error;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
3787
5079
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3788
5080
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3789
5081
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3793,8 +5085,12 @@ export class WorkflowService {
|
|
|
3793
5085
|
|| taskNext?.action === 'task step-review'
|
|
3794
5086
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
3795
5087
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
5088
|
+
|| (taskNext?.action === 'task writer-credential-replace'
|
|
5089
|
+
&& taskNext?.blockedAction === 'task remediation-mode-recover')
|
|
3796
5090
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3797
5091
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
5092
|
+
|| taskNext?.action === 'task corrective-yield'
|
|
5093
|
+
|| String(taskNext?.action ?? '').startsWith('task corrective-replan')
|
|
3798
5094
|
|| hardStop
|
|
3799
5095
|
|| terminalCorrectiveDecision === 'split-required'
|
|
3800
5096
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
@@ -3831,8 +5127,10 @@ export class WorkflowService {
|
|
|
3831
5127
|
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
3832
5128
|
: null)
|
|
3833
5129
|
: null;
|
|
5130
|
+
const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
|
|
5131
|
+
&& taskNext.rollingWave?.state === 'root-cause-replan-required';
|
|
3834
5132
|
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
3835
|
-
&& taskNext?.action !== 'task plan-set'
|
|
5133
|
+
&& (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
|
|
3836
5134
|
&& refreshAssessment?.eligible
|
|
3837
5135
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
3838
5136
|
: [];
|
|
@@ -3878,6 +5176,15 @@ export class WorkflowService {
|
|
|
3878
5176
|
},
|
|
3879
5177
|
}
|
|
3880
5178
|
: {}),
|
|
5179
|
+
...(auditedDependencyKnowledgeRefreshBlocked
|
|
5180
|
+
? {
|
|
5181
|
+
auditedDependencyKnowledgeRefreshBlocked: {
|
|
5182
|
+
...auditedDependencyKnowledgeRefreshBlocked,
|
|
5183
|
+
humanApprovalRequired: true,
|
|
5184
|
+
requiredAction: 'Resolve or explicitly approve the non-dependency Project Knowledge drift.',
|
|
5185
|
+
},
|
|
5186
|
+
}
|
|
5187
|
+
: {}),
|
|
3881
5188
|
}));
|
|
3882
5189
|
}
|
|
3883
5190
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -4184,6 +5491,32 @@ export class WorkflowService {
|
|
|
4184
5491
|
milestonePlanHash: milestone.planHash,
|
|
4185
5492
|
};
|
|
4186
5493
|
}
|
|
5494
|
+
currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
|
|
5495
|
+
if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
|
|
5496
|
+
return null;
|
|
5497
|
+
const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
|
|
5498
|
+
if (milestone.status !== 'active' || !milestone.planHash)
|
|
5499
|
+
return null;
|
|
5500
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
|
|
5501
|
+
if (membership?.disposition !== 'required')
|
|
5502
|
+
return null;
|
|
5503
|
+
const authorization = [...milestone.authorizations]
|
|
5504
|
+
.reverse()
|
|
5505
|
+
.find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
|
|
5506
|
+
if (authorization?.authorizationMode !== 'corrective-derived'
|
|
5507
|
+
|| authorization.planHash !== milestone.planHash
|
|
5508
|
+
|| authorization.actor !== actor.trim())
|
|
5509
|
+
return null;
|
|
5510
|
+
const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
|
|
5511
|
+
.reverse()
|
|
5512
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
5513
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
5514
|
+
&& candidate.remediationEvidence !== null
|
|
5515
|
+
&& candidate.remediationEvidence !== undefined);
|
|
5516
|
+
if (!scopeChange)
|
|
5517
|
+
return null;
|
|
5518
|
+
return { milestone, authorization, scopeChange };
|
|
5519
|
+
}
|
|
4187
5520
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
4188
5521
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
4189
5522
|
? 'task.execution_authorize'
|
|
@@ -4193,6 +5526,12 @@ export class WorkflowService {
|
|
|
4193
5526
|
const delegatedApprovalOptions = transition
|
|
4194
5527
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
4195
5528
|
: [];
|
|
5529
|
+
const correctiveDerivedActor = task.milestoneId
|
|
5530
|
+
? `agent:milestone-remediation:${task.milestoneId}`
|
|
5531
|
+
: null;
|
|
5532
|
+
const correctiveDerivedAuthority = transition && correctiveDerivedActor
|
|
5533
|
+
? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
|
|
5534
|
+
: null;
|
|
4196
5535
|
let remediationModeRecovery = null;
|
|
4197
5536
|
let guardedCorrectivePosture = null;
|
|
4198
5537
|
if (planRiskAudit) {
|
|
@@ -4200,7 +5539,7 @@ export class WorkflowService {
|
|
|
4200
5539
|
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
4201
5540
|
}
|
|
4202
5541
|
catch (error) {
|
|
4203
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5542
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4204
5543
|
if (!remediationModeRecovery)
|
|
4205
5544
|
throw error;
|
|
4206
5545
|
}
|
|
@@ -4213,7 +5552,7 @@ export class WorkflowService {
|
|
|
4213
5552
|
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
4214
5553
|
}
|
|
4215
5554
|
catch (error) {
|
|
4216
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5555
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4217
5556
|
if (!remediationModeRecovery)
|
|
4218
5557
|
throw error;
|
|
4219
5558
|
}
|
|
@@ -4225,6 +5564,11 @@ export class WorkflowService {
|
|
|
4225
5564
|
&& changedFiles(repositoryRoot).length > 0
|
|
4226
5565
|
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
4227
5566
|
: null;
|
|
5567
|
+
const planIntegrityDirtyCarryover = next.action === 'task run'
|
|
5568
|
+
&& nextStepId
|
|
5569
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
5570
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
5571
|
+
: null;
|
|
4228
5572
|
let mechanicalFeasibilityOverlay = {};
|
|
4229
5573
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
4230
5574
|
try {
|
|
@@ -4275,6 +5619,9 @@ export class WorkflowService {
|
|
|
4275
5619
|
}
|
|
4276
5620
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
4277
5621
|
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
5622
|
+
const failedStepRemediations = failedStep && !remediationModeRecovery
|
|
5623
|
+
? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
|
|
5624
|
+
: [];
|
|
4278
5625
|
const failedStepReviewRouting = failedStep
|
|
4279
5626
|
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
4280
5627
|
: null;
|
|
@@ -4405,11 +5752,21 @@ export class WorkflowService {
|
|
|
4405
5752
|
stepId: planIntegrityAssessment.stepId,
|
|
4406
5753
|
planIntegrityRecovery: {
|
|
4407
5754
|
state: 'eligible',
|
|
4408
|
-
reasonCode: '
|
|
5755
|
+
reasonCode: planIntegrityAssessment.evidence?.kind === 'missing-npm-script-outside-allowed-writes'
|
|
5756
|
+
? 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES'
|
|
5757
|
+
: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
5758
|
+
? 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
|
|
5759
|
+
: planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
5760
|
+
? 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
|
|
5761
|
+
: 'NON_EXECUTABLE_NPM_CHECK_OUTSIDE_ALLOWED_WRITES',
|
|
4409
5762
|
expectedRevision: task.revision,
|
|
4410
5763
|
requiredActor,
|
|
4411
5764
|
evidence: planIntegrityAssessment.evidence,
|
|
4412
|
-
boundedEffect: '
|
|
5765
|
+
boundedEffect: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
5766
|
+
? 'record-machine-derived-check-support-scope'
|
|
5767
|
+
: planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
5768
|
+
? 'record-machine-derived-component-owner-scope'
|
|
5769
|
+
: 'record-replan-required-decision',
|
|
4413
5770
|
preservesWorktree: true,
|
|
4414
5771
|
},
|
|
4415
5772
|
}
|
|
@@ -4542,7 +5899,48 @@ export class WorkflowService {
|
|
|
4542
5899
|
};
|
|
4543
5900
|
break;
|
|
4544
5901
|
case 'replan-required':
|
|
4545
|
-
guardedCorrectiveOverlay =
|
|
5902
|
+
guardedCorrectiveOverlay = guardedCorrectivePosture.correctiveDecision
|
|
5903
|
+
? correctiveReplanOverlay(guardedCorrectivePosture)
|
|
5904
|
+
: (() => {
|
|
5905
|
+
const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
5906
|
+
const rollingC1Posture = readTaskC1Posture(this.store, task);
|
|
5907
|
+
const blockers = [
|
|
5908
|
+
...(!['needs_fix', 'ready'].includes(task.status)
|
|
5909
|
+
? [`Task must be needs_fix or context-refreshed ready, not ${task.status}.`]
|
|
5910
|
+
: []),
|
|
5911
|
+
...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may remain in progress.'] : []),
|
|
5912
|
+
...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
|
|
5913
|
+
? [`${guardedCorrectivePosture.stepId} must remain failed.`]
|
|
5914
|
+
: []),
|
|
5915
|
+
...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
5916
|
+
...(rollingC1Posture.state !== 'none'
|
|
5917
|
+
? ['No C1 claim may remain after the root-cause failure pause.']
|
|
5918
|
+
: []),
|
|
5919
|
+
];
|
|
5920
|
+
return blockers.length > 0
|
|
5921
|
+
? {
|
|
5922
|
+
action: 'doctor',
|
|
5923
|
+
blockedAction: 'task plan-set',
|
|
5924
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5925
|
+
rollingWave: {
|
|
5926
|
+
state: 'root-cause-replan-blocked',
|
|
5927
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5928
|
+
humanApprovalRequired: false,
|
|
5929
|
+
blockers,
|
|
5930
|
+
},
|
|
5931
|
+
}
|
|
5932
|
+
: {
|
|
5933
|
+
action: 'task plan-set',
|
|
5934
|
+
blockedAction: 'task run',
|
|
5935
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5936
|
+
rollingWave: {
|
|
5937
|
+
state: 'root-cause-replan-required',
|
|
5938
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5939
|
+
humanApprovalRequired: false,
|
|
5940
|
+
boundary: 'Task objective, requirements, acceptance, and security semantics remain unchanged.',
|
|
5941
|
+
},
|
|
5942
|
+
};
|
|
5943
|
+
})();
|
|
4546
5944
|
break;
|
|
4547
5945
|
case 'split-required':
|
|
4548
5946
|
{
|
|
@@ -4700,6 +6098,11 @@ export class WorkflowService {
|
|
|
4700
6098
|
externalSealedReview: {
|
|
4701
6099
|
packetCommand: 'task step-review-packet',
|
|
4702
6100
|
recordCommand: 'task step-review-record',
|
|
6101
|
+
reviewMode: 'ordinary',
|
|
6102
|
+
allowedReviewModes: ['ordinary', 'security'],
|
|
6103
|
+
packetReviewModeOption: '--review-mode',
|
|
6104
|
+
implicitSecurityScan: false,
|
|
6105
|
+
reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${strictStepReviewStep.id}:${strictStepReviewStep.evidence.commitSha}:ordinary`),
|
|
4703
6106
|
reviewerMustUseSeparateChat: true,
|
|
4704
6107
|
repositorySealRequired: true,
|
|
4705
6108
|
},
|
|
@@ -4715,6 +6118,11 @@ export class WorkflowService {
|
|
|
4715
6118
|
externalSealedReview: {
|
|
4716
6119
|
packetCommand: 'task review-packet',
|
|
4717
6120
|
recordCommand: 'task review-sealed-record',
|
|
6121
|
+
reviewMode: 'ordinary',
|
|
6122
|
+
allowedReviewModes: ['ordinary', 'security'],
|
|
6123
|
+
packetReviewModeOption: '--review-mode',
|
|
6124
|
+
implicitSecurityScan: false,
|
|
6125
|
+
reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${headCommit(repositoryRoot)}:ordinary`),
|
|
4718
6126
|
reviewerMustUseSeparateChat: true,
|
|
4719
6127
|
repositorySealRequired: true,
|
|
4720
6128
|
},
|
|
@@ -4733,7 +6141,11 @@ export class WorkflowService {
|
|
|
4733
6141
|
: {}),
|
|
4734
6142
|
...guardedCorrectiveOverlay,
|
|
4735
6143
|
...downstreamProofOverlay,
|
|
4736
|
-
...(failedStepReviewRouting?.route === 'replan'
|
|
6144
|
+
...(failedStepReviewRouting?.route === 'replan'
|
|
6145
|
+
&& next.action === 'task run'
|
|
6146
|
+
&& (!failedStep
|
|
6147
|
+
|| deriveRollingCauseDecision(failedStepRemediations) === null
|
|
6148
|
+
|| failedStepRemediations.at(-1)?.causeBindings?.length === 0)
|
|
4737
6149
|
? {
|
|
4738
6150
|
action: 'task plan-set',
|
|
4739
6151
|
blockedAction: 'task run',
|
|
@@ -4770,7 +6182,44 @@ export class WorkflowService {
|
|
|
4770
6182
|
},
|
|
4771
6183
|
}
|
|
4772
6184
|
: {}),
|
|
6185
|
+
...(planIntegrityDirtyCarryover?.applicable
|
|
6186
|
+
? planIntegrityDirtyCarryover.eligible
|
|
6187
|
+
? {
|
|
6188
|
+
planIntegrityDirtyCarryover: {
|
|
6189
|
+
state: 'validated',
|
|
6190
|
+
stepId: nextStepId,
|
|
6191
|
+
dependencyRecoveryCommit: planIntegrityDirtyCarryover.dependencyRecoveryCommit,
|
|
6192
|
+
dirtyFiles: planIntegrityDirtyCarryover.compatibility?.dirtyFiles ?? [],
|
|
6193
|
+
dirtyWorktreeHash: planIntegrityDirtyCarryover.compatibility?.dirtyWorktreeHash ?? null,
|
|
6194
|
+
scopeExtensions: planIntegrityDirtyCarryover.compatibility?.scopeExtensions ?? [],
|
|
6195
|
+
meaning: 'The current Step may adopt exactly these hash-bound check-support bytes on task run.',
|
|
6196
|
+
},
|
|
6197
|
+
}
|
|
6198
|
+
: {
|
|
6199
|
+
action: 'doctor',
|
|
6200
|
+
blockedAction: 'task run',
|
|
6201
|
+
planIntegrityDirtyCarryover: {
|
|
6202
|
+
state: 'blocked',
|
|
6203
|
+
stepId: nextStepId,
|
|
6204
|
+
blockers: planIntegrityDirtyCarryover.blockers,
|
|
6205
|
+
},
|
|
6206
|
+
}
|
|
6207
|
+
: {}),
|
|
4773
6208
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6209
|
+
...(correctiveDerivedAuthority && correctiveDerivedActor
|
|
6210
|
+
? {
|
|
6211
|
+
correctiveDerivedApproval: {
|
|
6212
|
+
action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
|
|
6213
|
+
actor: correctiveDerivedActor,
|
|
6214
|
+
humanApprovalRequired: false,
|
|
6215
|
+
milestoneId: correctiveDerivedAuthority.milestone.id,
|
|
6216
|
+
milestoneRevision: correctiveDerivedAuthority.milestone.revision,
|
|
6217
|
+
milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
|
|
6218
|
+
planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
|
|
6219
|
+
authority: 'current-corrective-derived-milestone-execution-authorization',
|
|
6220
|
+
},
|
|
6221
|
+
}
|
|
6222
|
+
: {}),
|
|
4774
6223
|
};
|
|
4775
6224
|
if (handoffPosture.state === 'pending') {
|
|
4776
6225
|
return this.withTaskNavigationContracts(task, {
|
|
@@ -4829,7 +6278,20 @@ export class WorkflowService {
|
|
|
4829
6278
|
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
4830
6279
|
}
|
|
4831
6280
|
taskHasTerminalCorrectiveDecision(task) {
|
|
4832
|
-
|
|
6281
|
+
try {
|
|
6282
|
+
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
6283
|
+
}
|
|
6284
|
+
catch (error) {
|
|
6285
|
+
if (error instanceof WorkflowError
|
|
6286
|
+
&& error.code === 'TRANSITION_BLOCKED'
|
|
6287
|
+
&& (error.details.decision === 'stop-escalate' || error.details.decision === 'split-required')) {
|
|
6288
|
+
// A blocking Plan Risk Audit is not a guarded-Step terminal decision, but it
|
|
6289
|
+
// equally cannot own an atomic delegated context refresh. Treat it as
|
|
6290
|
+
// ineligible here so standalone Knowledge reconciliation remains executable.
|
|
6291
|
+
return true;
|
|
6292
|
+
}
|
|
6293
|
+
throw error;
|
|
6294
|
+
}
|
|
4833
6295
|
}
|
|
4834
6296
|
taskTerminalCorrectivePosture(task) {
|
|
4835
6297
|
if (!task.planHash)
|
|
@@ -4992,6 +6454,10 @@ export class WorkflowService {
|
|
|
4992
6454
|
statePlanHash: task.planHash,
|
|
4993
6455
|
});
|
|
4994
6456
|
}
|
|
6457
|
+
if (authorization.authorizationMode === 'corrective-derived'
|
|
6458
|
+
&& !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
|
|
6459
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective-derived Task authorization is no longer backed by the current Milestone authority.', { taskId: task.id, milestoneId: task.milestoneId, actor: authorization.actor });
|
|
6460
|
+
}
|
|
4995
6461
|
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4996
6462
|
if (!mechanicalFeasibility)
|
|
4997
6463
|
return authorization;
|
|
@@ -5063,7 +6529,218 @@ export class WorkflowService {
|
|
|
5063
6529
|
},
|
|
5064
6530
|
};
|
|
5065
6531
|
}
|
|
5066
|
-
|
|
6532
|
+
carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
|
|
6533
|
+
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
6534
|
+
let dependencyCommitCount = 0;
|
|
6535
|
+
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
6536
|
+
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
6537
|
+
if (!dependencyCommit)
|
|
6538
|
+
return null;
|
|
6539
|
+
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
6540
|
+
dependencyCommitCount += 1;
|
|
6541
|
+
}
|
|
6542
|
+
if (dependencyCommitCount === 0)
|
|
6543
|
+
return null;
|
|
6544
|
+
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
6545
|
+
const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
|
|
6546
|
+
const compatibility = record.planIntegrity;
|
|
6547
|
+
if (!compatibility
|
|
6548
|
+
|| record.commitSha !== boundaryCommit
|
|
6549
|
+
|| !task.systemCommits.includes(record.commitSha)
|
|
6550
|
+
|| compatibility.planHash !== task.planHash
|
|
6551
|
+
|| canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
|
|
6552
|
+
|| compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
|
|
6553
|
+
return false;
|
|
6554
|
+
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
|
|
6555
|
+
if (!step || step.status !== 'failed')
|
|
6556
|
+
return false;
|
|
6557
|
+
const remediationEventIds = readRemediationEvents(this.store, task)
|
|
6558
|
+
.filter((event) => event.stepId === compatibility.stepId)
|
|
6559
|
+
.map((event) => event.eventId);
|
|
6560
|
+
if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
|
|
6561
|
+
return false;
|
|
6562
|
+
return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
|
|
6563
|
+
&& event.stepId === compatibility.stepId
|
|
6564
|
+
&& event.decision === 'replan-required')
|
|
6565
|
+
&& readTaskC1Posture(this.store, task).state === 'none';
|
|
6566
|
+
});
|
|
6567
|
+
return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
|
|
6568
|
+
}
|
|
6569
|
+
postRebindCheckSupportCompatibility(snapshot, task) {
|
|
6570
|
+
if (task.status !== 'awaiting_execution_authorization'
|
|
6571
|
+
|| !task.planHash
|
|
6572
|
+
|| task.steps.some((step) => step.status === 'in_progress')
|
|
6573
|
+
|| readTaskC1Posture(this.store, task).state !== 'none')
|
|
6574
|
+
return null;
|
|
6575
|
+
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
6576
|
+
const candidates = readCorrectiveDecisionEvents(this.store, task).flatMap((event) => {
|
|
6577
|
+
const evidence = event.planIntegrityEvidence;
|
|
6578
|
+
if (event.planHash === task.planHash
|
|
6579
|
+
|| event.decision !== 'continue-fix'
|
|
6580
|
+
|| event.recoveryBasis !== 'plan-integrity'
|
|
6581
|
+
|| evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
|
|
6582
|
+
|| !task.systemCommits.includes(evidence.gitHead)
|
|
6583
|
+
|| canonicalJsonStringify([...evidence.changedFiles].sort()) !== canonicalJsonStringify(dirtyFiles)) {
|
|
6584
|
+
return [];
|
|
6585
|
+
}
|
|
6586
|
+
const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
|
|
6587
|
+
if (!step
|
|
6588
|
+
|| step.status !== 'planned'
|
|
6589
|
+
|| evidence.scopeExtensions.some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
|
|
6590
|
+
|| !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath)))
|
|
6591
|
+
return [];
|
|
6592
|
+
return [{
|
|
6593
|
+
stepId: step.id,
|
|
6594
|
+
planHash: task.planHash,
|
|
6595
|
+
conflictHash: evidence.conflictHash,
|
|
6596
|
+
dirtyFiles: [...dirtyFiles],
|
|
6597
|
+
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, dirtyFiles),
|
|
6598
|
+
remediationEventIds: evidence.remediationEvents.map((binding) => binding.eventId),
|
|
6599
|
+
mode: 'post-rebind-scope-adoption',
|
|
6600
|
+
scopeExtensions: [...evidence.scopeExtensions],
|
|
6601
|
+
}];
|
|
6602
|
+
});
|
|
6603
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
6604
|
+
}
|
|
6605
|
+
carriedPostRebindCheckSupportDirtyCarryover(snapshot, task) {
|
|
6606
|
+
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
6607
|
+
let dependencyCommitCount = 0;
|
|
6608
|
+
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
6609
|
+
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
6610
|
+
if (!dependencyCommit)
|
|
6611
|
+
return null;
|
|
6612
|
+
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
6613
|
+
dependencyCommitCount += 1;
|
|
6614
|
+
}
|
|
6615
|
+
if (dependencyCommitCount === 0)
|
|
6616
|
+
return null;
|
|
6617
|
+
const assessment = this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, boundaryCommit);
|
|
6618
|
+
if (!assessment.eligible || !assessment.compatibility || !task.planHash)
|
|
6619
|
+
return null;
|
|
6620
|
+
return {
|
|
6621
|
+
...assessment.compatibility,
|
|
6622
|
+
planHash: task.planHash,
|
|
6623
|
+
mode: 'post-rebind-dirty-carryover',
|
|
6624
|
+
};
|
|
6625
|
+
}
|
|
6626
|
+
assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, stepId, historyBoundary = headCommit(repositoryRoot)) {
|
|
6627
|
+
const relevantRecords = [...(task.dependencyProvenanceRecoveries ?? [])]
|
|
6628
|
+
.reverse()
|
|
6629
|
+
.filter((record) => {
|
|
6630
|
+
const compatibility = record.planIntegrity;
|
|
6631
|
+
return compatibility
|
|
6632
|
+
&& ['post-rebind-scope-adoption', 'post-rebind-dirty-carryover'].includes(compatibility.mode ?? '')
|
|
6633
|
+
&& (!stepId || compatibility.stepId === stepId);
|
|
6634
|
+
});
|
|
6635
|
+
if (relevantRecords.length === 0) {
|
|
6636
|
+
return {
|
|
6637
|
+
applicable: false,
|
|
6638
|
+
eligible: false,
|
|
6639
|
+
compatibility: null,
|
|
6640
|
+
dependencyRecoveryCommit: null,
|
|
6641
|
+
blockers: [],
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
const blockers = [];
|
|
6645
|
+
const record = relevantRecords[0];
|
|
6646
|
+
const compatibility = record.planIntegrity;
|
|
6647
|
+
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId) ?? null;
|
|
6648
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
6649
|
+
let dirtyWorktreeHash = null;
|
|
6650
|
+
try {
|
|
6651
|
+
dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
6652
|
+
}
|
|
6653
|
+
catch (error) {
|
|
6654
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
6655
|
+
}
|
|
6656
|
+
if (record.commitSha !== historyBoundary || !task.systemCommits.includes(record.commitSha)) {
|
|
6657
|
+
blockers.push('The latest check-support compatibility record must bind the exact registered history boundary.');
|
|
6658
|
+
}
|
|
6659
|
+
if (task.workspaceOwner !== 'local') {
|
|
6660
|
+
blockers.push('Check-support dirty carryover requires the recorded local Task workspace.');
|
|
6661
|
+
}
|
|
6662
|
+
if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
|
|
6663
|
+
blockers.push('Current branch must equal the recorded Task branch.');
|
|
6664
|
+
}
|
|
6665
|
+
if (!['ready', 'in_progress'].includes(task.status)) {
|
|
6666
|
+
blockers.push('The Task must retain an execution-authorized posture before dirty carryover.');
|
|
6667
|
+
}
|
|
6668
|
+
if (!task.planHash) {
|
|
6669
|
+
blockers.push('The Task must retain a current Plan hash.');
|
|
6670
|
+
}
|
|
6671
|
+
if (!step || step.status !== 'planned' || step.evidence !== null) {
|
|
6672
|
+
blockers.push(`The bound Step ${compatibility.stepId} must remain planned with no evidence.`);
|
|
6673
|
+
}
|
|
6674
|
+
if (task.steps.some((candidate) => candidate.status === 'in_progress')) {
|
|
6675
|
+
blockers.push('No Step may already be in progress before dirty carryover.');
|
|
6676
|
+
}
|
|
6677
|
+
if (canonicalJsonStringify(dirtyFiles)
|
|
6678
|
+
!== canonicalJsonStringify([...compatibility.dirtyFiles].sort())) {
|
|
6679
|
+
blockers.push('Dirty files changed after the registered check-support compatibility boundary.');
|
|
6680
|
+
}
|
|
6681
|
+
if (dirtyWorktreeHash !== compatibility.dirtyWorktreeHash) {
|
|
6682
|
+
blockers.push('Dirty file content changed after the registered check-support compatibility boundary.');
|
|
6683
|
+
}
|
|
6684
|
+
if (!compatibility.scopeExtensions?.length) {
|
|
6685
|
+
blockers.push('The compatibility record must retain at least one machine-derived scope extension.');
|
|
6686
|
+
}
|
|
6687
|
+
if (step) {
|
|
6688
|
+
if (dirtyFiles.some((dirtyPath) => !pathAllowed(dirtyPath, step.allowedWrites))) {
|
|
6689
|
+
blockers.push('The current Step must explicitly own every preserved dirty file.');
|
|
6690
|
+
}
|
|
6691
|
+
if ((compatibility.scopeExtensions ?? []).some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
|
|
6692
|
+
|| !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
|
|
6693
|
+
blockers.push('The current Step must explicitly own every check-support scope extension as allowedWrites and expectedOutputs.');
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
let reboundPlanHash = compatibility.planHash;
|
|
6697
|
+
for (const rebind of task.knowledgeRebinds) {
|
|
6698
|
+
if (rebind.previousPlanHash === reboundPlanHash)
|
|
6699
|
+
reboundPlanHash = rebind.reboundPlanHash;
|
|
6700
|
+
}
|
|
6701
|
+
if (!task.planHash || reboundPlanHash !== task.planHash) {
|
|
6702
|
+
blockers.push('The current Plan must equal the compatibility Plan or its exact Knowledge-rebind descendant.');
|
|
6703
|
+
}
|
|
6704
|
+
const matchingDecisions = readCorrectiveDecisionEvents(this.store, task).filter((event) => {
|
|
6705
|
+
const evidence = event.planIntegrityEvidence;
|
|
6706
|
+
return event.stepId === compatibility.stepId
|
|
6707
|
+
&& event.decision === 'continue-fix'
|
|
6708
|
+
&& event.recoveryBasis === 'plan-integrity'
|
|
6709
|
+
&& evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
6710
|
+
&& evidence.conflictHash === compatibility.conflictHash
|
|
6711
|
+
&& canonicalJsonStringify([...evidence.changedFiles].sort())
|
|
6712
|
+
=== canonicalJsonStringify([...compatibility.dirtyFiles].sort())
|
|
6713
|
+
&& canonicalJsonStringify(evidence.remediationEvents.map((binding) => binding.eventId))
|
|
6714
|
+
=== canonicalJsonStringify(compatibility.remediationEventIds)
|
|
6715
|
+
&& canonicalJsonStringify([...evidence.scopeExtensions].sort())
|
|
6716
|
+
=== canonicalJsonStringify([...(compatibility.scopeExtensions ?? [])].sort());
|
|
6717
|
+
});
|
|
6718
|
+
if (matchingDecisions.length !== 1) {
|
|
6719
|
+
blockers.push('Exactly one validated historical check-support decision must bind the preserved dirty set and scope.');
|
|
6720
|
+
}
|
|
6721
|
+
try {
|
|
6722
|
+
validateTaskHistory(repositoryRoot, task, historyBoundary);
|
|
6723
|
+
}
|
|
6724
|
+
catch (error) {
|
|
6725
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
6726
|
+
}
|
|
6727
|
+
return {
|
|
6728
|
+
applicable: true,
|
|
6729
|
+
eligible: blockers.length === 0,
|
|
6730
|
+
compatibility: blockers.length === 0
|
|
6731
|
+
? {
|
|
6732
|
+
...structuredClone(compatibility),
|
|
6733
|
+
planHash: task.planHash,
|
|
6734
|
+
dirtyFiles,
|
|
6735
|
+
dirtyWorktreeHash: dirtyWorktreeHash,
|
|
6736
|
+
mode: 'post-rebind-dirty-carryover',
|
|
6737
|
+
}
|
|
6738
|
+
: null,
|
|
6739
|
+
dependencyRecoveryCommit: record.commitSha,
|
|
6740
|
+
blockers,
|
|
6741
|
+
};
|
|
6742
|
+
}
|
|
6743
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
|
|
5067
6744
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
5068
6745
|
const now = this.now().getTime();
|
|
5069
6746
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -5083,7 +6760,20 @@ export class WorkflowService {
|
|
|
5083
6760
|
currentBranch: versions.currentBranch,
|
|
5084
6761
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
5085
6762
|
},
|
|
5086
|
-
}, activeDownstreamProof, downstreamProofReplan);
|
|
6763
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
|
|
6764
|
+
}
|
|
6765
|
+
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
6766
|
+
const now = this.now().getTime();
|
|
6767
|
+
return inspectHistoricalStepProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
6768
|
+
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
6769
|
+
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
6770
|
+
pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
|
|
6771
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
6772
|
+
+ snapshot.observation.pendingTaskTransactions.length,
|
|
6773
|
+
corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
|
|
6774
|
+
coreOperationCount: snapshot.observation.coreTaskOperations.length,
|
|
6775
|
+
verifiedHistoricalStepBindings: verifiedHistoricalStepReviewBindings(this.store, task),
|
|
6776
|
+
}, options);
|
|
5087
6777
|
}
|
|
5088
6778
|
withTaskNavigationContracts(task, next) {
|
|
5089
6779
|
const action = typeof next.action === 'string' ? next.action : null;
|
|
@@ -5092,10 +6782,9 @@ export class WorkflowService {
|
|
|
5092
6782
|
const policy = gitMutationPolicyForAction(task, action);
|
|
5093
6783
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
5094
6784
|
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
5095
|
-
const
|
|
5096
|
-
const
|
|
5097
|
-
|
|
5098
|
-
: null;
|
|
6785
|
+
const activeLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
6786
|
+
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture, activeLease !== null);
|
|
6787
|
+
const activeLeaseUsable = activeLease ? !toPublicWriterLease(activeLease, this.now()).stale : false;
|
|
5099
6788
|
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
5100
6789
|
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
5101
6790
|
? {
|
|
@@ -5107,9 +6796,24 @@ export class WorkflowService {
|
|
|
5107
6796
|
acquiresLeaseWhenAbsent: true,
|
|
5108
6797
|
}
|
|
5109
6798
|
: null;
|
|
6799
|
+
const credentialBlocksAction = Boolean(writerCredentialRecovery
|
|
6800
|
+
&& writerTokenContract
|
|
6801
|
+
&& (activeLease === null || !activeLeaseUsable));
|
|
5110
6802
|
return {
|
|
5111
6803
|
...next,
|
|
5112
6804
|
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
6805
|
+
...(credentialBlocksAction
|
|
6806
|
+
? {
|
|
6807
|
+
action: 'task writer-credential-replace',
|
|
6808
|
+
blockedAction: action,
|
|
6809
|
+
continuation: {
|
|
6810
|
+
kind: 'failed-step-pause',
|
|
6811
|
+
preservesTask: true,
|
|
6812
|
+
preservesWorktree: true,
|
|
6813
|
+
resumesWithFreshNext: true,
|
|
6814
|
+
},
|
|
6815
|
+
}
|
|
6816
|
+
: {}),
|
|
5113
6817
|
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
5114
6818
|
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
5115
6819
|
};
|
|
@@ -5521,16 +7225,20 @@ function assertTaskBranch(repositoryRoot, task) {
|
|
|
5521
7225
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
|
|
5522
7226
|
}
|
|
5523
7227
|
}
|
|
5524
|
-
function preserveCompletedStep(task, next) {
|
|
5525
|
-
const previous = task.steps.find((candidate) => candidate.id === next.id && candidate.status === 'completed');
|
|
5526
|
-
if (!previous)
|
|
5527
|
-
return null;
|
|
5528
|
-
const comparable = ({ status: _status, evidence: _evidence, ...definition }) => definition;
|
|
5529
|
-
return JSON.stringify(comparable(previous)) === JSON.stringify(next) ? previous : null;
|
|
5530
|
-
}
|
|
5531
7228
|
function stripStepRuntime(step) {
|
|
5532
|
-
|
|
5533
|
-
|
|
7229
|
+
return normalizePlanStep(step);
|
|
7230
|
+
}
|
|
7231
|
+
function normalizePlanStep(step) {
|
|
7232
|
+
return normalizePlanStepDefinition(step);
|
|
7233
|
+
}
|
|
7234
|
+
function parseWorkflowBetaVersion(value) {
|
|
7235
|
+
const match = value?.match(/^2[.]0[.]0-beta[.]([0-9]+)(?:[.]([0-9]+))?$/);
|
|
7236
|
+
if (!match)
|
|
7237
|
+
return null;
|
|
7238
|
+
return {
|
|
7239
|
+
train: Number(match[1]),
|
|
7240
|
+
patch: match[2] === undefined ? null : Number(match[2]),
|
|
7241
|
+
};
|
|
5534
7242
|
}
|
|
5535
7243
|
function validateReviewInput(review) {
|
|
5536
7244
|
if (!review.reviewer.trim())
|
|
@@ -5904,7 +7612,32 @@ function gitMutationPolicyForAction(task, action) {
|
|
|
5904
7612
|
return null;
|
|
5905
7613
|
}
|
|
5906
7614
|
}
|
|
5907
|
-
function
|
|
7615
|
+
function taskWithEffectiveStepAllowedWrites(store, task, stepId) {
|
|
7616
|
+
const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
|
|
7617
|
+
return {
|
|
7618
|
+
...task,
|
|
7619
|
+
steps: task.steps.map((step) => step.id === stepId ? { ...step, allowedWrites } : step),
|
|
7620
|
+
};
|
|
7621
|
+
}
|
|
7622
|
+
function reviewScopeAugmentations(store, task, stepId) {
|
|
7623
|
+
const step = requireStep(task, stepId);
|
|
7624
|
+
const paths = effectiveStepAllowedWrites(store, task, stepId)
|
|
7625
|
+
.filter((candidate) => !pathAllowed(candidate, step.allowedWrites));
|
|
7626
|
+
return paths.length === 0 ? [] : [{
|
|
7627
|
+
stepId,
|
|
7628
|
+
paths,
|
|
7629
|
+
authority: 'machine-derived-check-support',
|
|
7630
|
+
}];
|
|
7631
|
+
}
|
|
7632
|
+
function reviewCauseBindings(event) {
|
|
7633
|
+
return (event.remediationCauses ?? []).map((cause) => ({
|
|
7634
|
+
authority: 'external-reviewer',
|
|
7635
|
+
causeId: cause.causeId,
|
|
7636
|
+
causeFingerprint: cause.causeFingerprint,
|
|
7637
|
+
planConflict: cause.planConflict,
|
|
7638
|
+
}));
|
|
7639
|
+
}
|
|
7640
|
+
function writerTokenContractForAction(action, claimedC1Posture = false, existingLease = false) {
|
|
5908
7641
|
const alwaysLeaseBound = [
|
|
5909
7642
|
'task step-complete',
|
|
5910
7643
|
'task downstream-proof-recover',
|
|
@@ -5930,16 +7663,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
5930
7663
|
'task handback',
|
|
5931
7664
|
'task handback-create',
|
|
5932
7665
|
];
|
|
5933
|
-
if (!alwaysLeaseBound.includes(action)
|
|
7666
|
+
if (!alwaysLeaseBound.includes(action)
|
|
7667
|
+
&& !(claimedC1Posture && claimedC1LeaseBound.includes(action))
|
|
7668
|
+
&& !(existingLease && ['task run', 'task step-review', 'task step-review-record'].includes(action))) {
|
|
5934
7669
|
return null;
|
|
5935
7670
|
}
|
|
5936
7671
|
return {
|
|
5937
7672
|
required: true,
|
|
5938
|
-
option: '--writer-
|
|
5939
|
-
source: '
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
7673
|
+
option: '--writer-credential-ref',
|
|
7674
|
+
source: 'local-credential-vault',
|
|
7675
|
+
transport: 'local-vault-v1',
|
|
7676
|
+
secretInArgv: false,
|
|
7677
|
+
secretInOutput: false,
|
|
7678
|
+
renewalDisposition: 'reuse-reference',
|
|
5943
7679
|
};
|
|
5944
7680
|
}
|
|
5945
7681
|
function observationPreflightBlockers(observation) {
|