codex-workflow-v2 2.0.0-beta.12.9 → 2.0.0-beta.13.1
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.d.ts +1 -0
- package/dist/src/alpha6/plan-risk.js +36 -7
- 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 +168 -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 +21 -16
- 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/workflow.d.ts +65 -4
- package/dist/src/workflow.js +1710 -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 +189 -22
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +154 -26
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +333 -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 +92 -9
- package/docs/updating-existing-project.md +227 -0
- package/docs/validation-report.md +177 -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/plan-risk-audit-event.schema.json +1 -0
- 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,
|
|
@@ -1522,6 +1930,12 @@ export class WorkflowService {
|
|
|
1522
1930
|
knowledgeMapHash,
|
|
1523
1931
|
graphUse: task.graphUse,
|
|
1524
1932
|
steps: task.steps.map(stripStepRuntime),
|
|
1933
|
+
...(Object.hasOwn(task, 'productionEdges')
|
|
1934
|
+
? { productionEdges: structuredClone(task.productionEdges ?? []) }
|
|
1935
|
+
: {}),
|
|
1936
|
+
...(Object.hasOwn(task, 'requiresProductionEdges')
|
|
1937
|
+
? { requiresProductionEdges: task.requiresProductionEdges === true }
|
|
1938
|
+
: {}),
|
|
1525
1939
|
};
|
|
1526
1940
|
validatePlan(reboundPlan);
|
|
1527
1941
|
this.validateKnowledgeTargets(knowledgeMap, reboundPlan);
|
|
@@ -1678,7 +2092,12 @@ export class WorkflowService {
|
|
|
1678
2092
|
requiredAction: 'task plan-set',
|
|
1679
2093
|
});
|
|
1680
2094
|
}
|
|
1681
|
-
const
|
|
2095
|
+
const correctiveAuthority = delegationGrantId === null
|
|
2096
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
2097
|
+
: null;
|
|
2098
|
+
const delegation = correctiveAuthority
|
|
2099
|
+
? null
|
|
2100
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1682
2101
|
const event = {
|
|
1683
2102
|
kind: 'execution',
|
|
1684
2103
|
decision: 'approved',
|
|
@@ -1690,7 +2109,11 @@ export class WorkflowService {
|
|
|
1690
2109
|
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1691
2110
|
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1692
2111
|
reason,
|
|
1693
|
-
authorizationMode:
|
|
2112
|
+
authorizationMode: correctiveAuthority
|
|
2113
|
+
? 'corrective-derived'
|
|
2114
|
+
: delegation
|
|
2115
|
+
? 'delegated'
|
|
2116
|
+
: 'human',
|
|
1694
2117
|
...(delegation ? { delegation } : {}),
|
|
1695
2118
|
mechanicalFeasibility,
|
|
1696
2119
|
};
|
|
@@ -1875,7 +2298,7 @@ export class WorkflowService {
|
|
|
1875
2298
|
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1876
2299
|
const action = String(navigation.action);
|
|
1877
2300
|
const currentLease = context.locks.inspect(taskId);
|
|
1878
|
-
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
2301
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
1879
2302
|
const requiredActor = writerTokenContract
|
|
1880
2303
|
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1881
2304
|
: null;
|
|
@@ -1889,9 +2312,7 @@ export class WorkflowService {
|
|
|
1889
2312
|
nextAction: action,
|
|
1890
2313
|
});
|
|
1891
2314
|
}
|
|
1892
|
-
const lease =
|
|
1893
|
-
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1894
|
-
: context.locks.acquire(taskId, normalizedActor);
|
|
2315
|
+
const lease = context.locks.replaceOrRecoverCredential(taskId, normalizedActor);
|
|
1895
2316
|
if (posture.state === 'claimed') {
|
|
1896
2317
|
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1897
2318
|
}
|
|
@@ -2010,6 +2431,7 @@ export class WorkflowService {
|
|
|
2010
2431
|
}
|
|
2011
2432
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2012
2433
|
const step = requireStep(task, stepId);
|
|
2434
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2013
2435
|
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
2014
2436
|
if (!['planned', 'failed'].includes(step.status)) {
|
|
2015
2437
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
@@ -2044,18 +2466,25 @@ export class WorkflowService {
|
|
|
2044
2466
|
const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
|
|
2045
2467
|
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2046
2468
|
: null;
|
|
2469
|
+
const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2470
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2471
|
+
: null;
|
|
2047
2472
|
if (step.status === 'planned'
|
|
2048
2473
|
&& dirty.length > 0
|
|
2049
2474
|
&& !resumesCorrectiveReplan
|
|
2050
|
-
&& !downstreamProofCarryover?.eligible
|
|
2475
|
+
&& !downstreamProofCarryover?.eligible
|
|
2476
|
+
&& !planIntegrityDirtyCarryover?.eligible) {
|
|
2051
2477
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2052
2478
|
changedFiles: dirty,
|
|
2053
2479
|
...(downstreamProofCarryover?.applicable
|
|
2054
2480
|
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2055
2481
|
: {}),
|
|
2482
|
+
...(planIntegrityDirtyCarryover?.applicable
|
|
2483
|
+
? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
|
|
2484
|
+
: {}),
|
|
2056
2485
|
});
|
|
2057
2486
|
}
|
|
2058
|
-
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file,
|
|
2487
|
+
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
2059
2488
|
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
2060
2489
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
2061
2490
|
changedFiles: dirtyOutsideStep,
|
|
@@ -2102,7 +2531,9 @@ export class WorkflowService {
|
|
|
2102
2531
|
return context.locks.acquire(taskId, actor.trim() || 'worker');
|
|
2103
2532
|
})();
|
|
2104
2533
|
}
|
|
2105
|
-
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2534
|
+
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2535
|
+
? { ...candidate, status: 'in_progress', evidence: null, failurePause: null }
|
|
2536
|
+
: candidate);
|
|
2106
2537
|
let saved;
|
|
2107
2538
|
try {
|
|
2108
2539
|
saved = this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
@@ -2126,7 +2557,7 @@ export class WorkflowService {
|
|
|
2126
2557
|
path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
|
|
2127
2558
|
],
|
|
2128
2559
|
allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
|
|
2129
|
-
allowedWrites
|
|
2560
|
+
allowedWrites,
|
|
2130
2561
|
commands: step.checks,
|
|
2131
2562
|
forbiddenScope: [
|
|
2132
2563
|
...step.forbiddenScope,
|
|
@@ -2245,6 +2676,7 @@ export class WorkflowService {
|
|
|
2245
2676
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
2246
2677
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2247
2678
|
const step = requireStep(task, stepId);
|
|
2679
|
+
const allowedWrites = effectiveStepAllowedWrites(this.store, task, stepId);
|
|
2248
2680
|
if (task.status !== 'in_progress' || step.status !== 'in_progress') {
|
|
2249
2681
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
|
|
2250
2682
|
}
|
|
@@ -2277,18 +2709,114 @@ export class WorkflowService {
|
|
|
2277
2709
|
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
2278
2710
|
const failed = checks.filter((check) => !check.passed);
|
|
2279
2711
|
if (failed.length > 0) {
|
|
2280
|
-
|
|
2281
|
-
|
|
2712
|
+
const now = this.now();
|
|
2713
|
+
const normalizedFailures = normalizeMechanicalFailures(failed, context.identity.repositoryRoot);
|
|
2714
|
+
// Bind useful uncommitted work to this failure without treating it as
|
|
2715
|
+
// completed-Step authority.
|
|
2716
|
+
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
2717
|
+
const failurePause = {
|
|
2718
|
+
recordedAt: now.toISOString(),
|
|
2719
|
+
dirtyFiles,
|
|
2720
|
+
dirtyWorktreeHash: hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles),
|
|
2721
|
+
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2722
|
+
failedChecks: normalizedFailures,
|
|
2723
|
+
};
|
|
2724
|
+
const preparedRemediation = reviewRequired && planRiskAudit
|
|
2725
|
+
? prepareRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
2282
2726
|
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
2283
2727
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2284
2728
|
reviewEventHash: null,
|
|
2285
2729
|
reviewerAttestationHash: null,
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2730
|
+
executionTaskRevision: task.revision,
|
|
2731
|
+
dirtyWorktreeHash: failurePause.dirtyWorktreeHash,
|
|
2732
|
+
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), mechanicalCauseBindings(normalizedFailures))
|
|
2733
|
+
: null;
|
|
2734
|
+
const steps = task.steps.map((candidate) => candidate.id === stepId
|
|
2735
|
+
? { ...candidate, status: 'failed', evidence: null, failurePause }
|
|
2736
|
+
: candidate);
|
|
2737
|
+
const paused = {
|
|
2738
|
+
...task,
|
|
2739
|
+
revision: task.revision + 1,
|
|
2740
|
+
updatedAt: now.toISOString(),
|
|
2741
|
+
status: 'needs_fix',
|
|
2742
|
+
steps,
|
|
2743
|
+
blockReason: `${stepId} checks failed.`,
|
|
2744
|
+
};
|
|
2745
|
+
const stepHistory = readRemediationEvents(this.store, task)
|
|
2746
|
+
.filter((event) => event.stepId === stepId);
|
|
2747
|
+
const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
|
|
2748
|
+
? stepHistory
|
|
2749
|
+
: [...stepHistory, preparedRemediation.event];
|
|
2750
|
+
const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
|
|
2751
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2752
|
+
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
|
|
2753
|
+
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
2754
|
+
actor: c1Posture.claimant,
|
|
2755
|
+
writerToken: lease.token,
|
|
2756
|
+
reason: `Yield execution authority after repeated mechanical cause ${preparedRemediation.event.eventId}.`,
|
|
2757
|
+
eventId: `HOF-${sha256Hex([
|
|
2758
|
+
'checks-failed-root-cause-replan-pause-v1',
|
|
2759
|
+
task.id,
|
|
2760
|
+
String(task.revision),
|
|
2761
|
+
preparedRemediation.event.eventHash,
|
|
2762
|
+
c1Posture.claimant,
|
|
2763
|
+
].join('\0'))}`,
|
|
2764
|
+
}, now)
|
|
2765
|
+
: null;
|
|
2766
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2767
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2768
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2769
|
+
const failureHead = headCommit(context.identity.repositoryRoot);
|
|
2770
|
+
applyProjectTransaction({
|
|
2771
|
+
projectRoot,
|
|
2772
|
+
projectId: task.projectId,
|
|
2773
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2774
|
+
kind: rollingCauseDecision === 'root-cause-replan'
|
|
2775
|
+
? 'failed-step-root-cause-replan-pause'
|
|
2776
|
+
: 'failed-step-continuation',
|
|
2777
|
+
now,
|
|
2778
|
+
targets: [
|
|
2779
|
+
...(preparedRemediation?.postimage
|
|
2780
|
+
? [{
|
|
2781
|
+
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
2782
|
+
content: preparedRemediation.postimage,
|
|
2783
|
+
}]
|
|
2784
|
+
: []),
|
|
2785
|
+
...(preparedYield
|
|
2786
|
+
? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
|
|
2787
|
+
: []),
|
|
2788
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
|
|
2789
|
+
{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
|
|
2790
|
+
],
|
|
2791
|
+
validate: () => {
|
|
2792
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2793
|
+
const savedStep = requireStep(saved, stepId);
|
|
2794
|
+
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
2795
|
+
const remediationHash = preparedRemediation
|
|
2796
|
+
? readRemediationEvents(this.store, saved).find((event) => event.eventId === preparedRemediation.event.eventId)?.eventHash ?? null
|
|
2797
|
+
: null;
|
|
2798
|
+
if (saved.revision !== paused.revision
|
|
2799
|
+
|| saved.status !== 'needs_fix'
|
|
2800
|
+
|| savedStep.status !== 'failed'
|
|
2801
|
+
|| savedStep.failurePause?.dirtyWorktreeHash !== failurePause.dirtyWorktreeHash
|
|
2802
|
+
|| savedStep.failurePause?.checksHash !== failurePause.checksHash
|
|
2803
|
+
|| remediationHash !== (preparedRemediation?.event.eventHash ?? null)
|
|
2804
|
+
|| context.locks.inspect(task.id) !== null
|
|
2805
|
+
|| (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
|
|
2806
|
+
|| headCommit(context.identity.repositoryRoot) !== failureHead
|
|
2807
|
+
|| JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
|
|
2808
|
+
|| hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== failurePause.dirtyWorktreeHash) {
|
|
2809
|
+
throw new WorkflowError('STATE_CORRUPT', 'Failed-Step continuation transaction readback failed.', {
|
|
2810
|
+
taskId,
|
|
2811
|
+
stepId,
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
2815
|
+
},
|
|
2816
|
+
});
|
|
2817
|
+
return this.store.readTask(task.projectId, task.id);
|
|
2290
2818
|
}
|
|
2291
|
-
const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title,
|
|
2819
|
+
const committed = commitStep(context.identity.repositoryRoot, task.id, step.id, step.title, allowedWrites);
|
|
2292
2820
|
const evidence = {
|
|
2293
2821
|
recordedAt: this.now().toISOString(),
|
|
2294
2822
|
commitSha: committed.commitSha,
|
|
@@ -2327,7 +2855,7 @@ export class WorkflowService {
|
|
|
2327
2855
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2328
2856
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2329
2857
|
assertExpectedRevision(task, expectedRevision);
|
|
2330
|
-
this.
|
|
2858
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2331
2859
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
2332
2860
|
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2333
2861
|
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
@@ -2398,18 +2926,14 @@ export class WorkflowService {
|
|
|
2398
2926
|
resolution: currentCycle.resolution,
|
|
2399
2927
|
});
|
|
2400
2928
|
}
|
|
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));
|
|
2929
|
+
return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, currentCycle.reviewRecordedEvent, currentCycle.reviewerAttestationEvent.eventHash, planRiskAudit, undefined, reviewLease);
|
|
2407
2930
|
}
|
|
2408
2931
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
2409
2932
|
}
|
|
2410
2933
|
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2411
2934
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2412
|
-
const
|
|
2935
|
+
const reviewTask = taskWithEffectiveStepAllowedWrites(this.store, task, stepId);
|
|
2936
|
+
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
2937
|
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2414
2938
|
review: launch.review,
|
|
2415
2939
|
reviewerOutputHash: launch.reviewerOutputHash,
|
|
@@ -2426,16 +2950,11 @@ export class WorkflowService {
|
|
|
2426
2950
|
resolution: recorded.resolution,
|
|
2427
2951
|
});
|
|
2428
2952
|
}
|
|
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));
|
|
2953
|
+
return this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewRecordedEvent, reviewerAttestationEvent.eventHash, planRiskAudit, launch.review.summary, reviewLease);
|
|
2435
2954
|
}
|
|
2436
2955
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2437
2956
|
}
|
|
2438
|
-
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2957
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision, reviewMode = 'ordinary') {
|
|
2439
2958
|
const identity = resolveRepositoryIdentity(repository);
|
|
2440
2959
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2441
2960
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2455,15 +2974,18 @@ export class WorkflowService {
|
|
|
2455
2974
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2456
2975
|
}
|
|
2457
2976
|
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);
|
|
2977
|
+
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
2978
|
}
|
|
2460
2979
|
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2461
2980
|
const context = this.#mutationContext(repository);
|
|
2462
2981
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2463
2982
|
assertExpectedRevision(task, expectedRevision);
|
|
2464
|
-
this.
|
|
2983
|
+
const reviewLease = this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
2465
2984
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2466
|
-
|
|
2985
|
+
if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
|
|
2986
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'External Step Review requires reviewMode ordinary or security.');
|
|
2987
|
+
}
|
|
2988
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision, input.reviewMode);
|
|
2467
2989
|
let review;
|
|
2468
2990
|
try {
|
|
2469
2991
|
review = validateExternalStrictStepReviewInput(input, packet);
|
|
@@ -2498,23 +3020,131 @@ export class WorkflowService {
|
|
|
2498
3020
|
isolationResult: 'verified',
|
|
2499
3021
|
}, this.now());
|
|
2500
3022
|
if (recorded.resolution === 'failed') {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
3023
|
+
const saved = this.finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, recorded.reviewRecordedEvent, recorded.reviewerAttestationEvent.eventHash, readCurrentPlanRiskAudit(this.store, task), review.summary, reviewLease);
|
|
3024
|
+
return {
|
|
3025
|
+
mode: 'external-sealed',
|
|
3026
|
+
reviewMode: packet.reviewMode,
|
|
3027
|
+
packetHash: packet.packetHash,
|
|
3028
|
+
repositorySealHash: packet.repositorySealHash,
|
|
3029
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
3030
|
+
task: saved,
|
|
3031
|
+
};
|
|
2507
3032
|
}
|
|
2508
3033
|
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2509
3034
|
return {
|
|
2510
3035
|
mode: 'external-sealed',
|
|
3036
|
+
reviewMode: packet.reviewMode,
|
|
2511
3037
|
packetHash: packet.packetHash,
|
|
2512
3038
|
repositorySealHash: packet.repositorySealHash,
|
|
2513
3039
|
reviewerThreadId: input.reviewerThreadId,
|
|
2514
3040
|
task: saved,
|
|
2515
3041
|
};
|
|
2516
3042
|
}
|
|
2517
|
-
|
|
3043
|
+
finalizeFailedStrictStepReview(context, task, stepId, expectedRevision, reviewEvent, reviewerAttestationHash, planRiskAudit, summary, reviewLease) {
|
|
3044
|
+
const now = this.now();
|
|
3045
|
+
const preparedRemediation = prepareRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
3046
|
+
completionCommit: reviewEvent.completionCommit,
|
|
3047
|
+
checksHash: reviewEvent.checksHash,
|
|
3048
|
+
reviewEventHash: reviewEvent.eventHash,
|
|
3049
|
+
reviewerAttestationHash,
|
|
3050
|
+
}, planRiskAudit, now, this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot), reviewCauseBindings(reviewEvent));
|
|
3051
|
+
const stepHistory = readRemediationEvents(this.store, task)
|
|
3052
|
+
.filter((event) => event.stepId === stepId);
|
|
3053
|
+
const resultingHistory = preparedRemediation.postimage === null
|
|
3054
|
+
? stepHistory
|
|
3055
|
+
: [...stepHistory, preparedRemediation.event];
|
|
3056
|
+
const rollingCauseDecision = deriveRollingCauseDecision(resultingHistory);
|
|
3057
|
+
const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
|
|
3058
|
+
const paused = {
|
|
3059
|
+
...resolved,
|
|
3060
|
+
revision: task.revision + 1,
|
|
3061
|
+
updatedAt: now.toISOString(),
|
|
3062
|
+
};
|
|
3063
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
3064
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
3065
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
3066
|
+
const failureHead = headCommit(context.identity.repositoryRoot);
|
|
3067
|
+
const dirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3068
|
+
const dirtyWorktreeHash = hashDirtyWorktree(context.identity.repositoryRoot, dirtyFiles);
|
|
3069
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
3070
|
+
if (rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && !reviewLease) {
|
|
3071
|
+
throw new WorkflowError('STATE_CONFLICT', 'Root-cause replan pause requires the claimed writer authority it will release.', {
|
|
3072
|
+
taskId: task.id,
|
|
3073
|
+
stepId,
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed' && reviewLease
|
|
3077
|
+
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
3078
|
+
actor: c1Posture.claimant,
|
|
3079
|
+
writerToken: reviewLease.token,
|
|
3080
|
+
reason: `Yield execution authority after repeated authoritative cause ${preparedRemediation.event.eventId}.`,
|
|
3081
|
+
eventId: `HOF-${sha256Hex([
|
|
3082
|
+
'strict-review-root-cause-replan-pause-v1',
|
|
3083
|
+
task.id,
|
|
3084
|
+
String(task.revision),
|
|
3085
|
+
preparedRemediation.event.eventHash,
|
|
3086
|
+
c1Posture.claimant,
|
|
3087
|
+
].join('\0'))}`,
|
|
3088
|
+
}, now)
|
|
3089
|
+
: null;
|
|
3090
|
+
if (reviewLease) {
|
|
3091
|
+
const currentLease = context.locks.inspect(task.id);
|
|
3092
|
+
if (!currentLease || currentLease.token !== reviewLease.token || currentLease.owner !== reviewLease.owner) {
|
|
3093
|
+
throw new WorkflowError('STATE_CONFLICT', 'Strict Step Review writer authority changed before the failure pause could be recorded.', {
|
|
3094
|
+
taskId: task.id,
|
|
3095
|
+
stepId,
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
applyProjectTransaction({
|
|
3100
|
+
projectRoot,
|
|
3101
|
+
projectId: task.projectId,
|
|
3102
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
3103
|
+
kind: rollingCauseDecision === 'root-cause-replan'
|
|
3104
|
+
? 'strict-review-root-cause-replan-pause'
|
|
3105
|
+
: 'strict-review-failed-step-pause',
|
|
3106
|
+
now,
|
|
3107
|
+
targets: [
|
|
3108
|
+
...(preparedRemediation.postimage
|
|
3109
|
+
? [{
|
|
3110
|
+
path: relative(path.join(taskRoot, 'remediation-events.jsonl')),
|
|
3111
|
+
content: preparedRemediation.postimage,
|
|
3112
|
+
}]
|
|
3113
|
+
: []),
|
|
3114
|
+
...(preparedYield
|
|
3115
|
+
? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
|
|
3116
|
+
: []),
|
|
3117
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(paused, null, 2)}\n` },
|
|
3118
|
+
...(reviewLease
|
|
3119
|
+
? [{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null }]
|
|
3120
|
+
: []),
|
|
3121
|
+
],
|
|
3122
|
+
validate: () => {
|
|
3123
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
3124
|
+
const savedStep = requireStep(saved, stepId);
|
|
3125
|
+
const savedRemediation = readRemediationEvents(this.store, saved)
|
|
3126
|
+
.find((event) => event.eventId === preparedRemediation.event.eventId) ?? null;
|
|
3127
|
+
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
3128
|
+
if (saved.revision !== paused.revision
|
|
3129
|
+
|| saved.status !== 'needs_fix'
|
|
3130
|
+
|| savedStep.status !== 'failed'
|
|
3131
|
+
|| savedRemediation?.eventHash !== preparedRemediation.event.eventHash
|
|
3132
|
+
|| context.locks.inspect(task.id) !== null
|
|
3133
|
+
|| (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')
|
|
3134
|
+
|| headCommit(context.identity.repositoryRoot) !== failureHead
|
|
3135
|
+
|| JSON.stringify(readbackDirtyFiles) !== JSON.stringify(dirtyFiles)
|
|
3136
|
+
|| hashDirtyWorktree(context.identity.repositoryRoot, readbackDirtyFiles) !== dirtyWorktreeHash) {
|
|
3137
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review failure-pause transaction readback failed.', {
|
|
3138
|
+
taskId: task.id,
|
|
3139
|
+
stepId,
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
3143
|
+
},
|
|
3144
|
+
});
|
|
3145
|
+
return this.store.readTask(task.projectId, task.id);
|
|
3146
|
+
}
|
|
3147
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision, reviewMode = 'ordinary') {
|
|
2518
3148
|
const identity = resolveRepositoryIdentity(repository);
|
|
2519
3149
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2520
3150
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -2531,7 +3161,7 @@ export class WorkflowService {
|
|
|
2531
3161
|
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2532
3162
|
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2533
3163
|
}
|
|
2534
|
-
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
3164
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'), task.steps.flatMap((step) => reviewScopeAugmentations(this.store, task, step.id)), reviewMode);
|
|
2535
3165
|
}
|
|
2536
3166
|
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2537
3167
|
const context = this.#mutationContext(repository);
|
|
@@ -2539,7 +3169,10 @@ export class WorkflowService {
|
|
|
2539
3169
|
assertExpectedRevision(task, expectedRevision);
|
|
2540
3170
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2541
3171
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2542
|
-
|
|
3172
|
+
if (input.reviewMode !== 'ordinary' && input.reviewMode !== 'security') {
|
|
3173
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'External Task Review requires reviewMode ordinary or security.');
|
|
3174
|
+
}
|
|
3175
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision, input.reviewMode);
|
|
2543
3176
|
let review;
|
|
2544
3177
|
try {
|
|
2545
3178
|
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
@@ -2553,6 +3186,7 @@ export class WorkflowService {
|
|
|
2553
3186
|
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2554
3187
|
return {
|
|
2555
3188
|
mode: 'external-sealed',
|
|
3189
|
+
reviewMode: packet.reviewMode,
|
|
2556
3190
|
packetHash: packet.packetHash,
|
|
2557
3191
|
repositorySealHash: packet.repositorySealHash,
|
|
2558
3192
|
reviewerThreadId: input.reviewerThreadId,
|
|
@@ -2657,8 +3291,6 @@ export class WorkflowService {
|
|
|
2657
3291
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2658
3292
|
assertExpectedRevision(task, expectedRevision);
|
|
2659
3293
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2660
|
-
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2661
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2662
3294
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2663
3295
|
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2664
3296
|
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
@@ -2690,6 +3322,20 @@ export class WorkflowService {
|
|
|
2690
3322
|
blockers: assessment.blockers,
|
|
2691
3323
|
});
|
|
2692
3324
|
}
|
|
3325
|
+
const remediationEventIds = assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
3326
|
+
? [assessment.evidence.remediationEventId]
|
|
3327
|
+
: assessment.evidence.remediationEvents.map((event) => event.eventId);
|
|
3328
|
+
const compatibility = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.commitSha === headCommit(context.identity.repositoryRoot)
|
|
3329
|
+
&& record.packageVersion === PACKAGE_VERSION
|
|
3330
|
+
&& task.systemCommits.includes(record.commitSha)
|
|
3331
|
+
&& record.planIntegrity?.stepId === stepId
|
|
3332
|
+
&& record.planIntegrity.planHash === task.planHash
|
|
3333
|
+
&& canonicalJsonStringify(record.planIntegrity.remediationEventIds) === canonicalJsonStringify(remediationEventIds)
|
|
3334
|
+
&& record.planIntegrity.dirtyWorktreeHash === hashDirtyWorktree(context.identity.repositoryRoot, record.planIntegrity.dirtyFiles));
|
|
3335
|
+
if (!compatibility) {
|
|
3336
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
3337
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
3338
|
+
}
|
|
2693
3339
|
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2694
3340
|
return { assessment, event };
|
|
2695
3341
|
}
|
|
@@ -2775,7 +3421,12 @@ export class WorkflowService {
|
|
|
2775
3421
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
2776
3422
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
2777
3423
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
2778
|
-
const
|
|
3424
|
+
const correctiveAuthority = delegationGrantId === null
|
|
3425
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
3426
|
+
: null;
|
|
3427
|
+
const delegation = correctiveAuthority
|
|
3428
|
+
? null
|
|
3429
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
|
|
2779
3430
|
const event = {
|
|
2780
3431
|
kind: 'final_acceptance',
|
|
2781
3432
|
decision: 'approved',
|
|
@@ -2787,7 +3438,11 @@ export class WorkflowService {
|
|
|
2787
3438
|
evidenceHash,
|
|
2788
3439
|
headCommit: headCommit(identity.repositoryRoot),
|
|
2789
3440
|
reason: '',
|
|
2790
|
-
authorizationMode:
|
|
3441
|
+
authorizationMode: correctiveAuthority
|
|
3442
|
+
? 'corrective-derived'
|
|
3443
|
+
: delegation
|
|
3444
|
+
? 'delegated'
|
|
3445
|
+
: 'human',
|
|
2791
3446
|
...(delegation ? { delegation } : {}),
|
|
2792
3447
|
};
|
|
2793
3448
|
return this.store.writeTask({
|
|
@@ -2890,7 +3545,9 @@ export class WorkflowService {
|
|
|
2890
3545
|
return saved;
|
|
2891
3546
|
}
|
|
2892
3547
|
status(repository) {
|
|
2893
|
-
|
|
3548
|
+
return this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
3549
|
+
}
|
|
3550
|
+
statusFromSnapshot(snapshot) {
|
|
2894
3551
|
return {
|
|
2895
3552
|
projectId: snapshot.identity.projectId,
|
|
2896
3553
|
discoveries: snapshot.discoveries,
|
|
@@ -2904,6 +3561,73 @@ export class WorkflowService {
|
|
|
2904
3561
|
observation: snapshot.observation,
|
|
2905
3562
|
};
|
|
2906
3563
|
}
|
|
3564
|
+
compactStatus(repository) {
|
|
3565
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3566
|
+
const status = this.statusFromSnapshot(snapshot);
|
|
3567
|
+
const next = this.nextFromStatus(repository, null, status);
|
|
3568
|
+
const routedId = typeof next.id === 'string'
|
|
3569
|
+
? next.id
|
|
3570
|
+
: typeof next.taskId === 'string'
|
|
3571
|
+
? next.taskId
|
|
3572
|
+
: null;
|
|
3573
|
+
const activeTask = routedId === null
|
|
3574
|
+
? null
|
|
3575
|
+
: snapshot.tasks.find((task) => task.id === routedId) ?? null;
|
|
3576
|
+
const routedStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
3577
|
+
const activeStep = activeTask === null
|
|
3578
|
+
? null
|
|
3579
|
+
: routedStepId === null
|
|
3580
|
+
? activeTask.steps.find((step) => step.status === 'in_progress')
|
|
3581
|
+
?? activeTask.steps.find((step) => step.status === 'failed')
|
|
3582
|
+
?? null
|
|
3583
|
+
: activeTask.steps.find((step) => step.id === routedStepId) ?? null;
|
|
3584
|
+
const action = typeof next.action === 'string' ? next.action : 'blocked';
|
|
3585
|
+
const blockedAction = typeof next.blockedAction === 'string' ? next.blockedAction : null;
|
|
3586
|
+
const navigation = next.navigation && typeof next.navigation === 'object' && !Array.isArray(next.navigation)
|
|
3587
|
+
? next.navigation
|
|
3588
|
+
: null;
|
|
3589
|
+
const route = {
|
|
3590
|
+
scope: typeof next.scope === 'string' ? next.scope : null,
|
|
3591
|
+
status: typeof next.status === 'string' ? next.status : null,
|
|
3592
|
+
entityId: routedId,
|
|
3593
|
+
navigationKind: navigation && typeof navigation.kind === 'string' ? navigation.kind : null,
|
|
3594
|
+
transitionId: navigation && typeof navigation.transitionId === 'string' ? navigation.transitionId : null,
|
|
3595
|
+
};
|
|
3596
|
+
const routeFingerprint = sha256Hex(canonicalJsonStringify({
|
|
3597
|
+
projectId: snapshot.identity.projectId,
|
|
3598
|
+
taskId: activeTask?.id ?? null,
|
|
3599
|
+
taskRevision: activeTask?.revision ?? null,
|
|
3600
|
+
stepId: activeStep?.id ?? routedStepId,
|
|
3601
|
+
action,
|
|
3602
|
+
blockedAction,
|
|
3603
|
+
route,
|
|
3604
|
+
observationKind: snapshot.observation.kind,
|
|
3605
|
+
observationReasonCodes: snapshot.observation.reasonCodes,
|
|
3606
|
+
}));
|
|
3607
|
+
return {
|
|
3608
|
+
projection: 'codex-workflow-compact-status-v1',
|
|
3609
|
+
bounded: true,
|
|
3610
|
+
projectId: snapshot.identity.projectId,
|
|
3611
|
+
counts: {
|
|
3612
|
+
discoveries: snapshot.discoveries.length,
|
|
3613
|
+
tasks: snapshot.tasks.length,
|
|
3614
|
+
milestones: snapshot.milestones.length,
|
|
3615
|
+
delegations: snapshot.delegations.length,
|
|
3616
|
+
},
|
|
3617
|
+
activeTask: activeTask
|
|
3618
|
+
? { id: activeTask.id, revision: activeTask.revision, status: activeTask.status }
|
|
3619
|
+
: null,
|
|
3620
|
+
activeStep: activeStep ? { id: activeStep.id, status: activeStep.status } : null,
|
|
3621
|
+
action,
|
|
3622
|
+
blockedAction,
|
|
3623
|
+
routeFingerprint,
|
|
3624
|
+
route,
|
|
3625
|
+
observation: {
|
|
3626
|
+
kind: snapshot.observation.kind,
|
|
3627
|
+
reasonCodes: [...snapshot.observation.reasonCodes],
|
|
3628
|
+
},
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
2907
3631
|
recoverMilestoneTransactions(repository) {
|
|
2908
3632
|
const { identity } = this.#mutationContext(repository);
|
|
2909
3633
|
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
@@ -3239,12 +3963,179 @@ export class WorkflowService {
|
|
|
3239
3963
|
dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
|
|
3240
3964
|
requiredTransport: [
|
|
3241
3965
|
'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.',
|
|
3966
|
+
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
3967
|
+
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
3968
|
+
],
|
|
3969
|
+
blockers,
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
planIntegrityUpdatePreflight(repository, taskId) {
|
|
3973
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3974
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3975
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity update preflight requires a coherent observation boundary.', {
|
|
3976
|
+
observation: snapshot.observation,
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3980
|
+
if (!task)
|
|
3981
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3982
|
+
const assessments = task.steps
|
|
3983
|
+
.filter((step) => step.status === 'failed')
|
|
3984
|
+
.map((step) => assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id))
|
|
3985
|
+
.filter((assessment) => assessment.applicable);
|
|
3986
|
+
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
3987
|
+
const adoptedCompatibility = assessment
|
|
3988
|
+
? null
|
|
3989
|
+
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
3990
|
+
const dirtyCarryoverAssessment = assessment || adoptedCompatibility
|
|
3991
|
+
? null
|
|
3992
|
+
: this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
3993
|
+
const dirtyCarryoverCompatibility = dirtyCarryoverAssessment?.eligible
|
|
3994
|
+
&& dirtyCarryoverAssessment.compatibility
|
|
3995
|
+
? {
|
|
3996
|
+
...dirtyCarryoverAssessment.compatibility,
|
|
3997
|
+
planHash: task.planHash,
|
|
3998
|
+
mode: 'post-rebind-dirty-carryover',
|
|
3999
|
+
}
|
|
4000
|
+
: null;
|
|
4001
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4002
|
+
const versionSurfaces = {
|
|
4003
|
+
declared: versions.declared,
|
|
4004
|
+
locked: versions.locked,
|
|
4005
|
+
installed: versions.installed,
|
|
4006
|
+
currentBranch: versions.currentBranch,
|
|
4007
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4008
|
+
};
|
|
4009
|
+
const sourceVersion = versionSurfaces.declared;
|
|
4010
|
+
const sourceBetaVersion = parseWorkflowBetaVersion(sourceVersion);
|
|
4011
|
+
const targetBetaVersion = parseWorkflowBetaVersion(PACKAGE_VERSION);
|
|
4012
|
+
const sourceIsEligibleBeta12 = sourceBetaVersion?.train === 12
|
|
4013
|
+
&& sourceBetaVersion.patch !== null
|
|
4014
|
+
&& sourceBetaVersion.patch >= 11;
|
|
4015
|
+
const targetIsNewer = sourceBetaVersion !== null
|
|
4016
|
+
&& targetBetaVersion !== null
|
|
4017
|
+
&& (targetBetaVersion.train > sourceBetaVersion.train
|
|
4018
|
+
|| (targetBetaVersion.train === sourceBetaVersion.train
|
|
4019
|
+
&& targetBetaVersion.patch !== null
|
|
4020
|
+
&& sourceBetaVersion.patch !== null
|
|
4021
|
+
&& targetBetaVersion.patch > sourceBetaVersion.patch));
|
|
4022
|
+
const blockers = [
|
|
4023
|
+
...(assessments.length > 1 ? ['Exactly one failed Step may expose a Plan-integrity conflict.'] : []),
|
|
4024
|
+
...(assessment?.blockers ?? (adoptedCompatibility || dirtyCarryoverCompatibility
|
|
4025
|
+
? []
|
|
4026
|
+
: (dirtyCarryoverAssessment?.blockers.length
|
|
4027
|
+
? dirtyCarryoverAssessment.blockers
|
|
4028
|
+
: ['No eligible Plan-integrity conflict, post-rebind scope adoption, or dirty carryover was found.']))),
|
|
4029
|
+
...(!sourceIsEligibleBeta12 || !targetIsNewer
|
|
4030
|
+
? [`Declared Workflow source ${sourceVersion ?? 'missing'} must be an exact beta.12 patch at or after beta.12.11 and older than runner ${PACKAGE_VERSION}.`]
|
|
4031
|
+
: []),
|
|
4032
|
+
...Object.entries(versionSurfaces)
|
|
4033
|
+
.filter(([, version]) => version !== sourceVersion)
|
|
4034
|
+
.map(([surface, version]) => `${surface} Workflow version ${version ?? 'missing'} must equal the declared source ${sourceVersion ?? 'missing'}.`),
|
|
4035
|
+
...(snapshot.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4036
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4037
|
+
];
|
|
4038
|
+
return {
|
|
4039
|
+
readOnly: true,
|
|
4040
|
+
eligible: Boolean(assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0,
|
|
4041
|
+
action: (assessment?.eligible || adoptedCompatibility || dirtyCarryoverCompatibility) && blockers.length === 0
|
|
4042
|
+
? 'update plan-integrity-transport'
|
|
4043
|
+
: null,
|
|
4044
|
+
projectId: task.projectId,
|
|
4045
|
+
taskId: task.id,
|
|
4046
|
+
taskRevision: task.revision,
|
|
4047
|
+
stepId: assessment?.stepId ?? adoptedCompatibility?.stepId ?? dirtyCarryoverCompatibility?.stepId ?? null,
|
|
4048
|
+
sourceVersion,
|
|
4049
|
+
targetVersion: PACKAGE_VERSION,
|
|
4050
|
+
taskBranch: task.taskBranch,
|
|
4051
|
+
baseBranch: task.baseBranch,
|
|
4052
|
+
dirtyFiles: assessment?.evidence?.changedFiles
|
|
4053
|
+
?? adoptedCompatibility?.dirtyFiles
|
|
4054
|
+
?? dirtyCarryoverCompatibility?.dirtyFiles
|
|
4055
|
+
?? [],
|
|
4056
|
+
conflictHash: assessment?.evidence?.conflictHash
|
|
4057
|
+
?? adoptedCompatibility?.conflictHash
|
|
4058
|
+
?? dirtyCarryoverCompatibility?.conflictHash
|
|
4059
|
+
?? null,
|
|
4060
|
+
compatibilityMode: adoptedCompatibility?.mode
|
|
4061
|
+
?? dirtyCarryoverCompatibility?.mode
|
|
4062
|
+
?? 'failed-step-conflict',
|
|
4063
|
+
requiredTransport: [
|
|
4064
|
+
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4065
|
+
'Fast-forward or apply only that dependency commit to the Task branch while preserving all product bytes.',
|
|
4066
|
+
'Install the exact target package, then follow fresh next to Plan-integrity dependency registration.',
|
|
3244
4067
|
],
|
|
3245
4068
|
blockers,
|
|
3246
4069
|
};
|
|
3247
4070
|
}
|
|
4071
|
+
planIntegrityDependencyRecoveryPreflight(repository, taskId) {
|
|
4072
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4073
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
4074
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery requires a coherent observation boundary.', {
|
|
4075
|
+
observation: snapshot.observation,
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4079
|
+
if (!task)
|
|
4080
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4081
|
+
const assessmentBlockers = [];
|
|
4082
|
+
const assessments = task.steps
|
|
4083
|
+
.filter((step) => step.status === 'failed')
|
|
4084
|
+
.flatMap((step) => {
|
|
4085
|
+
try {
|
|
4086
|
+
return [assessPlanIntegrityRecovery(this.store, snapshot.identity.repositoryRoot, task, step.id)];
|
|
4087
|
+
}
|
|
4088
|
+
catch (error) {
|
|
4089
|
+
if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
|
|
4090
|
+
assessmentBlockers.push(`Plan-integrity compatibility cannot be assessed until the existing remediation history is repaired: ${error.message}`);
|
|
4091
|
+
return [];
|
|
4092
|
+
}
|
|
4093
|
+
throw error;
|
|
4094
|
+
}
|
|
4095
|
+
})
|
|
4096
|
+
.filter((assessment) => assessment.applicable && assessment.eligible && assessment.evidence);
|
|
4097
|
+
const assessment = assessments.length === 1 ? assessments[0] : null;
|
|
4098
|
+
const adoptedCompatibility = assessment?.evidence
|
|
4099
|
+
? null
|
|
4100
|
+
: this.postRebindCheckSupportCompatibility(snapshot, task);
|
|
4101
|
+
const dirtyCarryoverCompatibility = assessment?.evidence || adoptedCompatibility
|
|
4102
|
+
? null
|
|
4103
|
+
: this.carriedPostRebindCheckSupportDirtyCarryover(snapshot, task);
|
|
4104
|
+
const carriedCompatibility = !assessment?.evidence && task.planHash
|
|
4105
|
+
? this.carriedPlanIntegrityUpdateCompatibility(snapshot, task)
|
|
4106
|
+
: null;
|
|
4107
|
+
if ((!assessment?.evidence
|
|
4108
|
+
&& !carriedCompatibility
|
|
4109
|
+
&& !adoptedCompatibility
|
|
4110
|
+
&& !dirtyCarryoverCompatibility) || !task.planHash) {
|
|
4111
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
4112
|
+
return {
|
|
4113
|
+
...ordinary,
|
|
4114
|
+
eligible: false,
|
|
4115
|
+
action: null,
|
|
4116
|
+
planIntegrity: null,
|
|
4117
|
+
blockers: [...new Set([
|
|
4118
|
+
...ordinary.blockers,
|
|
4119
|
+
...assessmentBlockers,
|
|
4120
|
+
'The dependency candidate must preserve one exact eligible Plan-integrity conflict.',
|
|
4121
|
+
])],
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
const compatibility = carriedCompatibility
|
|
4125
|
+
?? adoptedCompatibility
|
|
4126
|
+
?? dirtyCarryoverCompatibility
|
|
4127
|
+
?? {
|
|
4128
|
+
stepId: assessment.stepId,
|
|
4129
|
+
planHash: task.planHash,
|
|
4130
|
+
conflictHash: assessment.evidence.conflictHash,
|
|
4131
|
+
dirtyFiles: [...assessment.evidence.changedFiles],
|
|
4132
|
+
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.evidence.changedFiles),
|
|
4133
|
+
remediationEventIds: assessment.evidence.kind === 'missing-npm-script-outside-allowed-writes'
|
|
4134
|
+
? [assessment.evidence.remediationEventId]
|
|
4135
|
+
: assessment.evidence.remediationEvents.map((event) => event.eventId),
|
|
4136
|
+
};
|
|
4137
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, compatibility);
|
|
4138
|
+
}
|
|
3248
4139
|
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
3249
4140
|
const snapshot = this.observationSnapshot(repository);
|
|
3250
4141
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -3299,6 +4190,9 @@ export class WorkflowService {
|
|
|
3299
4190
|
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3300
4191
|
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
3301
4192
|
}
|
|
4193
|
+
recoverPlanIntegrityDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
4194
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'plan-integrity');
|
|
4195
|
+
}
|
|
3302
4196
|
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
3303
4197
|
const normalizedActor = actor.trim();
|
|
3304
4198
|
const normalizedReason = reason.trim();
|
|
@@ -3315,7 +4209,9 @@ export class WorkflowService {
|
|
|
3315
4209
|
? Boolean(record.downstreamProofReplan)
|
|
3316
4210
|
: recoveryMode === 'historical-step'
|
|
3317
4211
|
? Boolean(record.historicalStepProvenance)
|
|
3318
|
-
:
|
|
4212
|
+
: recoveryMode === 'plan-integrity'
|
|
4213
|
+
? Boolean(record.planIntegrity)
|
|
4214
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
3319
4215
|
if (prior && current.systemCommits.includes(currentHead))
|
|
3320
4216
|
return current;
|
|
3321
4217
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -3327,42 +4223,113 @@ export class WorkflowService {
|
|
|
3327
4223
|
? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
|
|
3328
4224
|
: recoveryMode === 'historical-step'
|
|
3329
4225
|
? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
|
|
3330
|
-
: recoveryMode
|
|
3331
|
-
? this.
|
|
3332
|
-
:
|
|
4226
|
+
: recoveryMode === 'plan-integrity'
|
|
4227
|
+
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
4228
|
+
: recoveryMode
|
|
4229
|
+
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4230
|
+
: this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3333
4231
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
3334
4232
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
3335
4233
|
taskId,
|
|
3336
4234
|
blockers: preflight.blockers,
|
|
3337
4235
|
});
|
|
3338
4236
|
}
|
|
3339
|
-
|
|
4237
|
+
const now = this.now();
|
|
4238
|
+
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
4239
|
+
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
4240
|
+
return {
|
|
4241
|
+
commitSha: dependencyCommit.commitSha,
|
|
4242
|
+
parentCommitSha: dependencyCommit.parentCommitSha,
|
|
4243
|
+
packageName: PACKAGE_NAME,
|
|
4244
|
+
packageVersion: dependencyCommit.packageVersion,
|
|
4245
|
+
files: ['package-lock.json', 'package.json'],
|
|
4246
|
+
actor: normalizedActor,
|
|
4247
|
+
reason: normalizedReason,
|
|
4248
|
+
recordedAt: now.toISOString(),
|
|
4249
|
+
retainedHistoricalCommits: isTerminalCommit ? preflight.retainedHistoricalCommits : [],
|
|
4250
|
+
...(isTerminalCommit && preflight.activeDownstreamProof
|
|
4251
|
+
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
4252
|
+
: {}),
|
|
4253
|
+
...(isTerminalCommit && preflight.downstreamProofReplan
|
|
4254
|
+
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
4255
|
+
: {}),
|
|
4256
|
+
...(isTerminalCommit && preflight.historicalStepProvenance
|
|
4257
|
+
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
4258
|
+
: {}),
|
|
4259
|
+
...(isTerminalCommit && preflight.planIntegrity
|
|
4260
|
+
? { planIntegrity: preflight.planIntegrity }
|
|
4261
|
+
: {}),
|
|
4262
|
+
};
|
|
4263
|
+
});
|
|
4264
|
+
const nextTask = {
|
|
3340
4265
|
...task,
|
|
3341
|
-
|
|
4266
|
+
revision: task.revision + 1,
|
|
4267
|
+
updatedAt: now.toISOString(),
|
|
4268
|
+
systemCommits: [
|
|
4269
|
+
...task.systemCommits,
|
|
4270
|
+
...preflight.dependencyCommits.map((dependencyCommit) => dependencyCommit.commitSha),
|
|
4271
|
+
],
|
|
3342
4272
|
dependencyProvenanceRecoveries: [
|
|
3343
4273
|
...(task.dependencyProvenanceRecoveries ?? []),
|
|
4274
|
+
...recoveredRecords,
|
|
4275
|
+
],
|
|
4276
|
+
};
|
|
4277
|
+
if (recoveryMode !== 'plan-integrity' || !preflight.planIntegrity) {
|
|
4278
|
+
return context.store.writeTask({ ...nextTask, revision: task.revision }, expectedRevision);
|
|
4279
|
+
}
|
|
4280
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(task.projectId);
|
|
4281
|
+
const scannedKnowledgeMap = scanProjectKnowledge(context.identity.repositoryRoot, task.projectId, now);
|
|
4282
|
+
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
4283
|
+
let refreshedKnowledgeMap = null;
|
|
4284
|
+
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
4285
|
+
const knowledgeRefresh = inspectDependencyKnowledgeRefresh(context.identity.repositoryRoot, task.baseBranch, currentKnowledgeMap, inspectedKnowledgeMap, preflight.planIntegrity.dirtyFiles);
|
|
4286
|
+
if (!knowledgeRefresh.eligible) {
|
|
4287
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity dependency recovery cannot bypass unrelated Project Knowledge drift.', {
|
|
4288
|
+
taskId,
|
|
4289
|
+
dependencyKnowledgeRefresh: knowledgeRefresh,
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
4293
|
+
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
4294
|
+
if (!approved.approval) {
|
|
4295
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh produced no approval evidence.');
|
|
4296
|
+
}
|
|
4297
|
+
refreshedKnowledgeMap = {
|
|
4298
|
+
...approved,
|
|
4299
|
+
revision: currentKnowledgeMap.revision + 2,
|
|
4300
|
+
createdAt: currentKnowledgeMap.createdAt,
|
|
4301
|
+
updatedAt: now.toISOString(),
|
|
4302
|
+
approval: { ...approved.approval, authorizationMode: 'corrective-derived' },
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
4306
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
4307
|
+
applyProjectTransaction({
|
|
4308
|
+
projectRoot,
|
|
4309
|
+
projectId: task.projectId,
|
|
4310
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
4311
|
+
kind: 'plan-integrity-dependency-recovery',
|
|
4312
|
+
now,
|
|
4313
|
+
targets: [
|
|
4314
|
+
...(refreshedKnowledgeMap
|
|
4315
|
+
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
4316
|
+
: []),
|
|
3344
4317
|
{
|
|
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
|
-
: {}),
|
|
4318
|
+
path: relative(path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')),
|
|
4319
|
+
content: `${JSON.stringify(nextTask, null, 2)}\n`,
|
|
3363
4320
|
},
|
|
3364
4321
|
],
|
|
3365
|
-
|
|
4322
|
+
validate: () => {
|
|
4323
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
4324
|
+
if (saved.revision !== nextTask.revision || saved.systemCommits.at(-1) !== preflight.commitSha) {
|
|
4325
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency recovery Task readback failed.', { taskId });
|
|
4326
|
+
}
|
|
4327
|
+
if (refreshedKnowledgeMap && this.store.readKnowledgeMap(task.projectId).revision !== refreshedKnowledgeMap.revision) {
|
|
4328
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity dependency Knowledge refresh readback failed.', { taskId });
|
|
4329
|
+
}
|
|
4330
|
+
},
|
|
4331
|
+
});
|
|
4332
|
+
return this.store.readTask(task.projectId, task.id);
|
|
3366
4333
|
}
|
|
3367
4334
|
milestoneProgress(repository, milestoneId) {
|
|
3368
4335
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -3619,7 +4586,9 @@ export class WorkflowService {
|
|
|
3619
4586
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
3620
4587
|
}
|
|
3621
4588
|
next(repository, taskId = null) {
|
|
3622
|
-
|
|
4589
|
+
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
4590
|
+
}
|
|
4591
|
+
nextFromStatus(repository, taskId, status) {
|
|
3623
4592
|
if (status.observation.kind === 'blocked') {
|
|
3624
4593
|
return {
|
|
3625
4594
|
scope: 'project',
|
|
@@ -3759,7 +4728,24 @@ export class WorkflowService {
|
|
|
3759
4728
|
...(dependency ? { dependency } : {}),
|
|
3760
4729
|
});
|
|
3761
4730
|
}
|
|
4731
|
+
const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
|
|
4732
|
+
if (milestone.status !== 'active')
|
|
4733
|
+
return [];
|
|
4734
|
+
const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
|
|
4735
|
+
.reverse()
|
|
4736
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
4737
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
4738
|
+
&& candidate.remediationEvidence?.remediationTaskId);
|
|
4739
|
+
if (!event?.remediationEvidence)
|
|
4740
|
+
return [];
|
|
4741
|
+
const task = tasksById.get(event.remediationEvidence.remediationTaskId);
|
|
4742
|
+
return task
|
|
4743
|
+
&& taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4744
|
+
? [task]
|
|
4745
|
+
: [];
|
|
4746
|
+
}).at(0) ?? null;
|
|
3762
4747
|
const activeTask = requestedTask
|
|
4748
|
+
?? prioritizedRemediationTask
|
|
3763
4749
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3764
4750
|
if (activeTask) {
|
|
3765
4751
|
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
@@ -3789,6 +4775,22 @@ export class WorkflowService {
|
|
|
3789
4775
|
},
|
|
3790
4776
|
});
|
|
3791
4777
|
}
|
|
4778
|
+
const planIntegrityDependencyRecovery = this.planIntegrityDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4779
|
+
if (planIntegrityDependencyRecovery.eligible) {
|
|
4780
|
+
return withAdoption({
|
|
4781
|
+
scope: 'task',
|
|
4782
|
+
id: activeTask.id,
|
|
4783
|
+
status: activeTask.status,
|
|
4784
|
+
action: 'update plan-integrity-dependency-recover',
|
|
4785
|
+
revision: activeTask.revision,
|
|
4786
|
+
dependencyProvenanceRecovery: planIntegrityDependencyRecovery,
|
|
4787
|
+
compatibilityBoundary: {
|
|
4788
|
+
preservesWorktree: true,
|
|
4789
|
+
prerequisite: 'A newer package preflight approved the exact failed Step, remediation history, and dirty allowlist before dependency-only transport.',
|
|
4790
|
+
continuation: 'Register this dependency commit, then follow fresh next to Plan-integrity recovery and corrective replan.',
|
|
4791
|
+
},
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
3792
4794
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3793
4795
|
if (dependencyRecovery.eligible) {
|
|
3794
4796
|
return withAdoption({
|
|
@@ -3915,41 +4917,132 @@ export class WorkflowService {
|
|
|
3915
4917
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3916
4918
|
throw error;
|
|
3917
4919
|
const blockingDecision = error.details.decision;
|
|
3918
|
-
|
|
4920
|
+
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
4921
|
+
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
4922
|
+
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
4923
|
+
.reverse()
|
|
4924
|
+
.find((event) => event.authorizationMode === 'audited-remediation'
|
|
4925
|
+
&& event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
|
|
4926
|
+
: null;
|
|
4927
|
+
const existingRemediationTask = existingRemediationEvent?.remediationEvidence
|
|
4928
|
+
? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
|
|
4929
|
+
: null;
|
|
4930
|
+
const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
|
|
4931
|
+
const remediationMilestone = activeTask.milestoneId
|
|
4932
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
4933
|
+
: null;
|
|
4934
|
+
const remediationMemberships = remediationMilestone
|
|
4935
|
+
? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
|
|
4936
|
+
: new Map();
|
|
4937
|
+
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
4938
|
+
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
4939
|
+
&& remediationMilestone?.status === 'active'
|
|
4940
|
+
&& remediationMembership?.disposition === 'required';
|
|
4941
|
+
const remediationAncestorIds = new Set();
|
|
4942
|
+
const visitRemediationAncestors = (candidateTaskId) => {
|
|
4943
|
+
for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
|
|
4944
|
+
if (remediationAncestorIds.has(predecessorTaskId))
|
|
4945
|
+
continue;
|
|
4946
|
+
remediationAncestorIds.add(predecessorTaskId);
|
|
4947
|
+
visitRemediationAncestors(predecessorTaskId);
|
|
4948
|
+
}
|
|
4949
|
+
};
|
|
4950
|
+
visitRemediationAncestors(activeTask.id);
|
|
4951
|
+
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
4952
|
+
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
4953
|
+
.sort();
|
|
4954
|
+
planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
3919
4955
|
? {
|
|
3920
4956
|
scope: 'task',
|
|
3921
4957
|
id: activeTask.id,
|
|
3922
4958
|
status: activeTask.status,
|
|
3923
|
-
action:
|
|
4959
|
+
action: existingRemediationTask?.status === 'merged'
|
|
4960
|
+
? 'task plan-set'
|
|
4961
|
+
: existingRemediationTask
|
|
4962
|
+
? 'doctor'
|
|
4963
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4964
|
+
? 'milestone remediation-materialize'
|
|
4965
|
+
: remediationDiscovery
|
|
4966
|
+
? 'discovery update'
|
|
4967
|
+
: 'discovery start',
|
|
3924
4968
|
blockedAction: 'task authorize',
|
|
3925
4969
|
revision: activeTask.revision,
|
|
3926
4970
|
planRiskAuditDecision: {
|
|
3927
4971
|
state: 'recorded-blocking',
|
|
3928
4972
|
decision: blockingDecision,
|
|
3929
4973
|
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.',
|
|
4974
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
3933
4975
|
},
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4976
|
+
auditedRemediation: {
|
|
4977
|
+
state: existingRemediationTask?.status === 'merged'
|
|
4978
|
+
? 'merged-replan-required'
|
|
4979
|
+
: existingRemediationTask
|
|
4980
|
+
? 'materialized-not-merged'
|
|
4981
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4982
|
+
? 'ready-to-materialize'
|
|
4983
|
+
: 'discovery-required',
|
|
4984
|
+
humanApprovalRequired: false,
|
|
4985
|
+
blockedTaskId: activeTask.id,
|
|
4986
|
+
blockedTaskRevision: activeTask.revision,
|
|
4987
|
+
milestoneId: remediationMilestone.id,
|
|
4988
|
+
milestoneRevision: remediationMilestone.revision,
|
|
4989
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
4990
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
4991
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
4992
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
4993
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
4994
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
4995
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
4996
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
4997
|
+
command: existingRemediationTask?.status === 'merged'
|
|
4998
|
+
? 'task plan-set'
|
|
4999
|
+
: existingRemediationTask
|
|
5000
|
+
? 'doctor'
|
|
5001
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5002
|
+
? 'milestone remediation-materialize'
|
|
5003
|
+
: remediationDiscovery
|
|
5004
|
+
? 'discovery update'
|
|
5005
|
+
: 'discovery start',
|
|
5006
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5007
|
+
preservesMilestoneSemantics: true,
|
|
3951
5008
|
},
|
|
3952
|
-
|
|
5009
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5010
|
+
}
|
|
5011
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5012
|
+
? {
|
|
5013
|
+
scope: 'task',
|
|
5014
|
+
id: activeTask.id,
|
|
5015
|
+
status: activeTask.status,
|
|
5016
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5017
|
+
blockedAction: 'task authorize',
|
|
5018
|
+
revision: activeTask.revision,
|
|
5019
|
+
planRiskAuditDecision: {
|
|
5020
|
+
state: 'recorded-blocking',
|
|
5021
|
+
decision: blockingDecision,
|
|
5022
|
+
reason: error.message,
|
|
5023
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
5024
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5025
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5026
|
+
},
|
|
5027
|
+
...(blockingDecision === 'stop-escalate'
|
|
5028
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
5029
|
+
: {}),
|
|
5030
|
+
}
|
|
5031
|
+
: {
|
|
5032
|
+
scope: 'task',
|
|
5033
|
+
id: activeTask.id,
|
|
5034
|
+
status: activeTask.status,
|
|
5035
|
+
action: 'task plan-risk-audit',
|
|
5036
|
+
revision: activeTask.revision,
|
|
5037
|
+
planRiskAuditRecovery: {
|
|
5038
|
+
state: 'required',
|
|
5039
|
+
reason: error.message,
|
|
5040
|
+
currentBriefHash: activeTask.briefHash,
|
|
5041
|
+
currentPlanHash: activeTask.planHash,
|
|
5042
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
5043
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
5044
|
+
},
|
|
5045
|
+
};
|
|
3953
5046
|
}
|
|
3954
5047
|
}
|
|
3955
5048
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -3958,6 +5051,37 @@ export class WorkflowService {
|
|
|
3958
5051
|
? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
|
|
3959
5052
|
: planRiskAuditRecovery
|
|
3960
5053
|
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
5054
|
+
const auditedRemediationNavigation = taskNext?.auditedRemediation;
|
|
5055
|
+
let auditedDependencyKnowledgeRefreshBlocked = null;
|
|
5056
|
+
if (auditedRemediationNavigation
|
|
5057
|
+
&& activeTaskMilestone
|
|
5058
|
+
&& ['discovery start', 'discovery update', 'milestone remediation-materialize'].includes(String(taskNext?.action))) {
|
|
5059
|
+
try {
|
|
5060
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
|
|
5061
|
+
const inspectedKnowledgeMap = inspectCurrentKnowledgeMap();
|
|
5062
|
+
if (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval) {
|
|
5063
|
+
const dependencyKnowledgeRefresh = inspectDependencyKnowledgeRefresh(repositoryRoot, activeTaskMilestone.baseBranch, storedKnowledgeMap, inspectedKnowledgeMap);
|
|
5064
|
+
if (dependencyKnowledgeRefresh.eligible) {
|
|
5065
|
+
return withAdoption(withMilestoneCancellation({
|
|
5066
|
+
...taskNext,
|
|
5067
|
+
auditedRemediation: {
|
|
5068
|
+
...auditedRemediationNavigation,
|
|
5069
|
+
dependencyKnowledgeRefresh: {
|
|
5070
|
+
...dependencyKnowledgeRefresh,
|
|
5071
|
+
humanApprovalRequired: false,
|
|
5072
|
+
application: 'atomic-with-remediation-materialization',
|
|
5073
|
+
},
|
|
5074
|
+
},
|
|
5075
|
+
}));
|
|
5076
|
+
}
|
|
5077
|
+
auditedDependencyKnowledgeRefreshBlocked = dependencyKnowledgeRefresh;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
catch (error) {
|
|
5081
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
5082
|
+
throw error;
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
3961
5085
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3962
5086
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3963
5087
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3967,8 +5091,12 @@ export class WorkflowService {
|
|
|
3967
5091
|
|| taskNext?.action === 'task step-review'
|
|
3968
5092
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
3969
5093
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
5094
|
+
|| (taskNext?.action === 'task writer-credential-replace'
|
|
5095
|
+
&& taskNext?.blockedAction === 'task remediation-mode-recover')
|
|
3970
5096
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3971
5097
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
5098
|
+
|| taskNext?.action === 'task corrective-yield'
|
|
5099
|
+
|| String(taskNext?.action ?? '').startsWith('task corrective-replan')
|
|
3972
5100
|
|| hardStop
|
|
3973
5101
|
|| terminalCorrectiveDecision === 'split-required'
|
|
3974
5102
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
@@ -4005,8 +5133,10 @@ export class WorkflowService {
|
|
|
4005
5133
|
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
4006
5134
|
: null)
|
|
4007
5135
|
: null;
|
|
5136
|
+
const rollingRootCausePlanSet = taskNext?.action === 'task plan-set'
|
|
5137
|
+
&& taskNext.rollingWave?.state === 'root-cause-replan-required';
|
|
4008
5138
|
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4009
|
-
&& taskNext?.action !== 'task plan-set'
|
|
5139
|
+
&& (taskNext?.action !== 'task plan-set' || rollingRootCausePlanSet)
|
|
4010
5140
|
&& refreshAssessment?.eligible
|
|
4011
5141
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
4012
5142
|
: [];
|
|
@@ -4052,6 +5182,15 @@ export class WorkflowService {
|
|
|
4052
5182
|
},
|
|
4053
5183
|
}
|
|
4054
5184
|
: {}),
|
|
5185
|
+
...(auditedDependencyKnowledgeRefreshBlocked
|
|
5186
|
+
? {
|
|
5187
|
+
auditedDependencyKnowledgeRefreshBlocked: {
|
|
5188
|
+
...auditedDependencyKnowledgeRefreshBlocked,
|
|
5189
|
+
humanApprovalRequired: true,
|
|
5190
|
+
requiredAction: 'Resolve or explicitly approve the non-dependency Project Knowledge drift.',
|
|
5191
|
+
},
|
|
5192
|
+
}
|
|
5193
|
+
: {}),
|
|
4055
5194
|
}));
|
|
4056
5195
|
}
|
|
4057
5196
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -4358,6 +5497,32 @@ export class WorkflowService {
|
|
|
4358
5497
|
milestonePlanHash: milestone.planHash,
|
|
4359
5498
|
};
|
|
4360
5499
|
}
|
|
5500
|
+
currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
|
|
5501
|
+
if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
|
|
5502
|
+
return null;
|
|
5503
|
+
const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
|
|
5504
|
+
if (milestone.status !== 'active' || !milestone.planHash)
|
|
5505
|
+
return null;
|
|
5506
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
|
|
5507
|
+
if (membership?.disposition !== 'required')
|
|
5508
|
+
return null;
|
|
5509
|
+
const authorization = [...milestone.authorizations]
|
|
5510
|
+
.reverse()
|
|
5511
|
+
.find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
|
|
5512
|
+
if (authorization?.authorizationMode !== 'corrective-derived'
|
|
5513
|
+
|| authorization.planHash !== milestone.planHash
|
|
5514
|
+
|| authorization.actor !== actor.trim())
|
|
5515
|
+
return null;
|
|
5516
|
+
const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
|
|
5517
|
+
.reverse()
|
|
5518
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
5519
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
5520
|
+
&& candidate.remediationEvidence !== null
|
|
5521
|
+
&& candidate.remediationEvidence !== undefined);
|
|
5522
|
+
if (!scopeChange)
|
|
5523
|
+
return null;
|
|
5524
|
+
return { milestone, authorization, scopeChange };
|
|
5525
|
+
}
|
|
4361
5526
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
4362
5527
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
4363
5528
|
? 'task.execution_authorize'
|
|
@@ -4367,6 +5532,12 @@ export class WorkflowService {
|
|
|
4367
5532
|
const delegatedApprovalOptions = transition
|
|
4368
5533
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
4369
5534
|
: [];
|
|
5535
|
+
const correctiveDerivedActor = task.milestoneId
|
|
5536
|
+
? `agent:milestone-remediation:${task.milestoneId}`
|
|
5537
|
+
: null;
|
|
5538
|
+
const correctiveDerivedAuthority = transition && correctiveDerivedActor
|
|
5539
|
+
? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
|
|
5540
|
+
: null;
|
|
4370
5541
|
let remediationModeRecovery = null;
|
|
4371
5542
|
let guardedCorrectivePosture = null;
|
|
4372
5543
|
if (planRiskAudit) {
|
|
@@ -4374,7 +5545,7 @@ export class WorkflowService {
|
|
|
4374
5545
|
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
4375
5546
|
}
|
|
4376
5547
|
catch (error) {
|
|
4377
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5548
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4378
5549
|
if (!remediationModeRecovery)
|
|
4379
5550
|
throw error;
|
|
4380
5551
|
}
|
|
@@ -4387,7 +5558,7 @@ export class WorkflowService {
|
|
|
4387
5558
|
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
4388
5559
|
}
|
|
4389
5560
|
catch (error) {
|
|
4390
|
-
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
5561
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task, { authorityMode: 'observe' });
|
|
4391
5562
|
if (!remediationModeRecovery)
|
|
4392
5563
|
throw error;
|
|
4393
5564
|
}
|
|
@@ -4399,6 +5570,11 @@ export class WorkflowService {
|
|
|
4399
5570
|
&& changedFiles(repositoryRoot).length > 0
|
|
4400
5571
|
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
4401
5572
|
: null;
|
|
5573
|
+
const planIntegrityDirtyCarryover = next.action === 'task run'
|
|
5574
|
+
&& nextStepId
|
|
5575
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
5576
|
+
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
5577
|
+
: null;
|
|
4402
5578
|
let mechanicalFeasibilityOverlay = {};
|
|
4403
5579
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
4404
5580
|
try {
|
|
@@ -4449,6 +5625,9 @@ export class WorkflowService {
|
|
|
4449
5625
|
}
|
|
4450
5626
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
4451
5627
|
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
5628
|
+
const failedStepRemediations = failedStep && !remediationModeRecovery
|
|
5629
|
+
? readRemediationEvents(this.store, task).filter((event) => event.stepId === failedStep.id)
|
|
5630
|
+
: [];
|
|
4452
5631
|
const failedStepReviewRouting = failedStep
|
|
4453
5632
|
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
4454
5633
|
: null;
|
|
@@ -4579,11 +5758,21 @@ export class WorkflowService {
|
|
|
4579
5758
|
stepId: planIntegrityAssessment.stepId,
|
|
4580
5759
|
planIntegrityRecovery: {
|
|
4581
5760
|
state: 'eligible',
|
|
4582
|
-
reasonCode: '
|
|
5761
|
+
reasonCode: planIntegrityAssessment.evidence?.kind === 'missing-npm-script-outside-allowed-writes'
|
|
5762
|
+
? 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES'
|
|
5763
|
+
: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
5764
|
+
? 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES'
|
|
5765
|
+
: planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
5766
|
+
? 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES'
|
|
5767
|
+
: 'NON_EXECUTABLE_NPM_CHECK_OUTSIDE_ALLOWED_WRITES',
|
|
4583
5768
|
expectedRevision: task.revision,
|
|
4584
5769
|
requiredActor,
|
|
4585
5770
|
evidence: planIntegrityAssessment.evidence,
|
|
4586
|
-
boundedEffect: '
|
|
5771
|
+
boundedEffect: planIntegrityAssessment.evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
5772
|
+
? 'record-machine-derived-check-support-scope'
|
|
5773
|
+
: planIntegrityAssessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
|
|
5774
|
+
? 'record-machine-derived-component-owner-scope'
|
|
5775
|
+
: 'record-replan-required-decision',
|
|
4587
5776
|
preservesWorktree: true,
|
|
4588
5777
|
},
|
|
4589
5778
|
}
|
|
@@ -4716,7 +5905,48 @@ export class WorkflowService {
|
|
|
4716
5905
|
};
|
|
4717
5906
|
break;
|
|
4718
5907
|
case 'replan-required':
|
|
4719
|
-
guardedCorrectiveOverlay =
|
|
5908
|
+
guardedCorrectiveOverlay = guardedCorrectivePosture.correctiveDecision
|
|
5909
|
+
? correctiveReplanOverlay(guardedCorrectivePosture)
|
|
5910
|
+
: (() => {
|
|
5911
|
+
const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
5912
|
+
const rollingC1Posture = readTaskC1Posture(this.store, task);
|
|
5913
|
+
const blockers = [
|
|
5914
|
+
...(!['needs_fix', 'ready'].includes(task.status)
|
|
5915
|
+
? [`Task must be needs_fix or context-refreshed ready, not ${task.status}.`]
|
|
5916
|
+
: []),
|
|
5917
|
+
...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may remain in progress.'] : []),
|
|
5918
|
+
...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
|
|
5919
|
+
? [`${guardedCorrectivePosture.stepId} must remain failed.`]
|
|
5920
|
+
: []),
|
|
5921
|
+
...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
5922
|
+
...(rollingC1Posture.state !== 'none'
|
|
5923
|
+
? ['No C1 claim may remain after the root-cause failure pause.']
|
|
5924
|
+
: []),
|
|
5925
|
+
];
|
|
5926
|
+
return blockers.length > 0
|
|
5927
|
+
? {
|
|
5928
|
+
action: 'doctor',
|
|
5929
|
+
blockedAction: 'task plan-set',
|
|
5930
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5931
|
+
rollingWave: {
|
|
5932
|
+
state: 'root-cause-replan-blocked',
|
|
5933
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5934
|
+
humanApprovalRequired: false,
|
|
5935
|
+
blockers,
|
|
5936
|
+
},
|
|
5937
|
+
}
|
|
5938
|
+
: {
|
|
5939
|
+
action: 'task plan-set',
|
|
5940
|
+
blockedAction: 'task run',
|
|
5941
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5942
|
+
rollingWave: {
|
|
5943
|
+
state: 'root-cause-replan-required',
|
|
5944
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5945
|
+
humanApprovalRequired: false,
|
|
5946
|
+
boundary: 'Task objective, requirements, acceptance, and security semantics remain unchanged.',
|
|
5947
|
+
},
|
|
5948
|
+
};
|
|
5949
|
+
})();
|
|
4720
5950
|
break;
|
|
4721
5951
|
case 'split-required':
|
|
4722
5952
|
{
|
|
@@ -4874,6 +6104,11 @@ export class WorkflowService {
|
|
|
4874
6104
|
externalSealedReview: {
|
|
4875
6105
|
packetCommand: 'task step-review-packet',
|
|
4876
6106
|
recordCommand: 'task step-review-record',
|
|
6107
|
+
reviewMode: 'ordinary',
|
|
6108
|
+
allowedReviewModes: ['ordinary', 'security'],
|
|
6109
|
+
packetReviewModeOption: '--review-mode',
|
|
6110
|
+
implicitSecurityScan: false,
|
|
6111
|
+
reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${strictStepReviewStep.id}:${strictStepReviewStep.evidence.commitSha}:ordinary`),
|
|
4877
6112
|
reviewerMustUseSeparateChat: true,
|
|
4878
6113
|
repositorySealRequired: true,
|
|
4879
6114
|
},
|
|
@@ -4889,6 +6124,11 @@ export class WorkflowService {
|
|
|
4889
6124
|
externalSealedReview: {
|
|
4890
6125
|
packetCommand: 'task review-packet',
|
|
4891
6126
|
recordCommand: 'task review-sealed-record',
|
|
6127
|
+
reviewMode: 'ordinary',
|
|
6128
|
+
allowedReviewModes: ['ordinary', 'security'],
|
|
6129
|
+
packetReviewModeOption: '--review-mode',
|
|
6130
|
+
implicitSecurityScan: false,
|
|
6131
|
+
reviewerRuntime: prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${headCommit(repositoryRoot)}:ordinary`),
|
|
4892
6132
|
reviewerMustUseSeparateChat: true,
|
|
4893
6133
|
repositorySealRequired: true,
|
|
4894
6134
|
},
|
|
@@ -4907,7 +6147,11 @@ export class WorkflowService {
|
|
|
4907
6147
|
: {}),
|
|
4908
6148
|
...guardedCorrectiveOverlay,
|
|
4909
6149
|
...downstreamProofOverlay,
|
|
4910
|
-
...(failedStepReviewRouting?.route === 'replan'
|
|
6150
|
+
...(failedStepReviewRouting?.route === 'replan'
|
|
6151
|
+
&& next.action === 'task run'
|
|
6152
|
+
&& (!failedStep
|
|
6153
|
+
|| deriveRollingCauseDecision(failedStepRemediations) === null
|
|
6154
|
+
|| failedStepRemediations.at(-1)?.causeBindings?.length === 0)
|
|
4911
6155
|
? {
|
|
4912
6156
|
action: 'task plan-set',
|
|
4913
6157
|
blockedAction: 'task run',
|
|
@@ -4944,7 +6188,44 @@ export class WorkflowService {
|
|
|
4944
6188
|
},
|
|
4945
6189
|
}
|
|
4946
6190
|
: {}),
|
|
6191
|
+
...(planIntegrityDirtyCarryover?.applicable
|
|
6192
|
+
? planIntegrityDirtyCarryover.eligible
|
|
6193
|
+
? {
|
|
6194
|
+
planIntegrityDirtyCarryover: {
|
|
6195
|
+
state: 'validated',
|
|
6196
|
+
stepId: nextStepId,
|
|
6197
|
+
dependencyRecoveryCommit: planIntegrityDirtyCarryover.dependencyRecoveryCommit,
|
|
6198
|
+
dirtyFiles: planIntegrityDirtyCarryover.compatibility?.dirtyFiles ?? [],
|
|
6199
|
+
dirtyWorktreeHash: planIntegrityDirtyCarryover.compatibility?.dirtyWorktreeHash ?? null,
|
|
6200
|
+
scopeExtensions: planIntegrityDirtyCarryover.compatibility?.scopeExtensions ?? [],
|
|
6201
|
+
meaning: 'The current Step may adopt exactly these hash-bound check-support bytes on task run.',
|
|
6202
|
+
},
|
|
6203
|
+
}
|
|
6204
|
+
: {
|
|
6205
|
+
action: 'doctor',
|
|
6206
|
+
blockedAction: 'task run',
|
|
6207
|
+
planIntegrityDirtyCarryover: {
|
|
6208
|
+
state: 'blocked',
|
|
6209
|
+
stepId: nextStepId,
|
|
6210
|
+
blockers: planIntegrityDirtyCarryover.blockers,
|
|
6211
|
+
},
|
|
6212
|
+
}
|
|
6213
|
+
: {}),
|
|
4947
6214
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6215
|
+
...(correctiveDerivedAuthority && correctiveDerivedActor
|
|
6216
|
+
? {
|
|
6217
|
+
correctiveDerivedApproval: {
|
|
6218
|
+
action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
|
|
6219
|
+
actor: correctiveDerivedActor,
|
|
6220
|
+
humanApprovalRequired: false,
|
|
6221
|
+
milestoneId: correctiveDerivedAuthority.milestone.id,
|
|
6222
|
+
milestoneRevision: correctiveDerivedAuthority.milestone.revision,
|
|
6223
|
+
milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
|
|
6224
|
+
planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
|
|
6225
|
+
authority: 'current-corrective-derived-milestone-execution-authorization',
|
|
6226
|
+
},
|
|
6227
|
+
}
|
|
6228
|
+
: {}),
|
|
4948
6229
|
};
|
|
4949
6230
|
if (handoffPosture.state === 'pending') {
|
|
4950
6231
|
return this.withTaskNavigationContracts(task, {
|
|
@@ -5003,7 +6284,20 @@ export class WorkflowService {
|
|
|
5003
6284
|
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
5004
6285
|
}
|
|
5005
6286
|
taskHasTerminalCorrectiveDecision(task) {
|
|
5006
|
-
|
|
6287
|
+
try {
|
|
6288
|
+
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
6289
|
+
}
|
|
6290
|
+
catch (error) {
|
|
6291
|
+
if (error instanceof WorkflowError
|
|
6292
|
+
&& error.code === 'TRANSITION_BLOCKED'
|
|
6293
|
+
&& (error.details.decision === 'stop-escalate' || error.details.decision === 'split-required')) {
|
|
6294
|
+
// A blocking Plan Risk Audit is not a guarded-Step terminal decision, but it
|
|
6295
|
+
// equally cannot own an atomic delegated context refresh. Treat it as
|
|
6296
|
+
// ineligible here so standalone Knowledge reconciliation remains executable.
|
|
6297
|
+
return true;
|
|
6298
|
+
}
|
|
6299
|
+
throw error;
|
|
6300
|
+
}
|
|
5007
6301
|
}
|
|
5008
6302
|
taskTerminalCorrectivePosture(task) {
|
|
5009
6303
|
if (!task.planHash)
|
|
@@ -5166,6 +6460,10 @@ export class WorkflowService {
|
|
|
5166
6460
|
statePlanHash: task.planHash,
|
|
5167
6461
|
});
|
|
5168
6462
|
}
|
|
6463
|
+
if (authorization.authorizationMode === 'corrective-derived'
|
|
6464
|
+
&& !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
|
|
6465
|
+
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 });
|
|
6466
|
+
}
|
|
5169
6467
|
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
5170
6468
|
if (!mechanicalFeasibility)
|
|
5171
6469
|
return authorization;
|
|
@@ -5237,7 +6535,218 @@ export class WorkflowService {
|
|
|
5237
6535
|
},
|
|
5238
6536
|
};
|
|
5239
6537
|
}
|
|
5240
|
-
|
|
6538
|
+
carriedPlanIntegrityUpdateCompatibility(snapshot, task) {
|
|
6539
|
+
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
6540
|
+
let dependencyCommitCount = 0;
|
|
6541
|
+
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
6542
|
+
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
6543
|
+
if (!dependencyCommit)
|
|
6544
|
+
return null;
|
|
6545
|
+
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
6546
|
+
dependencyCommitCount += 1;
|
|
6547
|
+
}
|
|
6548
|
+
if (dependencyCommitCount === 0)
|
|
6549
|
+
return null;
|
|
6550
|
+
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
6551
|
+
const candidates = (task.dependencyProvenanceRecoveries ?? []).filter((record) => {
|
|
6552
|
+
const compatibility = record.planIntegrity;
|
|
6553
|
+
if (!compatibility
|
|
6554
|
+
|| record.commitSha !== boundaryCommit
|
|
6555
|
+
|| !task.systemCommits.includes(record.commitSha)
|
|
6556
|
+
|| compatibility.planHash !== task.planHash
|
|
6557
|
+
|| canonicalJsonStringify([...compatibility.dirtyFiles].sort()) !== canonicalJsonStringify(dirtyFiles)
|
|
6558
|
+
|| compatibility.dirtyWorktreeHash !== hashDirtyWorktree(snapshot.identity.repositoryRoot, compatibility.dirtyFiles))
|
|
6559
|
+
return false;
|
|
6560
|
+
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId);
|
|
6561
|
+
if (!step || step.status !== 'failed')
|
|
6562
|
+
return false;
|
|
6563
|
+
const remediationEventIds = readRemediationEvents(this.store, task)
|
|
6564
|
+
.filter((event) => event.stepId === compatibility.stepId)
|
|
6565
|
+
.map((event) => event.eventId);
|
|
6566
|
+
if (canonicalJsonStringify(compatibility.remediationEventIds) !== canonicalJsonStringify(remediationEventIds))
|
|
6567
|
+
return false;
|
|
6568
|
+
return readCorrectiveDecisionEvents(this.store, task).some((event) => event.planHash === task.planHash
|
|
6569
|
+
&& event.stepId === compatibility.stepId
|
|
6570
|
+
&& event.decision === 'replan-required')
|
|
6571
|
+
&& readTaskC1Posture(this.store, task).state === 'none';
|
|
6572
|
+
});
|
|
6573
|
+
return candidates.length === 1 ? structuredClone(candidates[0].planIntegrity) : null;
|
|
6574
|
+
}
|
|
6575
|
+
postRebindCheckSupportCompatibility(snapshot, task) {
|
|
6576
|
+
if (task.status !== 'awaiting_execution_authorization'
|
|
6577
|
+
|| !task.planHash
|
|
6578
|
+
|| task.steps.some((step) => step.status === 'in_progress')
|
|
6579
|
+
|| readTaskC1Posture(this.store, task).state !== 'none')
|
|
6580
|
+
return null;
|
|
6581
|
+
const dirtyFiles = changedFiles(snapshot.identity.repositoryRoot).sort();
|
|
6582
|
+
const candidates = readCorrectiveDecisionEvents(this.store, task).flatMap((event) => {
|
|
6583
|
+
const evidence = event.planIntegrityEvidence;
|
|
6584
|
+
if (event.planHash === task.planHash
|
|
6585
|
+
|| event.decision !== 'continue-fix'
|
|
6586
|
+
|| event.recoveryBasis !== 'plan-integrity'
|
|
6587
|
+
|| evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
|
|
6588
|
+
|| !task.systemCommits.includes(evidence.gitHead)
|
|
6589
|
+
|| canonicalJsonStringify([...evidence.changedFiles].sort()) !== canonicalJsonStringify(dirtyFiles)) {
|
|
6590
|
+
return [];
|
|
6591
|
+
}
|
|
6592
|
+
const step = task.steps.find((candidate) => candidate.id === event.stepId) ?? null;
|
|
6593
|
+
if (!step
|
|
6594
|
+
|| step.status !== 'planned'
|
|
6595
|
+
|| evidence.scopeExtensions.some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
|
|
6596
|
+
|| !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath)))
|
|
6597
|
+
return [];
|
|
6598
|
+
return [{
|
|
6599
|
+
stepId: step.id,
|
|
6600
|
+
planHash: task.planHash,
|
|
6601
|
+
conflictHash: evidence.conflictHash,
|
|
6602
|
+
dirtyFiles: [...dirtyFiles],
|
|
6603
|
+
dirtyWorktreeHash: hashDirtyWorktree(snapshot.identity.repositoryRoot, dirtyFiles),
|
|
6604
|
+
remediationEventIds: evidence.remediationEvents.map((binding) => binding.eventId),
|
|
6605
|
+
mode: 'post-rebind-scope-adoption',
|
|
6606
|
+
scopeExtensions: [...evidence.scopeExtensions],
|
|
6607
|
+
}];
|
|
6608
|
+
});
|
|
6609
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
6610
|
+
}
|
|
6611
|
+
carriedPostRebindCheckSupportDirtyCarryover(snapshot, task) {
|
|
6612
|
+
let boundaryCommit = headCommit(snapshot.identity.repositoryRoot);
|
|
6613
|
+
let dependencyCommitCount = 0;
|
|
6614
|
+
while (!task.systemCommits.includes(boundaryCommit)) {
|
|
6615
|
+
const dependencyCommit = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, boundaryCommit);
|
|
6616
|
+
if (!dependencyCommit)
|
|
6617
|
+
return null;
|
|
6618
|
+
boundaryCommit = dependencyCommit.parentCommitSha;
|
|
6619
|
+
dependencyCommitCount += 1;
|
|
6620
|
+
}
|
|
6621
|
+
if (dependencyCommitCount === 0)
|
|
6622
|
+
return null;
|
|
6623
|
+
const assessment = this.assessPostRebindCheckSupportDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, boundaryCommit);
|
|
6624
|
+
if (!assessment.eligible || !assessment.compatibility || !task.planHash)
|
|
6625
|
+
return null;
|
|
6626
|
+
return {
|
|
6627
|
+
...assessment.compatibility,
|
|
6628
|
+
planHash: task.planHash,
|
|
6629
|
+
mode: 'post-rebind-dirty-carryover',
|
|
6630
|
+
};
|
|
6631
|
+
}
|
|
6632
|
+
assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, stepId, historyBoundary = headCommit(repositoryRoot)) {
|
|
6633
|
+
const relevantRecords = [...(task.dependencyProvenanceRecoveries ?? [])]
|
|
6634
|
+
.reverse()
|
|
6635
|
+
.filter((record) => {
|
|
6636
|
+
const compatibility = record.planIntegrity;
|
|
6637
|
+
return compatibility
|
|
6638
|
+
&& ['post-rebind-scope-adoption', 'post-rebind-dirty-carryover'].includes(compatibility.mode ?? '')
|
|
6639
|
+
&& (!stepId || compatibility.stepId === stepId);
|
|
6640
|
+
});
|
|
6641
|
+
if (relevantRecords.length === 0) {
|
|
6642
|
+
return {
|
|
6643
|
+
applicable: false,
|
|
6644
|
+
eligible: false,
|
|
6645
|
+
compatibility: null,
|
|
6646
|
+
dependencyRecoveryCommit: null,
|
|
6647
|
+
blockers: [],
|
|
6648
|
+
};
|
|
6649
|
+
}
|
|
6650
|
+
const blockers = [];
|
|
6651
|
+
const record = relevantRecords[0];
|
|
6652
|
+
const compatibility = record.planIntegrity;
|
|
6653
|
+
const step = task.steps.find((candidate) => candidate.id === compatibility.stepId) ?? null;
|
|
6654
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
6655
|
+
let dirtyWorktreeHash = null;
|
|
6656
|
+
try {
|
|
6657
|
+
dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
6658
|
+
}
|
|
6659
|
+
catch (error) {
|
|
6660
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
6661
|
+
}
|
|
6662
|
+
if (record.commitSha !== historyBoundary || !task.systemCommits.includes(record.commitSha)) {
|
|
6663
|
+
blockers.push('The latest check-support compatibility record must bind the exact registered history boundary.');
|
|
6664
|
+
}
|
|
6665
|
+
if (task.workspaceOwner !== 'local') {
|
|
6666
|
+
blockers.push('Check-support dirty carryover requires the recorded local Task workspace.');
|
|
6667
|
+
}
|
|
6668
|
+
if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
|
|
6669
|
+
blockers.push('Current branch must equal the recorded Task branch.');
|
|
6670
|
+
}
|
|
6671
|
+
if (!['ready', 'in_progress'].includes(task.status)) {
|
|
6672
|
+
blockers.push('The Task must retain an execution-authorized posture before dirty carryover.');
|
|
6673
|
+
}
|
|
6674
|
+
if (!task.planHash) {
|
|
6675
|
+
blockers.push('The Task must retain a current Plan hash.');
|
|
6676
|
+
}
|
|
6677
|
+
if (!step || step.status !== 'planned' || step.evidence !== null) {
|
|
6678
|
+
blockers.push(`The bound Step ${compatibility.stepId} must remain planned with no evidence.`);
|
|
6679
|
+
}
|
|
6680
|
+
if (task.steps.some((candidate) => candidate.status === 'in_progress')) {
|
|
6681
|
+
blockers.push('No Step may already be in progress before dirty carryover.');
|
|
6682
|
+
}
|
|
6683
|
+
if (canonicalJsonStringify(dirtyFiles)
|
|
6684
|
+
!== canonicalJsonStringify([...compatibility.dirtyFiles].sort())) {
|
|
6685
|
+
blockers.push('Dirty files changed after the registered check-support compatibility boundary.');
|
|
6686
|
+
}
|
|
6687
|
+
if (dirtyWorktreeHash !== compatibility.dirtyWorktreeHash) {
|
|
6688
|
+
blockers.push('Dirty file content changed after the registered check-support compatibility boundary.');
|
|
6689
|
+
}
|
|
6690
|
+
if (!compatibility.scopeExtensions?.length) {
|
|
6691
|
+
blockers.push('The compatibility record must retain at least one machine-derived scope extension.');
|
|
6692
|
+
}
|
|
6693
|
+
if (step) {
|
|
6694
|
+
if (dirtyFiles.some((dirtyPath) => !pathAllowed(dirtyPath, step.allowedWrites))) {
|
|
6695
|
+
blockers.push('The current Step must explicitly own every preserved dirty file.');
|
|
6696
|
+
}
|
|
6697
|
+
if ((compatibility.scopeExtensions ?? []).some((scopePath) => !pathAllowed(scopePath, step.allowedWrites)
|
|
6698
|
+
|| !step.expectedOutputs.some((declared) => (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
|
|
6699
|
+
blockers.push('The current Step must explicitly own every check-support scope extension as allowedWrites and expectedOutputs.');
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
let reboundPlanHash = compatibility.planHash;
|
|
6703
|
+
for (const rebind of task.knowledgeRebinds) {
|
|
6704
|
+
if (rebind.previousPlanHash === reboundPlanHash)
|
|
6705
|
+
reboundPlanHash = rebind.reboundPlanHash;
|
|
6706
|
+
}
|
|
6707
|
+
if (!task.planHash || reboundPlanHash !== task.planHash) {
|
|
6708
|
+
blockers.push('The current Plan must equal the compatibility Plan or its exact Knowledge-rebind descendant.');
|
|
6709
|
+
}
|
|
6710
|
+
const matchingDecisions = readCorrectiveDecisionEvents(this.store, task).filter((event) => {
|
|
6711
|
+
const evidence = event.planIntegrityEvidence;
|
|
6712
|
+
return event.stepId === compatibility.stepId
|
|
6713
|
+
&& event.decision === 'continue-fix'
|
|
6714
|
+
&& event.recoveryBasis === 'plan-integrity'
|
|
6715
|
+
&& evidence?.kind === 'check-support-anchor-outside-allowed-writes'
|
|
6716
|
+
&& evidence.conflictHash === compatibility.conflictHash
|
|
6717
|
+
&& canonicalJsonStringify([...evidence.changedFiles].sort())
|
|
6718
|
+
=== canonicalJsonStringify([...compatibility.dirtyFiles].sort())
|
|
6719
|
+
&& canonicalJsonStringify(evidence.remediationEvents.map((binding) => binding.eventId))
|
|
6720
|
+
=== canonicalJsonStringify(compatibility.remediationEventIds)
|
|
6721
|
+
&& canonicalJsonStringify([...evidence.scopeExtensions].sort())
|
|
6722
|
+
=== canonicalJsonStringify([...(compatibility.scopeExtensions ?? [])].sort());
|
|
6723
|
+
});
|
|
6724
|
+
if (matchingDecisions.length !== 1) {
|
|
6725
|
+
blockers.push('Exactly one validated historical check-support decision must bind the preserved dirty set and scope.');
|
|
6726
|
+
}
|
|
6727
|
+
try {
|
|
6728
|
+
validateTaskHistory(repositoryRoot, task, historyBoundary);
|
|
6729
|
+
}
|
|
6730
|
+
catch (error) {
|
|
6731
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
6732
|
+
}
|
|
6733
|
+
return {
|
|
6734
|
+
applicable: true,
|
|
6735
|
+
eligible: blockers.length === 0,
|
|
6736
|
+
compatibility: blockers.length === 0
|
|
6737
|
+
? {
|
|
6738
|
+
...structuredClone(compatibility),
|
|
6739
|
+
planHash: task.planHash,
|
|
6740
|
+
dirtyFiles,
|
|
6741
|
+
dirtyWorktreeHash: dirtyWorktreeHash,
|
|
6742
|
+
mode: 'post-rebind-dirty-carryover',
|
|
6743
|
+
}
|
|
6744
|
+
: null,
|
|
6745
|
+
dependencyRecoveryCommit: record.commitSha,
|
|
6746
|
+
blockers,
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6749
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
|
|
5241
6750
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
5242
6751
|
const now = this.now().getTime();
|
|
5243
6752
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -5257,7 +6766,7 @@ export class WorkflowService {
|
|
|
5257
6766
|
currentBranch: versions.currentBranch,
|
|
5258
6767
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
5259
6768
|
},
|
|
5260
|
-
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance);
|
|
6769
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
|
|
5261
6770
|
}
|
|
5262
6771
|
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
5263
6772
|
const now = this.now().getTime();
|
|
@@ -5279,10 +6788,9 @@ export class WorkflowService {
|
|
|
5279
6788
|
const policy = gitMutationPolicyForAction(task, action);
|
|
5280
6789
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
5281
6790
|
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
5282
|
-
const
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
: null;
|
|
6791
|
+
const activeLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
6792
|
+
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture, activeLease !== null);
|
|
6793
|
+
const activeLeaseUsable = activeLease ? !toPublicWriterLease(activeLease, this.now()).stale : false;
|
|
5286
6794
|
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
5287
6795
|
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
5288
6796
|
? {
|
|
@@ -5294,9 +6802,24 @@ export class WorkflowService {
|
|
|
5294
6802
|
acquiresLeaseWhenAbsent: true,
|
|
5295
6803
|
}
|
|
5296
6804
|
: null;
|
|
6805
|
+
const credentialBlocksAction = Boolean(writerCredentialRecovery
|
|
6806
|
+
&& writerTokenContract
|
|
6807
|
+
&& (activeLease === null || !activeLeaseUsable));
|
|
5297
6808
|
return {
|
|
5298
6809
|
...next,
|
|
5299
6810
|
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
6811
|
+
...(credentialBlocksAction
|
|
6812
|
+
? {
|
|
6813
|
+
action: 'task writer-credential-replace',
|
|
6814
|
+
blockedAction: action,
|
|
6815
|
+
continuation: {
|
|
6816
|
+
kind: 'failed-step-pause',
|
|
6817
|
+
preservesTask: true,
|
|
6818
|
+
preservesWorktree: true,
|
|
6819
|
+
resumesWithFreshNext: true,
|
|
6820
|
+
},
|
|
6821
|
+
}
|
|
6822
|
+
: {}),
|
|
5300
6823
|
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
5301
6824
|
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
5302
6825
|
};
|
|
@@ -5708,16 +7231,20 @@ function assertTaskBranch(repositoryRoot, task) {
|
|
|
5708
7231
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Expected Task branch ${task.taskBranch}, found ${branch ?? 'detached HEAD'}.`);
|
|
5709
7232
|
}
|
|
5710
7233
|
}
|
|
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
7234
|
function stripStepRuntime(step) {
|
|
5719
|
-
|
|
5720
|
-
|
|
7235
|
+
return normalizePlanStep(step);
|
|
7236
|
+
}
|
|
7237
|
+
function normalizePlanStep(step) {
|
|
7238
|
+
return normalizePlanStepDefinition(step);
|
|
7239
|
+
}
|
|
7240
|
+
function parseWorkflowBetaVersion(value) {
|
|
7241
|
+
const match = value?.match(/^2[.]0[.]0-beta[.]([0-9]+)(?:[.]([0-9]+))?$/);
|
|
7242
|
+
if (!match)
|
|
7243
|
+
return null;
|
|
7244
|
+
return {
|
|
7245
|
+
train: Number(match[1]),
|
|
7246
|
+
patch: match[2] === undefined ? null : Number(match[2]),
|
|
7247
|
+
};
|
|
5721
7248
|
}
|
|
5722
7249
|
function validateReviewInput(review) {
|
|
5723
7250
|
if (!review.reviewer.trim())
|
|
@@ -6091,7 +7618,32 @@ function gitMutationPolicyForAction(task, action) {
|
|
|
6091
7618
|
return null;
|
|
6092
7619
|
}
|
|
6093
7620
|
}
|
|
6094
|
-
function
|
|
7621
|
+
function taskWithEffectiveStepAllowedWrites(store, task, stepId) {
|
|
7622
|
+
const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
|
|
7623
|
+
return {
|
|
7624
|
+
...task,
|
|
7625
|
+
steps: task.steps.map((step) => step.id === stepId ? { ...step, allowedWrites } : step),
|
|
7626
|
+
};
|
|
7627
|
+
}
|
|
7628
|
+
function reviewScopeAugmentations(store, task, stepId) {
|
|
7629
|
+
const step = requireStep(task, stepId);
|
|
7630
|
+
const paths = effectiveStepAllowedWrites(store, task, stepId)
|
|
7631
|
+
.filter((candidate) => !pathAllowed(candidate, step.allowedWrites));
|
|
7632
|
+
return paths.length === 0 ? [] : [{
|
|
7633
|
+
stepId,
|
|
7634
|
+
paths,
|
|
7635
|
+
authority: 'machine-derived-check-support',
|
|
7636
|
+
}];
|
|
7637
|
+
}
|
|
7638
|
+
function reviewCauseBindings(event) {
|
|
7639
|
+
return (event.remediationCauses ?? []).map((cause) => ({
|
|
7640
|
+
authority: 'external-reviewer',
|
|
7641
|
+
causeId: cause.causeId,
|
|
7642
|
+
causeFingerprint: cause.causeFingerprint,
|
|
7643
|
+
planConflict: cause.planConflict,
|
|
7644
|
+
}));
|
|
7645
|
+
}
|
|
7646
|
+
function writerTokenContractForAction(action, claimedC1Posture = false, existingLease = false) {
|
|
6095
7647
|
const alwaysLeaseBound = [
|
|
6096
7648
|
'task step-complete',
|
|
6097
7649
|
'task downstream-proof-recover',
|
|
@@ -6117,16 +7669,19 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
6117
7669
|
'task handback',
|
|
6118
7670
|
'task handback-create',
|
|
6119
7671
|
];
|
|
6120
|
-
if (!alwaysLeaseBound.includes(action)
|
|
7672
|
+
if (!alwaysLeaseBound.includes(action)
|
|
7673
|
+
&& !(claimedC1Posture && claimedC1LeaseBound.includes(action))
|
|
7674
|
+
&& !(existingLease && ['task run', 'task step-review', 'task step-review-record'].includes(action))) {
|
|
6121
7675
|
return null;
|
|
6122
7676
|
}
|
|
6123
7677
|
return {
|
|
6124
7678
|
required: true,
|
|
6125
|
-
option: '--writer-
|
|
6126
|
-
source: '
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
7679
|
+
option: '--writer-credential-ref',
|
|
7680
|
+
source: 'local-credential-vault',
|
|
7681
|
+
transport: 'local-vault-v1',
|
|
7682
|
+
secretInArgv: false,
|
|
7683
|
+
secretInOutput: false,
|
|
7684
|
+
renewalDisposition: 'reuse-reference',
|
|
6130
7685
|
};
|
|
6131
7686
|
}
|
|
6132
7687
|
function observationPreflightBlockers(observation) {
|