codex-workflow-v2 2.0.0-beta.12.9 → 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 +58 -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.js +34 -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 +216 -258
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +166 -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 +221 -34
- 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/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 +18 -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 +65 -4
- package/dist/src/workflow.js +1704 -155
- 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 +181 -22
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +148 -26
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +326 -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 +83 -9
- package/docs/updating-existing-project.md +205 -0
- package/docs/validation-report.md +156 -99
- package/package.json +12 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +42 -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 +61 -1
- package/schemas/transition-definition.schema.json +8 -0
package/dist/src/workflow.js
CHANGED
|
@@ -2,23 +2,26 @@ 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
6
|
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
7
7
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
8
8
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
9
9
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
10
|
+
import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
|
|
10
11
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
11
12
|
import { WorkflowError } from './errors.js';
|
|
13
|
+
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
12
14
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
13
15
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
14
16
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
15
17
|
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
16
18
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
17
19
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
18
|
-
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';
|
|
19
21
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
22
|
+
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
20
23
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
21
|
-
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';
|
|
22
25
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
23
26
|
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
|
|
24
27
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
@@ -28,7 +31,7 @@ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
|
28
31
|
import { ensureDirectory } from './fs-utils.js';
|
|
29
32
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
30
33
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
31
|
-
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
34
|
+
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, prepareReviewerRuntimePreflight, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
32
35
|
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
33
36
|
import { FileStateStore } from './state/store.js';
|
|
34
37
|
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
@@ -601,6 +604,353 @@ export class WorkflowService {
|
|
|
601
604
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
602
605
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
603
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
|
+
}
|
|
604
954
|
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
605
955
|
const { identity, locks } = this.#mutationContext(repository);
|
|
606
956
|
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
@@ -1266,11 +1616,14 @@ export class WorkflowService {
|
|
|
1266
1616
|
|| (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
|
|
1267
1617
|
const downstreamReplanRecovered = readDownstreamProofReplanRecoveries(this.store, task)
|
|
1268
1618
|
.some((event) => event.resultTaskRevision === task.revision && event.reboundPlanHash === task.planHash);
|
|
1269
|
-
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)
|
|
1270
1623
|
&& !downstreamReplanRecovered
|
|
1271
1624
|
? readCurrentPlanRiskAudit(this.store, task)
|
|
1272
1625
|
: null;
|
|
1273
|
-
const classifiedCorrectivePosture =
|
|
1626
|
+
const classifiedCorrectivePosture = currentPlanRiskAudit
|
|
1274
1627
|
? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
|
|
1275
1628
|
: null;
|
|
1276
1629
|
const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
|
|
@@ -1300,7 +1653,10 @@ export class WorkflowService {
|
|
|
1300
1653
|
: 'Use task corrective-replan-prepare, confirm, and execute.',
|
|
1301
1654
|
});
|
|
1302
1655
|
}
|
|
1303
|
-
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)) {
|
|
1304
1660
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
1305
1661
|
}
|
|
1306
1662
|
if (currentHardBlockingDecisions.length > 0) {
|
|
@@ -1370,7 +1726,8 @@ export class WorkflowService {
|
|
|
1370
1726
|
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
|
|
1371
1727
|
}
|
|
1372
1728
|
}
|
|
1373
|
-
if (task.planHash && task.status === 'needs_fix'
|
|
1729
|
+
if (task.planHash && (task.status === 'needs_fix'
|
|
1730
|
+
|| correctiveReplanPosture?.posture === 'replan-required')) {
|
|
1374
1731
|
if (task.planObjective !== null && plan.objective !== task.planObjective) {
|
|
1375
1732
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
|
|
1376
1733
|
taskId: task.id,
|
|
@@ -1392,14 +1749,16 @@ export class WorkflowService {
|
|
|
1392
1749
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
1393
1750
|
? { ...authorization, decision: 'superseded' }
|
|
1394
1751
|
: authorization);
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
+
});
|
|
1403
1762
|
const invalidatedStepCommits = [...new Set([
|
|
1404
1763
|
...(task.invalidatedStepCommits ?? []),
|
|
1405
1764
|
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
@@ -1410,15 +1769,62 @@ export class WorkflowService {
|
|
|
1410
1769
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
1411
1770
|
}
|
|
1412
1771
|
if (correctiveReplanPosture?.posture === 'replan-required') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
});
|
|
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
|
+
}
|
|
1422
1828
|
}
|
|
1423
1829
|
}
|
|
1424
1830
|
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
@@ -1441,6 +1847,8 @@ export class WorkflowService {
|
|
|
1441
1847
|
knowledgeMapRevision: plan.knowledgeMapRevision,
|
|
1442
1848
|
knowledgeMapHash: plan.knowledgeMapHash,
|
|
1443
1849
|
graphUse: plan.graphUse,
|
|
1850
|
+
productionEdges: structuredClone(plan.productionEdges ?? []),
|
|
1851
|
+
requiresProductionEdges: plan.requiresProductionEdges === true,
|
|
1444
1852
|
steps: plannedSteps,
|
|
1445
1853
|
invalidatedStepCommits,
|
|
1446
1854
|
authorizations,
|
|
@@ -1678,7 +2086,12 @@ export class WorkflowService {
|
|
|
1678
2086
|
requiredAction: 'task plan-set',
|
|
1679
2087
|
});
|
|
1680
2088
|
}
|
|
1681
|
-
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);
|
|
1682
2095
|
const event = {
|
|
1683
2096
|
kind: 'execution',
|
|
1684
2097
|
decision: 'approved',
|
|
@@ -1690,7 +2103,11 @@ export class WorkflowService {
|
|
|
1690
2103
|
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1691
2104
|
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1692
2105
|
reason,
|
|
1693
|
-
authorizationMode:
|
|
2106
|
+
authorizationMode: correctiveAuthority
|
|
2107
|
+
? 'corrective-derived'
|
|
2108
|
+
: delegation
|
|
2109
|
+
? 'delegated'
|
|
2110
|
+
: 'human',
|
|
1694
2111
|
...(delegation ? { delegation } : {}),
|
|
1695
2112
|
mechanicalFeasibility,
|
|
1696
2113
|
};
|
|
@@ -1875,7 +2292,7 @@ export class WorkflowService {
|
|
|
1875
2292
|
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1876
2293
|
const action = String(navigation.action);
|
|
1877
2294
|
const currentLease = context.locks.inspect(taskId);
|
|
1878
|
-
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
2295
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
1879
2296
|
const requiredActor = writerTokenContract
|
|
1880
2297
|
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1881
2298
|
: null;
|
|
@@ -1889,9 +2306,7 @@ export class WorkflowService {
|
|
|
1889
2306
|
nextAction: action,
|
|
1890
2307
|
});
|
|
1891
2308
|
}
|
|
1892
|
-
const lease =
|
|
1893
|
-
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1894
|
-
: context.locks.acquire(taskId, normalizedActor);
|
|
2309
|
+
const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
|
|
1895
2310
|
if (posture.state === 'claimed') {
|
|
1896
2311
|
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1897
2312
|
}
|
|
@@ -2010,6 +2425,7 @@ export class WorkflowService {
|
|
|
2010
2425
|
}
|
|
2011
2426
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2012
2427
|
const step = requireStep(task, stepId);
|
|
2428
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2013
2429
|
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
2014
2430
|
if (!['planned', 'failed'].includes(step.status)) {
|
|
2015
2431
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
@@ -2044,18 +2460,25 @@ export class WorkflowService {
|
|
|
2044
2460
|
const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
|
|
2045
2461
|
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2046
2462
|
: null;
|
|
2463
|
+
const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2464
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2465
|
+
: null;
|
|
2047
2466
|
if (step.status === 'planned'
|
|
2048
2467
|
&& dirty.length > 0
|
|
2049
2468
|
&& !resumesCorrectiveReplan
|
|
2050
|
-
&& !downstreamProofCarryover?.eligible
|
|
2469
|
+
&& !downstreamProofCarryover?.eligible
|
|
2470
|
+
&& !planIntegrityDirtyCarryover?.eligible) {
|
|
2051
2471
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2052
2472
|
changedFiles: dirty,
|
|
2053
2473
|
...(downstreamProofCarryover?.applicable
|
|
2054
2474
|
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2055
2475
|
: {}),
|
|
2476
|
+
...(planIntegrityDirtyCarryover?.applicable
|
|
2477
|
+
? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
|
|
2478
|
+
: {}),
|
|
2056
2479
|
});
|
|
2057
2480
|
}
|
|
2058
|
-
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file,
|
|
2481
|
+
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
2059
2482
|
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
2060
2483
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
2061
2484
|
changedFiles: dirtyOutsideStep,
|
|
@@ -2102,7 +2525,9 @@ export class WorkflowService {
|
|
|
2102
2525
|
return context.locks.acquire(taskId, actor.trim() || 'worker');
|
|
2103
2526
|
})();
|
|
2104
2527
|
}
|
|
2105
|
-
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);
|
|
2106
2531
|
let saved;
|
|
2107
2532
|
try {
|
|
2108
2533
|
saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
@@ -2126,7 +2551,7 @@ export class WorkflowService {
|
|
|
2126
2551
|
path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
|
|
2127
2552
|
],
|
|
2128
2553
|
allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
|
|
2129
|
-
allowedWrites
|
|
2554
|
+
allowedWrites,
|
|
2130
2555
|
commands: step.checks,
|
|
2131
2556
|
forbiddenScope: [
|
|
2132
2557
|
...step.forbiddenScope,
|
|
@@ -2245,6 +2670,7 @@ export class WorkflowService {
|
|
|
2245
2670
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
2246
2671
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2247
2672
|
const step = requireStep(task, stepId);
|
|
2673
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2248
2674
|
if (task.status !== 'in_progress' || step.status !== 'in_progress') {
|
|
2249
2675
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
|
|
2250
2676
|
}
|
|
@@ -2277,18 +2703,114 @@ export class WorkflowService {
|
|
|
2277
2703
|
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
2278
2704
|
const failed = checks.filter((check) => !check.passed);
|
|
2279
2705
|
if (failed.length > 0) {
|
|
2280
|
-
|
|
2281
|
-
|
|
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', {
|
|
2282
2720
|
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
2283
2721
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2284
2722
|
reviewEventHash: null,
|
|
2285
2723
|
reviewerAttestationHash: null,
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
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);
|
|
2290
2812
|
}
|
|
2291
|
-
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);
|
|
2292
2814
|
const evidence = {
|
|
2293
2815
|
recordedAt: this.now().toISOString(),
|
|
2294
2816
|
commitSha: committed.commitSha,
|
|
@@ -2327,7 +2849,7 @@ export class WorkflowService {
|
|
|
2327
2849
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2328
2850
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2329
2851
|
assertExpectedRevision(task, expectedRevision);
|
|
2330
|
-
this.
|
|
2852
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2331
2853
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
2332
2854
|
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2333
2855
|
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
@@ -2398,18 +2920,14 @@ export class WorkflowService {
|
|
|
2398
2920
|
resolution: currentCycle.resolution,
|
|
2399
2921
|
});
|
|
2400
2922
|
}
|
|
2401
|
-
|
|
2402
|
-
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
2403
|
-
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2404
|
-
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
2405
|
-
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
2406
|
-
}, 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);
|
|
2407
2924
|
}
|
|
2408
2925
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
2409
2926
|
}
|
|
2410
2927
|
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2411
2928
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2412
|
-
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));
|
|
2413
2931
|
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2414
2932
|
review: launch.review,
|
|
2415
2933
|
reviewerOutputHash: launch.reviewerOutputHash,
|
|
@@ -2426,16 +2944,11 @@ export class WorkflowService {
|
|
|
2426
2944
|
resolution: recorded.resolution,
|
|
2427
2945
|
});
|
|
2428
2946
|
}
|
|
2429
|
-
|
|
2430
|
-
completionCommit: reviewRecordedEvent.completionCommit,
|
|
2431
|
-
checksHash: reviewRecordedEvent.checksHash,
|
|
2432
|
-
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
2433
|
-
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
2434
|
-
}, 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);
|
|
2435
2948
|
}
|
|
2436
2949
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2437
2950
|
}
|
|
2438
|
-
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2951
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
|
|
2439
2952
|
const identity = resolveRepositoryIdentity(repository);
|
|
2440
2953
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2441
2954
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2455,15 +2968,18 @@ export class WorkflowService {
|
|
|
2455
2968
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2456
2969
|
}
|
|
2457
2970
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2458
|
-
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);
|
|
2459
2972
|
}
|
|
2460
2973
|
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2461
2974
|
const context = this.#mutationContext(repository);
|
|
2462
2975
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2463
2976
|
assertExpectedRevision(task, expectedRevision);
|
|
2464
|
-
this.
|
|
2977
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2465
2978
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2466
|
-
|
|
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);
|
|
2467
2983
|
let review;
|
|
2468
2984
|
try {
|
|
2469
2985
|
review = validateExternalStrictStepReviewInput(input, packet);
|
|
@@ -2498,23 +3014,131 @@ export class WorkflowService {
|
|
|
2498
3014
|
isolationResult: 'verified',
|
|
2499
3015
|
}, this.now());
|
|
2500
3016
|
if (recorded.resolution === 'failed') {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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
|
+
};
|
|
2507
3026
|
}
|
|
2508
3027
|
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2509
3028
|
return {
|
|
2510
3029
|
mode: 'external-sealed',
|
|
3030
|
+
reviewMode: packet.reviewMode,
|
|
2511
3031
|
packetHash: packet.packetHash,
|
|
2512
3032
|
repositorySealHash: packet.repositorySealHash,
|
|
2513
3033
|
reviewerThreadId: input.reviewerThreadId,
|
|
2514
3034
|
task: saved,
|
|
2515
3035
|
};
|
|
2516
3036
|
}
|
|
2517
|
-
|
|
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') {
|
|
2518
3142
|
const identity = resolveRepositoryIdentity(repository);
|
|
2519
3143
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2520
3144
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2531,7 +3155,7 @@ export class WorkflowService {
|
|
|
2531
3155
|
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2532
3156
|
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2533
3157
|
}
|
|
2534
|
-
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);
|
|
2535
3159
|
}
|
|
2536
3160
|
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2537
3161
|
const context = this.#mutationContext(repository);
|
|
@@ -2539,7 +3163,10 @@ export class WorkflowService {
|
|
|
2539
3163
|
assertExpectedRevision(task, expectedRevision);
|
|
2540
3164
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2541
3165
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2542
|
-
|
|
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);
|
|
2543
3170
|
let review;
|
|
2544
3171
|
try {
|
|
2545
3172
|
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
@@ -2553,6 +3180,7 @@ export class WorkflowService {
|
|
|
2553
3180
|
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2554
3181
|
return {
|
|
2555
3182
|
mode: 'external-sealed',
|
|
3183
|
+
reviewMode: packet.reviewMode,
|
|
2556
3184
|
packetHash: packet.packetHash,
|
|
2557
3185
|
repositorySealHash: packet.repositorySealHash,
|
|
2558
3186
|
reviewerThreadId: input.reviewerThreadId,
|
|
@@ -2657,8 +3285,6 @@ export class WorkflowService {
|
|
|
2657
3285
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2658
3286
|
assertExpectedRevision(task, expectedRevision);
|
|
2659
3287
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2660
|
-
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2661
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2662
3288
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2663
3289
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2664
3290
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
@@ -2690,6 +3316,20 @@ export class WorkflowService {
|
|
|
2690
3316
|
blockers: assessment.blockers,
|
|
2691
3317
|
});
|
|
2692
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
|
+
}
|
|
2693
3333
|
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2694
3334
|
return { assessment, event };
|
|
2695
3335
|
}
|
|
@@ -2775,7 +3415,12 @@ export class WorkflowService {
|
|
|
2775
3415
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
2776
3416
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
2777
3417
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
2778
|
-
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);
|
|
2779
3424
|
const event = {
|
|
2780
3425
|
kind: 'final_acceptance',
|
|
2781
3426
|
decision: 'approved',
|
|
@@ -2787,7 +3432,11 @@ export class WorkflowService {
|
|
|
2787
3432
|
evidenceHash,
|
|
2788
3433
|
headCommit: headCommit(identity.repositoryRoot),
|
|
2789
3434
|
reason: '',
|
|
2790
|
-
authorizationMode:
|
|
3435
|
+
authorizationMode: correctiveAuthority
|
|
3436
|
+
? 'corrective-derived'
|
|
3437
|
+
: delegation
|
|
3438
|
+
? 'delegated'
|
|
3439
|
+
: 'human',
|
|
2791
3440
|
...(delegation ? { delegation } : {}),
|
|
2792
3441
|
};
|
|
2793
3442
|
return this.store.writeTask({
|
|
@@ -2890,7 +3539,9 @@ export class WorkflowService {
|
|
|
2890
3539
|
return saved;
|
|
2891
3540
|
}
|
|
2892
3541
|
status(repository) {
|
|
2893
|
-
|
|
3542
|
+
return this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
3543
|
+
}
|
|
3544
|
+
statusFromSnapshot(snapshot) {
|
|
2894
3545
|
return {
|
|
2895
3546
|
projectId: snapshot.identity.projectId,
|
|
2896
3547
|
discoveries: snapshot.discoveries,
|
|
@@ -2904,6 +3555,73 @@ export class WorkflowService {
|
|
|
2904
3555
|
observation: snapshot.observation,
|
|
2905
3556
|
};
|
|
2906
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
|
+
}
|
|
2907
3625
|
recoverMilestoneTransactions(repository) {
|
|
2908
3626
|
const { identity } = this.#mutationContext(repository);
|
|
2909
3627
|
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
@@ -3239,12 +3957,179 @@ export class WorkflowService {
|
|
|
3239
3957
|
dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
|
|
3240
3958
|
requiredTransport: [
|
|
3241
3959
|
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
3242
|
-
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
3243
|
-
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
3960
|
+
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
3961
|
+
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
3962
|
+
],
|
|
3963
|
+
blockers,
|
|
3964
|
+
};
|
|
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.',
|
|
3244
4061
|
],
|
|
3245
4062
|
blockers,
|
|
3246
4063
|
};
|
|
3247
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
|
+
}
|
|
3248
4133
|
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
3249
4134
|
const snapshot = this.observationSnapshot(repository);
|
|
3250
4135
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -3299,6 +4184,9 @@ export class WorkflowService {
|
|
|
3299
4184
|
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3300
4185
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
3301
4186
|
}
|
|
4187
|
+
recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4188
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
|
|
4189
|
+
}
|
|
3302
4190
|
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
3303
4191
|
const normalizedActor = actor.trim();
|
|
3304
4192
|
const normalizedReason = reason.trim();
|
|
@@ -3315,7 +4203,9 @@ export class WorkflowService {
|
|
|
3315
4203
|
? Boolean(record.downstreamProofReplan)
|
|
3316
4204
|
: recoveryMode === 'historical-step'
|
|
3317
4205
|
? Boolean(record.historicalStepProvenance)
|
|
3318
|
-
:
|
|
4206
|
+
: recoveryMode === 'plan-integrity'
|
|
4207
|
+
? Boolean(record.planIntegrity)
|
|
4208
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
3319
4209
|
if (prior && current.systemCommits.includes(currentHead))
|
|
3320
4210
|
return current;
|
|
3321
4211
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -3327,42 +4217,113 @@ export class WorkflowService {
|
|
|
3327
4217
|
? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
|
|
3328
4218
|
: recoveryMode === 'historical-step'
|
|
3329
4219
|
? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
|
|
3330
|
-
: recoveryMode
|
|
3331
|
-
? this.
|
|
3332
|
-
:
|
|
4220
|
+
: recoveryMode === 'plan-integrity'
|
|
4221
|
+
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
4222
|
+
: recoveryMode
|
|
4223
|
+
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4224
|
+
: this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3333
4225
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
3334
4226
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
3335
4227
|
taskId,
|
|
3336
4228
|
blockers: preflight.blockers,
|
|
3337
4229
|
});
|
|
3338
4230
|
}
|
|
3339
|
-
|
|
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 = {
|
|
3340
4259
|
...task,
|
|
3341
|
-
|
|
4260
|
+
revision: task.revision + 1,
|
|
4261
|
+
updatedAt: now.toISOString(),
|
|
4262
|
+
systemCommits: [
|
|
4263
|
+
...task.systemCommits,
|
|
4264
|
+
...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
|
|
4265
|
+
],
|
|
3342
4266
|
dependencyProvenanceRecoveries: [
|
|
3343
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
|
+
: []),
|
|
3344
4311
|
{
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
packageName: PACKAGE_NAME,
|
|
3348
|
-
packageVersion: PACKAGE_VERSION,
|
|
3349
|
-
files: ['package-lock.json', 'package.json'],
|
|
3350
|
-
actor: normalizedActor,
|
|
3351
|
-
reason: normalizedReason,
|
|
3352
|
-
recordedAt: this.now().toISOString(),
|
|
3353
|
-
retainedHistoricalCommits: preflight.retainedHistoricalCommits,
|
|
3354
|
-
...(preflight.activeDownstreamProof
|
|
3355
|
-
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
3356
|
-
: {}),
|
|
3357
|
-
...(preflight.downstreamProofReplan
|
|
3358
|
-
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
3359
|
-
: {}),
|
|
3360
|
-
...(preflight.historicalStepProvenance
|
|
3361
|
-
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
3362
|
-
: {}),
|
|
4312
|
+
path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
|
|
4313
|
+
content: `${JSON.stringify(nextTask, null, 2)}\n`,
|
|
3363
4314
|
},
|
|
3364
4315
|
],
|
|
3365
|
-
|
|
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);
|
|
3366
4327
|
}
|
|
3367
4328
|
milestoneProgress(repository, milestoneId) {
|
|
3368
4329
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -3619,7 +4580,9 @@ export class WorkflowService {
|
|
|
3619
4580
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
3620
4581
|
}
|
|
3621
4582
|
next(repository, taskId = null) {
|
|
3622
|
-
|
|
4583
|
+
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
4584
|
+
}
|
|
4585
|
+
nextFromStatus(repository, taskId, status) {
|
|
3623
4586
|
if (status.observation.kind === 'blocked') {
|
|
3624
4587
|
return {
|
|
3625
4588
|
scope: 'project',
|
|
@@ -3759,7 +4722,24 @@ export class WorkflowService {
|
|
|
3759
4722
|
...(dependency ? { dependency } : {}),
|
|
3760
4723
|
});
|
|
3761
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;
|
|
3762
4741
|
const activeTask = requestedTask
|
|
4742
|
+
?? prioritizedRemediationTask
|
|
3763
4743
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3764
4744
|
if (activeTask) {
|
|
3765
4745
|
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
@@ -3789,6 +4769,22 @@ export class WorkflowService {
|
|
|
3789
4769
|
},
|
|
3790
4770
|
});
|
|
3791
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
|
+
}
|
|
3792
4788
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3793
4789
|
if (dependencyRecovery.eligible) {
|
|
3794
4790
|
return withAdoption({
|
|
@@ -3915,41 +4911,132 @@ export class WorkflowService {
|
|
|
3915
4911
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3916
4912
|
throw error;
|
|
3917
4913
|
const blockingDecision = error.details.decision;
|
|
3918
|
-
|
|
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
|
|
3919
4949
|
? {
|
|
3920
4950
|
scope: 'task',
|
|
3921
4951
|
id: activeTask.id,
|
|
3922
4952
|
status: activeTask.status,
|
|
3923
|
-
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',
|
|
3924
4962
|
blockedAction: 'task authorize',
|
|
3925
4963
|
revision: activeTask.revision,
|
|
3926
4964
|
planRiskAuditDecision: {
|
|
3927
4965
|
state: 'recorded-blocking',
|
|
3928
4966
|
decision: blockingDecision,
|
|
3929
4967
|
reason: error.message,
|
|
3930
|
-
requiredAction:
|
|
3931
|
-
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
3932
|
-
: '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.',
|
|
3933
4969
|
},
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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,
|
|
3951
5002
|
},
|
|
3952
|
-
|
|
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
|
+
};
|
|
3953
5040
|
}
|
|
3954
5041
|
}
|
|
3955
5042
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -3958,6 +5045,37 @@ export class WorkflowService {
|
|
|
3958
5045
|
? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
|
|
3959
5046
|
: planRiskAuditRecovery
|
|
3960
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
|
+
}
|
|
3961
5079
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3962
5080
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3963
5081
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3967,8 +5085,12 @@ export class WorkflowService {
|
|
|
3967
5085
|
|| taskNext?.action === 'task step-review'
|
|
3968
5086
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
3969
5087
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
5088
|
+
|| (taskNext?.action === 'task writer-credential-replace'
|
|
5089
|
+
&& taskNext?.blockedAction === 'task remediation-mode-recover')
|
|
3970
5090
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3971
5091
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
5092
|
+
|| taskNext?.action === 'task corrective-yield'
|
|
5093
|
+
|| String(taskNext?.action ?? '').startsWith('task corrective-replan')
|
|
3972
5094
|
|| hardStop
|
|
3973
5095
|
|| terminalCorrectiveDecision === 'split-required'
|
|
3974
5096
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
@@ -4005,8 +5127,10 @@ export class WorkflowService {
|
|
|
4005
5127
|
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
4006
5128
|
: null)
|
|
4007
5129
|
: null;
|
|
5130
|
+
const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
|
|
5131
|
+
&& taskNext.rollingWave?.state === 'root-cause-replan-required';
|
|
4008
5132
|
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4009
|
-
&& taskNext?.action !== 'task plan-set'
|
|
5133
|
+
&& (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
|
|
4010
5134
|
&& refreshAssessment?.eligible
|
|
4011
5135
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
4012
5136
|
: [];
|
|
@@ -4052,6 +5176,15 @@ export class WorkflowService {
|
|
|
4052
5176
|
},
|
|
4053
5177
|
}
|
|
4054
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
|
+
: {}),
|
|
4055
5188
|
}));
|
|
4056
5189
|
}
|
|
4057
5190
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -4358,6 +5491,32 @@ export class WorkflowService {
|
|
|
4358
5491
|
milestonePlanHash: milestone.planHash,
|
|
4359
5492
|
};
|
|
4360
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
|
+
}
|
|
4361
5520
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
4362
5521
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
4363
5522
|
? 'task.execution_authorize'
|
|
@@ -4367,6 +5526,12 @@ export class WorkflowService {
|
|
|
4367
5526
|
const delegatedApprovalOptions = transition
|
|
4368
5527
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
4369
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;
|
|
4370
5535
|
let remediationModeRecovery = null;
|
|
4371
5536
|
let guardedCorrectivePosture = null;
|
|
4372
5537
|
if (planRiskAudit) {
|
|
@@ -4374,7 +5539,7 @@ export class WorkflowService {
|
|
|
4374
5539
|
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
4375
5540
|
}
|
|
4376
5541
|
catch (error) {
|
|
4377
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5542
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4378
5543
|
if (!remediationModeRecovery)
|
|
4379
5544
|
throw error;
|
|
4380
5545
|
}
|
|
@@ -4387,7 +5552,7 @@ export class WorkflowService {
|
|
|
4387
5552
|
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
4388
5553
|
}
|
|
4389
5554
|
catch (error) {
|
|
4390
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5555
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4391
5556
|
if (!remediationModeRecovery)
|
|
4392
5557
|
throw error;
|
|
4393
5558
|
}
|
|
@@ -4399,6 +5564,11 @@ export class WorkflowService {
|
|
|
4399
5564
|
&& changedFiles(repositoryRoot).length > 0
|
|
4400
5565
|
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
4401
5566
|
: null;
|
|
5567
|
+
const planIntegrityDirtyCarryover = next.action === 'task run'
|
|
5568
|
+
&& nextStepId
|
|
5569
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
5570
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
5571
|
+
: null;
|
|
4402
5572
|
let mechanicalFeasibilityOverlay = {};
|
|
4403
5573
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
4404
5574
|
try {
|
|
@@ -4449,6 +5619,9 @@ export class WorkflowService {
|
|
|
4449
5619
|
}
|
|
4450
5620
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
4451
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
|
+
: [];
|
|
4452
5625
|
const failedStepReviewRouting = failedStep
|
|
4453
5626
|
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
4454
5627
|
: null;
|
|
@@ -4579,11 +5752,21 @@ export class WorkflowService {
|
|
|
4579
5752
|
stepId: planIntegrityAssessment.stepId,
|
|
4580
5753
|
planIntegrityRecovery: {
|
|
4581
5754
|
state: 'eligible',
|
|
4582
|
-
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',
|
|
4583
5762
|
expectedRevision: task.revision,
|
|
4584
5763
|
requiredActor,
|
|
4585
5764
|
evidence: planIntegrityAssessment.evidence,
|
|
4586
|
-
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',
|
|
4587
5770
|
preservesWorktree: true,
|
|
4588
5771
|
},
|
|
4589
5772
|
}
|
|
@@ -4716,7 +5899,48 @@ export class WorkflowService {
|
|
|
4716
5899
|
};
|
|
4717
5900
|
break;
|
|
4718
5901
|
case 'replan-required':
|
|
4719
|
-
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
|
+
})();
|
|
4720
5944
|
break;
|
|
4721
5945
|
case 'split-required':
|
|
4722
5946
|
{
|
|
@@ -4874,6 +6098,11 @@ export class WorkflowService {
|
|
|
4874
6098
|
externalSealedReview: {
|
|
4875
6099
|
packetCommand: 'task step-review-packet',
|
|
4876
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`),
|
|
4877
6106
|
reviewerMustUseSeparateChat: true,
|
|
4878
6107
|
repositorySealRequired: true,
|
|
4879
6108
|
},
|
|
@@ -4889,6 +6118,11 @@ export class WorkflowService {
|
|
|
4889
6118
|
externalSealedReview: {
|
|
4890
6119
|
packetCommand: 'task review-packet',
|
|
4891
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`),
|
|
4892
6126
|
reviewerMustUseSeparateChat: true,
|
|
4893
6127
|
repositorySealRequired: true,
|
|
4894
6128
|
},
|
|
@@ -4907,7 +6141,11 @@ export class WorkflowService {
|
|
|
4907
6141
|
: {}),
|
|
4908
6142
|
...guardedCorrectiveOverlay,
|
|
4909
6143
|
...downstreamProofOverlay,
|
|
4910
|
-
...(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)
|
|
4911
6149
|
? {
|
|
4912
6150
|
action: 'task plan-set',
|
|
4913
6151
|
blockedAction: 'task run',
|
|
@@ -4944,7 +6182,44 @@ export class WorkflowService {
|
|
|
4944
6182
|
},
|
|
4945
6183
|
}
|
|
4946
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
|
+
: {}),
|
|
4947
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
|
+
: {}),
|
|
4948
6223
|
};
|
|
4949
6224
|
if (handoffPosture.state === 'pending') {
|
|
4950
6225
|
return this.withTaskNavigationContracts(task, {
|
|
@@ -5003,7 +6278,20 @@ export class WorkflowService {
|
|
|
5003
6278
|
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
5004
6279
|
}
|
|
5005
6280
|
taskHasTerminalCorrectiveDecision(task) {
|
|
5006
|
-
|
|
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
|
+
}
|
|
5007
6295
|
}
|
|
5008
6296
|
taskTerminalCorrectivePosture(task) {
|
|
5009
6297
|
if (!task.planHash)
|
|
@@ -5166,6 +6454,10 @@ export class WorkflowService {
|
|
|
5166
6454
|
statePlanHash: task.planHash,
|
|
5167
6455
|
});
|
|
5168
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
|
+
}
|
|
5169
6461
|
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
5170
6462
|
if (!mechanicalFeasibility)
|
|
5171
6463
|
return authorization;
|
|
@@ -5237,7 +6529,218 @@ export class WorkflowService {
|
|
|
5237
6529
|
},
|
|
5238
6530
|
};
|
|
5239
6531
|
}
|
|
5240
|
-
|
|
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) {
|
|
5241
6744
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
5242
6745
|
const now = this.now().getTime();
|
|
5243
6746
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -5257,7 +6760,7 @@ export class WorkflowService {
|
|
|
5257
6760
|
currentBranch: versions.currentBranch,
|
|
5258
6761
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
5259
6762
|
},
|
|
5260
|
-
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance);
|
|
6763
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
|
|
5261
6764
|
}
|
|
5262
6765
|
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
5263
6766
|
const now = this.now().getTime();
|
|
@@ -5279,10 +6782,9 @@ export class WorkflowService {
|
|
|
5279
6782
|
const policy = gitMutationPolicyForAction(task, action);
|
|
5280
6783
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
5281
6784
|
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
5282
|
-
const
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
: 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;
|
|
5286
6788
|
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
5287
6789
|
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
5288
6790
|
? {
|
|
@@ -5294,9 +6796,24 @@ export class WorkflowService {
|
|
|
5294
6796
|
acquiresLeaseWhenAbsent: true,
|
|
5295
6797
|
}
|
|
5296
6798
|
: null;
|
|
6799
|
+
const credentialBlocksAction = Boolean(writerCredentialRecovery
|
|
6800
|
+
&& writerTokenContract
|
|
6801
|
+
&& (activeLease === null || !activeLeaseUsable));
|
|
5297
6802
|
return {
|
|
5298
6803
|
...next,
|
|
5299
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
|
+
: {}),
|
|
5300
6817
|
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
5301
6818
|
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
5302
6819
|
};
|
|
@@ -5708,16 +7225,20 @@ function assertTaskBranch(repositoryRoot, task) {
|
|
|
5708
7225
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
|
|
5709
7226
|
}
|
|
5710
7227
|
}
|
|
5711
|
-
function preserveCompletedStep(task, next) {
|
|
5712
|
-
const previous = task.steps.find((candidate) => candidate.id === next.id && candidate.status === 'completed');
|
|
5713
|
-
if (!previous)
|
|
5714
|
-
return null;
|
|
5715
|
-
const comparable = ({ status: _status, evidence: _evidence, ...definition }) => definition;
|
|
5716
|
-
return JSON.stringify(comparable(previous)) === JSON.stringify(next) ? previous : null;
|
|
5717
|
-
}
|
|
5718
7228
|
function stripStepRuntime(step) {
|
|
5719
|
-
|
|
5720
|
-
|
|
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
|
+
};
|
|
5721
7242
|
}
|
|
5722
7243
|
function validateReviewInput(review) {
|
|
5723
7244
|
if (!review.reviewer.trim())
|
|
@@ -6091,7 +7612,32 @@ function gitMutationPolicyForAction(task, action) {
|
|
|
6091
7612
|
return null;
|
|
6092
7613
|
}
|
|
6093
7614
|
}
|
|
6094
|
-
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) {
|
|
6095
7641
|
const alwaysLeaseBound = [
|
|
6096
7642
|
'task step-complete',
|
|
6097
7643
|
'task downstream-proof-recover',
|
|
@@ -6117,16 +7663,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
6117
7663
|
'task handback',
|
|
6118
7664
|
'task handback-create',
|
|
6119
7665
|
];
|
|
6120
|
-
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))) {
|
|
6121
7669
|
return null;
|
|
6122
7670
|
}
|
|
6123
7671
|
return {
|
|
6124
7672
|
required: true,
|
|
6125
|
-
option: '--writer-
|
|
6126
|
-
source: '
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
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',
|
|
6130
7679
|
};
|
|
6131
7680
|
}
|
|
6132
7681
|
function observationPreflightBlockers(observation) {
|