codex-workflow-v2 2.0.0-beta.1 → 2.0.0-beta.11
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 +94 -12
- package/dist/src/alpha6/adoption.js +13 -3
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/handoff.d.ts +26 -2
- package/dist/src/alpha6/handoff.js +277 -29
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
- package/dist/src/alpha6/mechanical-feasibility.js +303 -0
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +37 -2
- package/dist/src/alpha6/milestone.js +472 -33
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-integrity.d.ts +14 -0
- package/dist/src/alpha6/plan-integrity.js +127 -0
- package/dist/src/alpha6/plan-integrity.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +1 -0
- package/dist/src/alpha6/plan-risk.js +15 -7
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +2 -1
- package/dist/src/alpha6/remediation.js +186 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +3 -0
- package/dist/src/alpha6/review.js +24 -0
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +486 -46
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +238 -6
- package/dist/src/contracts.js +1 -1
- package/dist/src/credential-output.d.ts +10 -0
- package/dist/src/credential-output.js +73 -0
- package/dist/src/credential-output.js.map +1 -0
- package/dist/src/dependency-provenance.d.ts +19 -0
- package/dist/src/dependency-provenance.js +185 -0
- package/dist/src/dependency-provenance.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +13 -5
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
- package/dist/src/lifecycle/canonical-hash.js +24 -0
- package/dist/src/lifecycle/canonical-hash.js.map +1 -0
- package/dist/src/lifecycle/catalog.d.ts +26 -0
- package/dist/src/lifecycle/catalog.js +208 -0
- package/dist/src/lifecycle/catalog.js.map +1 -0
- package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
- package/dist/src/lifecycle/compiler-inspection.js +29 -0
- package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
- package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
- package/dist/src/lifecycle/core-static-readiness.js +130 -0
- package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
- package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
- package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
- package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
- package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
- package/dist/src/lifecycle/corrective-replan.js +553 -0
- package/dist/src/lifecycle/corrective-replan.js.map +1 -0
- package/dist/src/lifecycle/evaluator.d.ts +2 -0
- package/dist/src/lifecycle/evaluator.js +35 -0
- package/dist/src/lifecycle/evaluator.js.map +1 -0
- package/dist/src/lifecycle/fingerprint.d.ts +31 -0
- package/dist/src/lifecycle/fingerprint.js +388 -0
- package/dist/src/lifecycle/fingerprint.js.map +1 -0
- package/dist/src/lifecycle/immutable.d.ts +1 -0
- package/dist/src/lifecycle/immutable.js +10 -0
- package/dist/src/lifecycle/immutable.js.map +1 -0
- package/dist/src/lifecycle/implementation-table.d.ts +29 -0
- package/dist/src/lifecycle/implementation-table.js +87 -0
- package/dist/src/lifecycle/implementation-table.js.map +1 -0
- package/dist/src/lifecycle/index.d.ts +4 -0
- package/dist/src/lifecycle/index.js +5 -0
- package/dist/src/lifecycle/index.js.map +1 -0
- package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
- package/dist/src/lifecycle/schema-artifact.js +214 -0
- package/dist/src/lifecycle/schema-artifact.js.map +1 -0
- package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
- package/dist/src/lifecycle/semantic-registry.js +489 -0
- package/dist/src/lifecycle/semantic-registry.js.map +1 -0
- package/dist/src/lifecycle/types.d.ts +718 -0
- package/dist/src/lifecycle/types.js +4 -0
- package/dist/src/lifecycle/types.js.map +1 -0
- package/dist/src/observation.d.ts +20 -0
- package/dist/src/observation.js +171 -0
- package/dist/src/observation.js.map +1 -0
- package/dist/src/reviewer.d.ts +43 -1
- package/dist/src/reviewer.js +203 -50
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.d.ts +64 -0
- package/dist/src/state/corrective-replan-executor.js +608 -0
- package/dist/src/state/corrective-replan-executor.js.map +1 -0
- package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
- package/dist/src/state/corrective-replan-public-schema.js +43 -0
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
- package/dist/src/state/corrective-replan-public.d.ts +95 -0
- package/dist/src/state/corrective-replan-public.js +665 -0
- package/dist/src/state/corrective-replan-public.js.map +1 -0
- package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
- package/dist/src/state/corrective-replan-transaction.js +730 -0
- package/dist/src/state/corrective-replan-transaction.js.map +1 -0
- package/dist/src/state/corrective-yield-executor.d.ts +31 -0
- package/dist/src/state/corrective-yield-executor.js +236 -0
- package/dist/src/state/corrective-yield-executor.js.map +1 -0
- package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
- package/dist/src/state/corrective-yield-transaction.js +591 -0
- package/dist/src/state/corrective-yield-transaction.js.map +1 -0
- package/dist/src/state/lock.d.ts +76 -3
- package/dist/src/state/lock.js +611 -30
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/state/store.d.ts +3 -0
- package/dist/src/state/store.js +65 -12
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/transition-core.d.ts +67 -0
- package/dist/src/transition-core.js +318 -0
- package/dist/src/transition-core.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +88 -24
- package/dist/src/workflow.js +1647 -270
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +61 -1
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
- package/docs/delegated-approval.md +26 -1
- package/docs/development-flow.md +63 -8
- package/docs/lifecycle/state-machine-stabilization.md +641 -0
- package/docs/pdf/README.md +24 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +1 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
- package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
- package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
- package/docs/problem-briefs/README.md +120 -0
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
- package/docs/release.md +30 -6
- package/docs/split-required-recovery.md +19 -25
- package/docs/stable-release-defect-register.md +465 -3
- package/docs/updating-existing-project.md +59 -4
- package/docs/validation-report.md +124 -56
- package/package.json +9 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
- package/plugins/codex-workflow-gateway/references/protocol.md +92 -4
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +263 -4
- package/references/state-machine.md +13 -6
- package/roles/delivery-coordinator.md +39 -0
- package/schemas/adoption-posture-event.schema.json +1 -1
- package/schemas/authorization-event.schema.json +64 -1
- package/schemas/context-envelope.schema.json +1 -1
- package/schemas/corrective-decision-event.schema.json +48 -3
- package/schemas/corrective-replan-credentials.private.schema.json +98 -0
- package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
- package/schemas/corrective-replan-public.schema.json +142 -0
- package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
- package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
- package/schemas/milestone-scope-change-event.schema.json +6 -1
- package/schemas/milestone.schema.json +6 -1
- package/schemas/remediation-event.schema.json +11 -1
- package/schemas/task-handoff-event.schema.json +19 -3
- package/schemas/task.schema.json +24 -1
- package/schemas/transition-definition.schema.json +2609 -0
- package/schemas/transition-payloads.schema.json +140 -0
- package/scripts/generate-pdf-docs.py +512 -0
- package/scripts/run-pdf-docs.mjs +62 -0
package/dist/src/workflow.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
6
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
6
7
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
7
8
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
@@ -9,24 +10,34 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
|
9
10
|
import { WorkflowError } from './errors.js';
|
|
10
11
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
11
12
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
12
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
14
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
14
15
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
15
|
-
import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
|
-
import {
|
|
16
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
17
|
+
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
18
|
+
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
19
|
+
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
20
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
18
|
-
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
21
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
19
22
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
20
23
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
21
24
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
22
25
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
26
|
+
import { ensureDirectory } from './fs-utils.js';
|
|
23
27
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
24
28
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
25
|
-
import { launchStrictStepReviewer } from './reviewer.js';
|
|
26
|
-
import { WriterLockManager } from './state/lock.js';
|
|
29
|
+
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
30
|
+
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
27
31
|
import { FileStateStore } from './state/store.js';
|
|
32
|
+
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
33
|
+
import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
|
|
34
|
+
import { createCorrectiveYieldExecutor } from './state/corrective-yield-executor.js';
|
|
35
|
+
import { createCorrectiveReplanPublicController, } from './state/corrective-replan-public.js';
|
|
36
|
+
import { inspectWorkflowObservationBoundary, } from './observation.js';
|
|
28
37
|
import { createEntityId } from './ulid.js';
|
|
29
|
-
import { PACKAGE_VERSION } from './version.js';
|
|
38
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
39
|
+
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
40
|
+
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
30
41
|
export class WorkflowService {
|
|
31
42
|
store;
|
|
32
43
|
now;
|
|
@@ -34,13 +45,42 @@ export class WorkflowService {
|
|
|
34
45
|
this.store = store;
|
|
35
46
|
this.now = now;
|
|
36
47
|
}
|
|
37
|
-
|
|
48
|
+
#mutationContext(repository) {
|
|
38
49
|
const identity = resolveRepositoryIdentity(repository);
|
|
39
50
|
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
51
|
+
const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
|
|
52
|
+
const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
|
|
53
|
+
const taskTransactions = {
|
|
54
|
+
corrupt: [...replanTransactions.corrupt, ...yieldTransactions.corrupt],
|
|
55
|
+
pending: [...replanTransactions.pending, ...yieldTransactions.pending],
|
|
56
|
+
};
|
|
57
|
+
if (taskTransactions.corrupt.length > 0) {
|
|
58
|
+
throw new WorkflowError('STATE_CORRUPT', 'A Task transaction journal failed integrity validation.', {
|
|
59
|
+
transactions: taskTransactions.corrupt,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (taskTransactions.pending.length > 0) {
|
|
63
|
+
throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'A Task transaction requires explicit recovery.', {
|
|
64
|
+
taskIds: [...new Set(taskTransactions.pending.map((transaction) => transaction.taskId))].sort(),
|
|
65
|
+
transactionIds: taskTransactions.pending.map((transaction) => transaction.transactionId).sort(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const coreTaskOperations = listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root);
|
|
69
|
+
if (coreTaskOperations.length > 0) {
|
|
70
|
+
throw new WorkflowError('LOCKED', 'A Core Task operation owns the project mutation boundary.', {
|
|
71
|
+
taskIds: coreTaskOperations.map((operation) => operation.entityId).sort(),
|
|
72
|
+
operationIds: coreTaskOperations.map((operation) => operation.operationId).sort(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
40
75
|
recoverProjectTransactions(projectRoot, identity.projectId);
|
|
41
76
|
const projectFile = path.join(projectRoot, 'project.json');
|
|
42
77
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
43
78
|
if (!projectAlreadyRegistered) {
|
|
79
|
+
// The registration posture is a sidecar under the project root. A completely
|
|
80
|
+
// fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
|
|
81
|
+
// only that directory before writing the posture; project.json remains the
|
|
82
|
+
// final registration record written below.
|
|
83
|
+
ensureDirectory(projectRoot);
|
|
44
84
|
initializeProjectRegistrationAdoption(this.store, {
|
|
45
85
|
projectId: identity.projectId,
|
|
46
86
|
repositoryIdentity: identity,
|
|
@@ -54,17 +94,103 @@ export class WorkflowService {
|
|
|
54
94
|
return {
|
|
55
95
|
identity,
|
|
56
96
|
store: this.store,
|
|
57
|
-
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
|
|
97
|
+
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
inspectWriterLease(repository, entityId) {
|
|
101
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
102
|
+
const lease = inspectRawWriterLease(this.store.lockRoot(identity.projectId), entityId, this.store.root);
|
|
103
|
+
return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
|
|
104
|
+
}
|
|
105
|
+
repairStaleWriterLease(repository, entityId) {
|
|
106
|
+
const context = this.#mutationContext(repository);
|
|
107
|
+
const lease = context.locks.repairStale(entityId);
|
|
108
|
+
return {
|
|
109
|
+
...this.publicWriterLeaseDiagnostic(lease),
|
|
110
|
+
repaired: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
prepareCorrectiveReplan(repository, taskId, candidate) {
|
|
114
|
+
return this.correctiveReplanController(repository).prepare({ taskId, candidate });
|
|
115
|
+
}
|
|
116
|
+
confirmCorrectiveReplan(repository, taskId, candidate, gate, confirmationCode) {
|
|
117
|
+
return this.correctiveReplanController(repository).confirm({ taskId, candidate, gate, confirmationCode });
|
|
118
|
+
}
|
|
119
|
+
executeCorrectiveReplan(repository, taskId, candidate, credential) {
|
|
120
|
+
return this.correctiveReplanController(repository).execute({ taskId, candidate, credential });
|
|
121
|
+
}
|
|
122
|
+
recoverCorrectiveReplan(repository, taskId, transactionId) {
|
|
123
|
+
return this.correctiveReplanController(repository).recover({ taskId, transactionId });
|
|
124
|
+
}
|
|
125
|
+
executeCorrectiveYield(repository, taskId, expectedTaskRevision, actor, writerToken, reason) {
|
|
126
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
127
|
+
return createCorrectiveYieldExecutor({
|
|
128
|
+
store: this.store,
|
|
129
|
+
projectId: identity.projectId,
|
|
130
|
+
now: this.now,
|
|
131
|
+
}).execute({
|
|
132
|
+
projectId: identity.projectId,
|
|
133
|
+
taskId,
|
|
134
|
+
expectedTaskRevision,
|
|
135
|
+
actor,
|
|
136
|
+
writerToken,
|
|
137
|
+
reason,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
recoverCorrectiveYield(repository, taskId, transactionId) {
|
|
141
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
142
|
+
return createCorrectiveYieldExecutor({
|
|
143
|
+
store: this.store,
|
|
144
|
+
projectId: identity.projectId,
|
|
145
|
+
now: this.now,
|
|
146
|
+
}).recover({ taskId, transactionId });
|
|
147
|
+
}
|
|
148
|
+
correctiveReplanController(repository) {
|
|
149
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
150
|
+
return createCorrectiveReplanPublicController({
|
|
151
|
+
store: this.store,
|
|
152
|
+
projectId: identity.projectId,
|
|
153
|
+
repositoryRoot: identity.repositoryRoot,
|
|
154
|
+
now: this.now,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
observationSnapshot(repository) {
|
|
158
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
159
|
+
const boundary = inspectWorkflowObservationBoundary({
|
|
160
|
+
store: this.store,
|
|
161
|
+
identity,
|
|
162
|
+
now: this.now(),
|
|
163
|
+
});
|
|
164
|
+
if (boundary.observation.kind !== 'assessment') {
|
|
165
|
+
return {
|
|
166
|
+
...boundary,
|
|
167
|
+
discoveries: [],
|
|
168
|
+
tasks: [],
|
|
169
|
+
milestones: [],
|
|
170
|
+
delegations: [],
|
|
171
|
+
adoption: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
175
|
+
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
176
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones, boundary.leases, boundary.currentAdoptionPosture);
|
|
177
|
+
return {
|
|
178
|
+
...boundary,
|
|
179
|
+
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
180
|
+
tasks,
|
|
181
|
+
milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
|
|
182
|
+
delegations: this.store.listDelegations(identity.projectId),
|
|
183
|
+
adoption,
|
|
58
184
|
};
|
|
59
185
|
}
|
|
60
186
|
prepareDelegation(repository, input) {
|
|
61
|
-
const { identity } = this
|
|
187
|
+
const { identity } = this.#mutationContext(repository);
|
|
62
188
|
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
63
189
|
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
64
190
|
return gate;
|
|
65
191
|
}
|
|
66
192
|
grantDelegation(repository, input, confirmationCode) {
|
|
67
|
-
const { identity } = this
|
|
193
|
+
const { identity } = this.#mutationContext(repository);
|
|
68
194
|
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
69
195
|
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
70
196
|
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
@@ -100,7 +226,7 @@ export class WorkflowService {
|
|
|
100
226
|
}, null);
|
|
101
227
|
}
|
|
102
228
|
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
103
|
-
const { identity } = this
|
|
229
|
+
const { identity } = this.#mutationContext(repository);
|
|
104
230
|
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
105
231
|
assertExpectedRevision(grant, expectedRevision);
|
|
106
232
|
if (grant.status !== 'active') {
|
|
@@ -124,7 +250,7 @@ export class WorkflowService {
|
|
|
124
250
|
startDiscovery(repository, input) {
|
|
125
251
|
if (!input.goal.trim())
|
|
126
252
|
throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
|
|
127
|
-
const { identity } = this
|
|
253
|
+
const { identity } = this.#mutationContext(repository);
|
|
128
254
|
const timestamp = this.now().toISOString();
|
|
129
255
|
const initial = {
|
|
130
256
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
@@ -152,7 +278,7 @@ export class WorkflowService {
|
|
|
152
278
|
return this.store.writeDiscovery(initial, null);
|
|
153
279
|
}
|
|
154
280
|
updateDiscovery(repository, discoveryId, expectedRevision, patch) {
|
|
155
|
-
const { identity } = this
|
|
281
|
+
const { identity } = this.#mutationContext(repository);
|
|
156
282
|
const current = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
157
283
|
if (current.status === 'materialized' || current.status === 'abandoned') {
|
|
158
284
|
throw new WorkflowError('TRANSITION_BLOCKED', `Discovery ${discoveryId} is ${current.status}.`);
|
|
@@ -176,7 +302,7 @@ export class WorkflowService {
|
|
|
176
302
|
return this.store.writeDiscovery(updated, expectedRevision);
|
|
177
303
|
}
|
|
178
304
|
materializeTask(repository, discoveryId, expectedRevision, title, baseBranch = 'main', milestoneId = null) {
|
|
179
|
-
const { identity } = this
|
|
305
|
+
const { identity } = this.#mutationContext(repository);
|
|
180
306
|
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
181
307
|
assertExpectedRevision(discovery, expectedRevision);
|
|
182
308
|
const readiness = assessDiscovery(discovery);
|
|
@@ -253,7 +379,7 @@ export class WorkflowService {
|
|
|
253
379
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
254
380
|
}
|
|
255
381
|
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
256
|
-
const { identity, locks } = this
|
|
382
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
257
383
|
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
258
384
|
assertExpectedRevision(oldTask, expectedOldTaskRevision);
|
|
259
385
|
if (!oldTask.milestoneId) {
|
|
@@ -270,6 +396,32 @@ export class WorkflowService {
|
|
|
270
396
|
disposition: membership?.disposition ?? null,
|
|
271
397
|
});
|
|
272
398
|
}
|
|
399
|
+
const incidentDependencies = milestone.memberships.flatMap((candidate) => {
|
|
400
|
+
if (!Array.isArray(candidate.dependsOnTaskIds))
|
|
401
|
+
return [];
|
|
402
|
+
return [
|
|
403
|
+
...(candidate.taskId === oldTask.id
|
|
404
|
+
? candidate.dependsOnTaskIds.map((predecessorTaskId) => ({
|
|
405
|
+
predecessorTaskId,
|
|
406
|
+
consumerTaskId: candidate.taskId,
|
|
407
|
+
}))
|
|
408
|
+
: []),
|
|
409
|
+
...(candidate.dependsOnTaskIds.includes(oldTask.id)
|
|
410
|
+
? [{ predecessorTaskId: oldTask.id, consumerTaskId: candidate.taskId }]
|
|
411
|
+
: []),
|
|
412
|
+
];
|
|
413
|
+
});
|
|
414
|
+
if (structuralReplacementIsDisabled()) {
|
|
415
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Structural replacement is disabled until a topology-preserving replacement transaction is implemented.', {
|
|
416
|
+
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
417
|
+
milestoneId: milestone.id,
|
|
418
|
+
oldTaskId: oldTask.id,
|
|
419
|
+
incidentDependencies,
|
|
420
|
+
requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/* P04-B retains the existing one-for-one transaction below as implementation evidence,
|
|
424
|
+
but this route must remain unreachable until dependency rewiring is specified. */
|
|
273
425
|
const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
|
|
274
426
|
if (!riskAudit)
|
|
275
427
|
throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
|
|
@@ -379,9 +531,19 @@ export class WorkflowService {
|
|
|
379
531
|
checks: milestone.checks,
|
|
380
532
|
memberships: [
|
|
381
533
|
...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
|
|
382
|
-
? {
|
|
383
|
-
|
|
384
|
-
|
|
534
|
+
? {
|
|
535
|
+
...candidate,
|
|
536
|
+
disposition: 'cancelled',
|
|
537
|
+
reason: replacementReason,
|
|
538
|
+
dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])],
|
|
539
|
+
}
|
|
540
|
+
: { ...candidate, dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])] }),
|
|
541
|
+
{
|
|
542
|
+
taskId: replacementTaskId,
|
|
543
|
+
disposition: 'required',
|
|
544
|
+
reason: replacementReason,
|
|
545
|
+
dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])],
|
|
546
|
+
},
|
|
385
547
|
],
|
|
386
548
|
};
|
|
387
549
|
if (autonomyContractEventHash) {
|
|
@@ -504,7 +666,7 @@ export class WorkflowService {
|
|
|
504
666
|
};
|
|
505
667
|
}
|
|
506
668
|
materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
|
|
507
|
-
const { identity } = this
|
|
669
|
+
const { identity } = this.#mutationContext(repository);
|
|
508
670
|
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
509
671
|
assertExpectedRevision(discovery, expectedRevision);
|
|
510
672
|
const readiness = assessDiscovery(discovery);
|
|
@@ -552,7 +714,7 @@ export class WorkflowService {
|
|
|
552
714
|
};
|
|
553
715
|
}
|
|
554
716
|
setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
|
|
555
|
-
const { identity } = this
|
|
717
|
+
const { identity } = this.#mutationContext(repository);
|
|
556
718
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
557
719
|
assertExpectedRevision(milestone, expectedRevision);
|
|
558
720
|
if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
|
|
@@ -590,41 +752,18 @@ export class WorkflowService {
|
|
|
590
752
|
throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
|
|
591
753
|
}
|
|
592
754
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
checks: normalized.checks,
|
|
603
|
-
}, null, 2));
|
|
604
|
-
const candidate = {
|
|
605
|
-
...milestone,
|
|
606
|
-
status: 'awaiting_execution_authorization',
|
|
607
|
-
outcome: normalized.outcome,
|
|
608
|
-
successSignal: normalized.successSignal,
|
|
609
|
-
acceptance: normalized.acceptance,
|
|
610
|
-
taskIds,
|
|
611
|
-
memberships: normalized.memberships,
|
|
612
|
-
checks: normalized.checks,
|
|
613
|
-
membershipRevision: milestone.membershipRevision + 1,
|
|
614
|
-
authorizations,
|
|
615
|
-
planHash: stagedPlan.hash,
|
|
616
|
-
resultHash: null,
|
|
617
|
-
evidenceHash: null,
|
|
618
|
-
acceptedHead: null,
|
|
619
|
-
cancellationReason: null,
|
|
620
|
-
};
|
|
621
|
-
assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
|
|
622
|
-
const saved = this.store.writeMilestone(candidate, expectedRevision);
|
|
623
|
-
this.store.publishArtifact(stagedPlan);
|
|
624
|
-
return saved;
|
|
755
|
+
return applyInitialMilestonePlanTransaction({
|
|
756
|
+
store: this.store,
|
|
757
|
+
milestone,
|
|
758
|
+
expectedRevision,
|
|
759
|
+
normalizedPlan: normalized,
|
|
760
|
+
now: this.now(),
|
|
761
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
762
|
+
listTasks: () => this.store.listTasks(identity.projectId),
|
|
763
|
+
});
|
|
625
764
|
}
|
|
626
765
|
prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
|
|
627
|
-
const { identity } = this
|
|
766
|
+
const { identity } = this.#mutationContext(repository);
|
|
628
767
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
629
768
|
assertExpectedRevision(milestone, expectedRevision);
|
|
630
769
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -641,7 +780,7 @@ export class WorkflowService {
|
|
|
641
780
|
};
|
|
642
781
|
}
|
|
643
782
|
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
644
|
-
const { identity } = this
|
|
783
|
+
const { identity } = this.#mutationContext(repository);
|
|
645
784
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
646
785
|
assertExpectedRevision(milestone, expectedRevision);
|
|
647
786
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -664,9 +803,10 @@ export class WorkflowService {
|
|
|
664
803
|
});
|
|
665
804
|
}
|
|
666
805
|
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
667
|
-
const { identity } = this
|
|
806
|
+
const { identity } = this.#mutationContext(repository);
|
|
668
807
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
669
808
|
assertExpectedRevision(milestone, expectedRevision);
|
|
809
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
670
810
|
return prepareMilestoneAutonomyContract({
|
|
671
811
|
projectId: identity.projectId,
|
|
672
812
|
milestone,
|
|
@@ -677,9 +817,10 @@ export class WorkflowService {
|
|
|
677
817
|
});
|
|
678
818
|
}
|
|
679
819
|
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
680
|
-
const { identity } = this
|
|
820
|
+
const { identity } = this.#mutationContext(repository);
|
|
681
821
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
682
822
|
assertExpectedRevision(milestone, expectedRevision);
|
|
823
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
683
824
|
return applyMilestoneAutonomyContract({
|
|
684
825
|
store: this.store,
|
|
685
826
|
projectId: identity.projectId,
|
|
@@ -692,7 +833,7 @@ export class WorkflowService {
|
|
|
692
833
|
});
|
|
693
834
|
}
|
|
694
835
|
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
695
|
-
const { identity } = this
|
|
836
|
+
const { identity } = this.#mutationContext(repository);
|
|
696
837
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
697
838
|
assertExpectedRevision(milestone, expectedRevision);
|
|
698
839
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -722,12 +863,14 @@ export class WorkflowService {
|
|
|
722
863
|
});
|
|
723
864
|
}
|
|
724
865
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
725
|
-
const { identity } = this
|
|
866
|
+
const { identity } = this.#mutationContext(repository);
|
|
726
867
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
727
868
|
assertExpectedRevision(milestone, expectedRevision);
|
|
728
869
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
729
870
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
730
871
|
}
|
|
872
|
+
assertMilestoneDependenciesDeclared(milestone);
|
|
873
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
731
874
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
732
875
|
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
733
876
|
if (planHash !== milestone.planHash) {
|
|
@@ -751,7 +894,7 @@ export class WorkflowService {
|
|
|
751
894
|
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
752
895
|
}
|
|
753
896
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
754
|
-
const { identity } = this
|
|
897
|
+
const { identity } = this.#mutationContext(repository);
|
|
755
898
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
756
899
|
assertExpectedRevision(milestone, expectedRevision);
|
|
757
900
|
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
@@ -814,7 +957,7 @@ export class WorkflowService {
|
|
|
814
957
|
return saved;
|
|
815
958
|
}
|
|
816
959
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
817
|
-
const { identity } = this
|
|
960
|
+
const { identity } = this.#mutationContext(repository);
|
|
818
961
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
819
962
|
assertExpectedRevision(milestone, expectedRevision);
|
|
820
963
|
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
@@ -853,7 +996,7 @@ export class WorkflowService {
|
|
|
853
996
|
resultHash,
|
|
854
997
|
evidenceHash,
|
|
855
998
|
headCommit: currentHead,
|
|
856
|
-
reason:
|
|
999
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
857
1000
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
858
1001
|
...(delegation ? { delegation } : {}),
|
|
859
1002
|
};
|
|
@@ -869,7 +1012,7 @@ export class WorkflowService {
|
|
|
869
1012
|
cancelMilestone(repository, milestoneId, expectedRevision, reason) {
|
|
870
1013
|
if (!reason.trim())
|
|
871
1014
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
872
|
-
const { identity } = this
|
|
1015
|
+
const { identity } = this.#mutationContext(repository);
|
|
873
1016
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
874
1017
|
assertExpectedRevision(milestone, expectedRevision);
|
|
875
1018
|
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
@@ -879,21 +1022,59 @@ export class WorkflowService {
|
|
|
879
1022
|
}
|
|
880
1023
|
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
881
1024
|
validatePlan(plan);
|
|
882
|
-
const { identity } = this
|
|
1025
|
+
const { identity } = this.#mutationContext(repository);
|
|
883
1026
|
const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
|
|
884
1027
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
885
1028
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
886
1029
|
assertExpectedRevision(task, expectedRevision);
|
|
887
|
-
|
|
888
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
889
|
-
}
|
|
1030
|
+
const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
|
|
890
1031
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
891
|
-
?
|
|
1032
|
+
? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
|
|
892
1033
|
: [];
|
|
893
1034
|
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
894
|
-
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) =>
|
|
1035
|
+
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => {
|
|
1036
|
+
if (event.decision !== 'continue-fix')
|
|
1037
|
+
return false;
|
|
1038
|
+
const passedBoundary = latestPassedVerifiedStepReviewEvent(this.store, task, event.stepId);
|
|
1039
|
+
return !passedBoundary || passedBoundary.recordedAt <= event.recordedAt;
|
|
1040
|
+
});
|
|
895
1041
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
896
|
-
const
|
|
1042
|
+
const currentPlanRiskAudit = task.status === 'needs_fix' || currentReplanDecisions.length > 0
|
|
1043
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
1044
|
+
: null;
|
|
1045
|
+
const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
|
|
1046
|
+
? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
|
|
1047
|
+
: null;
|
|
1048
|
+
const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
|
|
1049
|
+
? classifiedCorrectivePosture
|
|
1050
|
+
: currentPlanRiskAudit
|
|
1051
|
+
? identifyCorrectiveReplanPosture(task, currentPlanRiskAudit.reviewRequiredStepIds, correctiveDecisionEvents, readRemediationEvents(this.store, task))
|
|
1052
|
+
: null;
|
|
1053
|
+
const correctiveReplanAvailability = correctiveReplanPosture?.posture === 'replan-required'
|
|
1054
|
+
? evaluateTaskCorrectiveReplanAvailability(task, correctiveReplanPosture)
|
|
1055
|
+
: null;
|
|
1056
|
+
if (currentReplanDecisions.length > 0) {
|
|
1057
|
+
if (!correctiveReplanAvailability?.allowed) {
|
|
1058
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} is blocked for Task ${task.id}.`, {
|
|
1059
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1060
|
+
posture: correctiveReplanPosture?.posture ?? null,
|
|
1061
|
+
evaluationPhase: correctiveReplanAvailability?.phase ?? 'availability',
|
|
1062
|
+
blockingGuardIds: correctiveReplanAvailability?.blockingGuardIds ?? [],
|
|
1063
|
+
pendingGuardIds: correctiveReplanAvailability?.pendingGuardIds ?? [],
|
|
1064
|
+
guardResults: correctiveReplanAvailability?.guardResults ?? [],
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Legacy task plan-set cannot execute ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID}.`, {
|
|
1068
|
+
taskId: task.id,
|
|
1069
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1070
|
+
requiredAction: readTaskC1Posture(this.store, task).state === 'claimed'
|
|
1071
|
+
? 'Run task corrective-yield first, then use task corrective-replan-prepare.'
|
|
1072
|
+
: 'Use task corrective-replan-prepare, confirm, and execute.',
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
1076
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
1077
|
+
}
|
|
897
1078
|
if (currentHardBlockingDecisions.length > 0) {
|
|
898
1079
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
|
|
899
1080
|
taskId: task.id,
|
|
@@ -923,9 +1104,6 @@ export class WorkflowService {
|
|
|
923
1104
|
})),
|
|
924
1105
|
});
|
|
925
1106
|
}
|
|
926
|
-
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
927
|
-
? readCurrentPlanRiskAudit(this.store, task)
|
|
928
|
-
: null;
|
|
929
1107
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
930
1108
|
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
931
1109
|
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
@@ -968,7 +1146,7 @@ export class WorkflowService {
|
|
|
968
1146
|
}
|
|
969
1147
|
}
|
|
970
1148
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
971
|
-
const
|
|
1149
|
+
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
972
1150
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
973
1151
|
? { ...authorization, decision: 'superseded' }
|
|
974
1152
|
: authorization);
|
|
@@ -985,12 +1163,17 @@ export class WorkflowService {
|
|
|
985
1163
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
986
1164
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
987
1165
|
}
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1166
|
+
if (correctiveReplanPosture?.posture === 'replan-required') {
|
|
1167
|
+
const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
|
|
1168
|
+
if (!executionEvaluation.allowed) {
|
|
1169
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
|
|
1170
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1171
|
+
evaluationPhase: executionEvaluation.phase,
|
|
1172
|
+
blockingGuardIds: executionEvaluation.blockingGuardIds,
|
|
1173
|
+
pendingGuardIds: executionEvaluation.pendingGuardIds,
|
|
1174
|
+
guardResults: executionEvaluation.guardResults,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
994
1177
|
}
|
|
995
1178
|
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
996
1179
|
throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
|
|
@@ -998,14 +1181,10 @@ export class WorkflowService {
|
|
|
998
1181
|
stepId: guardedRemediationGate.stepId,
|
|
999
1182
|
});
|
|
1000
1183
|
}
|
|
1001
|
-
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
1002
|
-
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
1003
|
-
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
1004
|
-
}
|
|
1005
1184
|
const candidate = {
|
|
1006
1185
|
...task,
|
|
1007
1186
|
status: 'awaiting_execution_authorization',
|
|
1008
|
-
planHash:
|
|
1187
|
+
planHash: preparedPlan.planHash,
|
|
1009
1188
|
planObjective: plan.objective,
|
|
1010
1189
|
requirements: plan.requirements,
|
|
1011
1190
|
acceptance: plan.acceptance,
|
|
@@ -1025,6 +1204,18 @@ export class WorkflowService {
|
|
|
1025
1204
|
};
|
|
1026
1205
|
if (planRiskAudit)
|
|
1027
1206
|
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
1207
|
+
const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
|
|
1208
|
+
if (stagedPlan.hash !== preparedPlan.planHash) {
|
|
1209
|
+
throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
|
|
1210
|
+
taskId: task.id,
|
|
1211
|
+
preparedPlanHash: preparedPlan.planHash,
|
|
1212
|
+
stagedPlanHash: stagedPlan.hash,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
1216
|
+
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
1217
|
+
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, preparedPlan.planHash, this.now());
|
|
1218
|
+
}
|
|
1028
1219
|
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
1029
1220
|
this.store.publishArtifact(stagedPlan);
|
|
1030
1221
|
if (planRiskAudit)
|
|
@@ -1035,7 +1226,7 @@ export class WorkflowService {
|
|
|
1035
1226
|
return saved;
|
|
1036
1227
|
}
|
|
1037
1228
|
rebindTaskKnowledge(repository, taskId, expectedRevision) {
|
|
1038
|
-
const { identity } = this
|
|
1229
|
+
const { identity } = this.#mutationContext(repository);
|
|
1039
1230
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1040
1231
|
assertExpectedRevision(task, expectedRevision);
|
|
1041
1232
|
this.assertTaskContextChangeAllowed(task);
|
|
@@ -1120,7 +1311,7 @@ export class WorkflowService {
|
|
|
1120
1311
|
if (!delegationGrantId) {
|
|
1121
1312
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
|
|
1122
1313
|
}
|
|
1123
|
-
const { identity } = this
|
|
1314
|
+
const { identity } = this.#mutationContext(repository);
|
|
1124
1315
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1125
1316
|
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
1126
1317
|
assertExpectedRevision(task, expectedTaskRevision);
|
|
@@ -1211,7 +1402,7 @@ export class WorkflowService {
|
|
|
1211
1402
|
});
|
|
1212
1403
|
}
|
|
1213
1404
|
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
1214
|
-
const { identity } = this
|
|
1405
|
+
const { identity } = this.#mutationContext(repository);
|
|
1215
1406
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1216
1407
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1217
1408
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1222,7 +1413,24 @@ export class WorkflowService {
|
|
|
1222
1413
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1223
1414
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
1224
1415
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
1416
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
1417
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
1418
|
+
taskId: task.id,
|
|
1419
|
+
stateBriefHash: task.briefHash,
|
|
1420
|
+
artifactBriefHash: briefHash,
|
|
1421
|
+
statePlanHash: task.planHash,
|
|
1422
|
+
artifactPlanHash: planHash,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1225
1425
|
readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
|
|
1426
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
|
|
1427
|
+
if (mechanicalFeasibility.decision === 'blocked') {
|
|
1428
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
|
|
1429
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
1430
|
+
mechanicalFeasibility,
|
|
1431
|
+
requiredAction: 'task plan-set',
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1226
1434
|
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1227
1435
|
const event = {
|
|
1228
1436
|
kind: 'execution',
|
|
@@ -1232,16 +1440,17 @@ export class WorkflowService {
|
|
|
1232
1440
|
briefHash,
|
|
1233
1441
|
planHash,
|
|
1234
1442
|
resultHash: null,
|
|
1235
|
-
evidenceHash:
|
|
1236
|
-
headCommit:
|
|
1443
|
+
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1444
|
+
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1237
1445
|
reason,
|
|
1238
1446
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1239
1447
|
...(delegation ? { delegation } : {}),
|
|
1448
|
+
mechanicalFeasibility,
|
|
1240
1449
|
};
|
|
1241
1450
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
1242
1451
|
}
|
|
1243
1452
|
recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
|
|
1244
|
-
const { identity } = this
|
|
1453
|
+
const { identity } = this.#mutationContext(repository);
|
|
1245
1454
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1246
1455
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1247
1456
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1249,7 +1458,7 @@ export class WorkflowService {
|
|
|
1249
1458
|
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
1250
1459
|
}
|
|
1251
1460
|
showTaskHandoff(repository, taskId) {
|
|
1252
|
-
const { identity } = this
|
|
1461
|
+
const { identity } = this.#mutationContext(repository);
|
|
1253
1462
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1254
1463
|
return {
|
|
1255
1464
|
task,
|
|
@@ -1257,8 +1466,8 @@ export class WorkflowService {
|
|
|
1257
1466
|
claimTokenAvailable: false,
|
|
1258
1467
|
};
|
|
1259
1468
|
}
|
|
1260
|
-
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
1261
|
-
const context = this
|
|
1469
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
1470
|
+
const context = this.#mutationContext(repository);
|
|
1262
1471
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1263
1472
|
assertExpectedRevision(task, expectedRevision);
|
|
1264
1473
|
const normalizedActor = actor.trim();
|
|
@@ -1268,6 +1477,9 @@ export class WorkflowService {
|
|
|
1268
1477
|
if (grantId)
|
|
1269
1478
|
this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
|
|
1270
1479
|
const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
|
|
1480
|
+
const dependencyBinding = milestone
|
|
1481
|
+
? this.currentMilestoneDependencyBinding(milestone, task.id)
|
|
1482
|
+
: null;
|
|
1271
1483
|
const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
|
|
1272
1484
|
const milestoneDisplayNumber = milestone
|
|
1273
1485
|
? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
|
|
@@ -1313,9 +1525,11 @@ export class WorkflowService {
|
|
|
1313
1525
|
writerToken: lease.token,
|
|
1314
1526
|
milestoneDisplayNumber,
|
|
1315
1527
|
taskDisplayNumber,
|
|
1528
|
+
dependencyBinding,
|
|
1316
1529
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1317
1530
|
grantId,
|
|
1318
1531
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
1532
|
+
replacePending,
|
|
1319
1533
|
}, this.now());
|
|
1320
1534
|
releaseLease();
|
|
1321
1535
|
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
@@ -1327,15 +1541,19 @@ export class WorkflowService {
|
|
|
1327
1541
|
}
|
|
1328
1542
|
}
|
|
1329
1543
|
claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
|
|
1330
|
-
const context = this
|
|
1544
|
+
const context = this.#mutationContext(repository);
|
|
1331
1545
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1332
1546
|
assertExpectedRevision(task, expectedRevision);
|
|
1333
1547
|
const normalizedActor = actor.trim();
|
|
1334
1548
|
if (!normalizedActor) {
|
|
1335
1549
|
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
1336
1550
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1551
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1552
|
+
const dependencyBinding = task.milestoneId
|
|
1553
|
+
? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
|
|
1554
|
+
: null;
|
|
1555
|
+
const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
|
|
1556
|
+
if (pendingHandoff.latestEvent.grantId) {
|
|
1339
1557
|
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
1340
1558
|
}
|
|
1341
1559
|
const existingLease = context.locks.inspect(taskId);
|
|
@@ -1367,8 +1585,15 @@ export class WorkflowService {
|
|
|
1367
1585
|
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1368
1586
|
}
|
|
1369
1587
|
try {
|
|
1370
|
-
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1371
|
-
return {
|
|
1588
|
+
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
|
|
1589
|
+
return {
|
|
1590
|
+
task,
|
|
1591
|
+
event,
|
|
1592
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1593
|
+
writerCredential: acquiredHere
|
|
1594
|
+
? { disposition: 'issued', token: lease.token }
|
|
1595
|
+
: { disposition: 'renewed' },
|
|
1596
|
+
};
|
|
1372
1597
|
}
|
|
1373
1598
|
catch (error) {
|
|
1374
1599
|
if (acquiredHere)
|
|
@@ -1376,8 +1601,61 @@ export class WorkflowService {
|
|
|
1376
1601
|
throw error;
|
|
1377
1602
|
}
|
|
1378
1603
|
}
|
|
1604
|
+
replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
|
|
1605
|
+
const context = this.#mutationContext(repository);
|
|
1606
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1607
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1608
|
+
const normalizedActor = actor.trim();
|
|
1609
|
+
if (!normalizedActor)
|
|
1610
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1611
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1612
|
+
if (posture.state === 'claimed' && task.milestoneId) {
|
|
1613
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
|
|
1614
|
+
const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
|
|
1615
|
+
if (claimedBinding === null
|
|
1616
|
+
|| claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1617
|
+
|| claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1618
|
+
|| claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1619
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
|
|
1620
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1621
|
+
taskId: task.id,
|
|
1622
|
+
milestoneId: task.milestoneId,
|
|
1623
|
+
claimDependencyBinding: claimedBinding,
|
|
1624
|
+
currentDependencyBinding: dependencyBinding,
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1629
|
+
const action = String(navigation.action);
|
|
1630
|
+
const currentLease = context.locks.inspect(taskId);
|
|
1631
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
1632
|
+
const requiredActor = writerTokenContract
|
|
1633
|
+
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1634
|
+
: null;
|
|
1635
|
+
if (!requiredActor || requiredActor !== normalizedActor) {
|
|
1636
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
|
|
1637
|
+
taskId,
|
|
1638
|
+
posture: posture.state,
|
|
1639
|
+
claimant: posture.state === 'claimed' ? posture.claimant : null,
|
|
1640
|
+
actor: normalizedActor,
|
|
1641
|
+
requiredActor,
|
|
1642
|
+
nextAction: action,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
const lease = currentLease
|
|
1646
|
+
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1647
|
+
: context.locks.acquire(taskId, normalizedActor);
|
|
1648
|
+
if (posture.state === 'claimed') {
|
|
1649
|
+
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
task,
|
|
1653
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1654
|
+
writerCredential: { disposition: 'issued', token: lease.token },
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1379
1657
|
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1380
|
-
const context = this
|
|
1658
|
+
const context = this.#mutationContext(repository);
|
|
1381
1659
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1382
1660
|
assertExpectedRevision(task, expectedRevision);
|
|
1383
1661
|
const lease = task.status === 'merged' || task.status === 'cancelled'
|
|
@@ -1389,23 +1667,16 @@ export class WorkflowService {
|
|
|
1389
1667
|
return { task, event };
|
|
1390
1668
|
}
|
|
1391
1669
|
startTask(repository, taskId, expectedRevision, workspaceOwner) {
|
|
1392
|
-
const { identity } = this
|
|
1670
|
+
const { identity } = this.#mutationContext(repository);
|
|
1393
1671
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1394
1672
|
assertExpectedRevision(task, expectedRevision);
|
|
1395
1673
|
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
1396
1674
|
if (task.status !== 'ready')
|
|
1397
1675
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
|
|
1398
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1676
|
+
const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
|
|
1399
1677
|
if (task.milestoneId) {
|
|
1400
1678
|
const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
|
|
1401
|
-
const
|
|
1402
|
-
if (milestone.status !== 'active' || membership?.disposition !== 'required') {
|
|
1403
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
|
|
1404
|
-
milestoneId: milestone.id,
|
|
1405
|
-
milestoneStatus: milestone.status,
|
|
1406
|
-
disposition: membership?.disposition ?? null,
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1679
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
|
|
1409
1680
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1410
1681
|
if (handoffPosture.state !== 'claimed') {
|
|
1411
1682
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
|
|
@@ -1415,6 +1686,20 @@ export class WorkflowService {
|
|
|
1415
1686
|
requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
|
|
1416
1687
|
});
|
|
1417
1688
|
}
|
|
1689
|
+
const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
|
|
1690
|
+
if (claimBinding === null
|
|
1691
|
+
|| claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1692
|
+
|| claimBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1693
|
+
|| claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1694
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
|
|
1695
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1696
|
+
taskId: task.id,
|
|
1697
|
+
milestoneId: milestone.id,
|
|
1698
|
+
claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
|
|
1699
|
+
currentDependencyBinding: dependencyBinding,
|
|
1700
|
+
requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1418
1703
|
}
|
|
1419
1704
|
if (!isClean(identity.repositoryRoot)) {
|
|
1420
1705
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
@@ -1424,12 +1709,22 @@ export class WorkflowService {
|
|
|
1424
1709
|
const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
|
|
1425
1710
|
let baseCommit;
|
|
1426
1711
|
let taskBranch;
|
|
1712
|
+
const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
|
|
1713
|
+
? executionAuthorization.mechanicalFeasibility.evaluatedHead
|
|
1714
|
+
: null;
|
|
1427
1715
|
if (workspaceOwner === 'local') {
|
|
1428
|
-
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
|
|
1716
|
+
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
|
|
1429
1717
|
taskBranch = branch;
|
|
1430
1718
|
}
|
|
1431
1719
|
else {
|
|
1432
1720
|
baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
1721
|
+
if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
|
|
1722
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
|
|
1723
|
+
baseBranch: task.baseBranch,
|
|
1724
|
+
authorizedBaseCommit,
|
|
1725
|
+
currentBaseCommit: baseCommit,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1433
1728
|
taskBranch = currentBranch(identity.repositoryRoot);
|
|
1434
1729
|
if (!taskBranch || taskBranch === task.baseBranch) {
|
|
1435
1730
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
|
|
@@ -1441,14 +1736,14 @@ export class WorkflowService {
|
|
|
1441
1736
|
return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
|
|
1442
1737
|
}
|
|
1443
1738
|
runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1444
|
-
const context = this
|
|
1739
|
+
const context = this.#mutationContext(repository);
|
|
1445
1740
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1446
1741
|
assertExpectedRevision(task, expectedRevision);
|
|
1447
1742
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
1448
1743
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
1449
1744
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
1450
1745
|
}
|
|
1451
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1746
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1452
1747
|
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1453
1748
|
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1454
1749
|
if (currentPlanBlockingDecisions.length > 0) {
|
|
@@ -1470,6 +1765,7 @@ export class WorkflowService {
|
|
|
1470
1765
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
1471
1766
|
}
|
|
1472
1767
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1768
|
+
let resumesCorrectiveReplan = false;
|
|
1473
1769
|
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1474
1770
|
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} has remediation history but is no longer marked for guarded review by the current Plan Risk Audit; execution is blocked until the audit and Step posture are reconciled.`, {
|
|
1475
1771
|
taskId,
|
|
@@ -1484,20 +1780,21 @@ export class WorkflowService {
|
|
|
1484
1780
|
&& guardedPosture.correctiveDecision.planHash !== task.planHash
|
|
1485
1781
|
? guardedPosture.correctiveDecision
|
|
1486
1782
|
: null;
|
|
1487
|
-
const recovery = staleDecision
|
|
1783
|
+
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1488
1784
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1489
1785
|
: null;
|
|
1490
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1786
|
+
const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1787
|
+
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1491
1788
|
}
|
|
1492
1789
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1493
1790
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1494
|
-
if (step.status === 'planned' && dirty.length > 0) {
|
|
1791
|
+
if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
|
|
1495
1792
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1496
1793
|
changedFiles: dirty,
|
|
1497
1794
|
});
|
|
1498
1795
|
}
|
|
1499
1796
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
1500
|
-
if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
|
|
1797
|
+
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
1501
1798
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
1502
1799
|
changedFiles: dirtyOutsideStep,
|
|
1503
1800
|
});
|
|
@@ -1555,7 +1852,8 @@ export class WorkflowService {
|
|
|
1555
1852
|
}
|
|
1556
1853
|
return {
|
|
1557
1854
|
task: saved,
|
|
1558
|
-
lease,
|
|
1855
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1856
|
+
writerCredential: { disposition: acquiredHere ? 'issued' : 'renewed' },
|
|
1559
1857
|
envelope: {
|
|
1560
1858
|
protocolVersion: PROTOCOL_VERSION,
|
|
1561
1859
|
packageVersion: PACKAGE_VERSION,
|
|
@@ -1591,14 +1889,14 @@ export class WorkflowService {
|
|
|
1591
1889
|
taskId,
|
|
1592
1890
|
stepId,
|
|
1593
1891
|
expectedRevision: saved.revision,
|
|
1594
|
-
writerToken: lease.token,
|
|
1892
|
+
writerToken: acquiredHere ? lease.token : null,
|
|
1595
1893
|
knowledgeMap: knowledgeSummary(knowledgeMap),
|
|
1596
1894
|
knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
|
|
1597
1895
|
},
|
|
1598
1896
|
};
|
|
1599
1897
|
}
|
|
1600
1898
|
taskContext(repository, taskId, role) {
|
|
1601
|
-
const { identity } = this
|
|
1899
|
+
const { identity } = this.#mutationContext(repository);
|
|
1602
1900
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1603
1901
|
const historicalStrictReview = role === 'independent-reviewer'
|
|
1604
1902
|
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
@@ -1677,7 +1975,7 @@ export class WorkflowService {
|
|
|
1677
1975
|
};
|
|
1678
1976
|
}
|
|
1679
1977
|
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
|
|
1680
|
-
const context = this
|
|
1978
|
+
const context = this.#mutationContext(repository);
|
|
1681
1979
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1682
1980
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1683
1981
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1763,7 +2061,7 @@ export class WorkflowService {
|
|
|
1763
2061
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
1764
2062
|
}
|
|
1765
2063
|
reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
|
|
1766
|
-
const context = this
|
|
2064
|
+
const context = this.#mutationContext(repository);
|
|
1767
2065
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1768
2066
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1769
2067
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1819,10 +2117,18 @@ export class WorkflowService {
|
|
|
1819
2117
|
resolution: currentCycle.resolution,
|
|
1820
2118
|
});
|
|
1821
2119
|
}
|
|
1822
|
-
|
|
2120
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2121
|
+
if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2122
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
|
|
2123
|
+
taskId: task.id,
|
|
2124
|
+
stepId,
|
|
2125
|
+
infrastructureFailures,
|
|
2126
|
+
requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
1823
2129
|
}
|
|
1824
2130
|
else if (currentCycle.resolution !== 'pending') {
|
|
1825
|
-
if (currentCycle.resolution === 'failed'
|
|
2131
|
+
if (currentCycle.resolution === 'failed') {
|
|
1826
2132
|
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1827
2133
|
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1828
2134
|
taskId: task.id,
|
|
@@ -1830,7 +2136,7 @@ export class WorkflowService {
|
|
|
1830
2136
|
resolution: currentCycle.resolution,
|
|
1831
2137
|
});
|
|
1832
2138
|
}
|
|
1833
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2139
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
1834
2140
|
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
1835
2141
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
1836
2142
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
@@ -1848,7 +2154,7 @@ export class WorkflowService {
|
|
|
1848
2154
|
reviewedCommit: launch.reviewedCommit,
|
|
1849
2155
|
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
1850
2156
|
}, this.now());
|
|
1851
|
-
if (recorded.resolution === 'failed'
|
|
2157
|
+
if (recorded.resolution === 'failed') {
|
|
1852
2158
|
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
1853
2159
|
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
1854
2160
|
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
@@ -1858,7 +2164,7 @@ export class WorkflowService {
|
|
|
1858
2164
|
resolution: recorded.resolution,
|
|
1859
2165
|
});
|
|
1860
2166
|
}
|
|
1861
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2167
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
1862
2168
|
completionCommit: reviewRecordedEvent.completionCommit,
|
|
1863
2169
|
checksHash: reviewRecordedEvent.checksHash,
|
|
1864
2170
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
@@ -1867,8 +2173,132 @@ export class WorkflowService {
|
|
|
1867
2173
|
}
|
|
1868
2174
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
1869
2175
|
}
|
|
2176
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2177
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2178
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2179
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2180
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2181
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2182
|
+
}
|
|
2183
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2184
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2185
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
|
|
2186
|
+
}
|
|
2187
|
+
const step = requireStep(task, stepId);
|
|
2188
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
2189
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
|
|
2190
|
+
}
|
|
2191
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2192
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
|
|
2193
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2194
|
+
}
|
|
2195
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2196
|
+
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
|
|
2197
|
+
}
|
|
2198
|
+
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2199
|
+
const context = this.#mutationContext(repository);
|
|
2200
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2201
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2202
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2203
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2204
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
|
|
2205
|
+
let review;
|
|
2206
|
+
try {
|
|
2207
|
+
review = validateExternalStrictStepReviewInput(input, packet);
|
|
2208
|
+
}
|
|
2209
|
+
catch (error) {
|
|
2210
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
|
|
2211
|
+
}
|
|
2212
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2213
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
|
|
2214
|
+
}
|
|
2215
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
2216
|
+
if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
|
|
2217
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
|
|
2218
|
+
taskId,
|
|
2219
|
+
stepId,
|
|
2220
|
+
resolution: currentCycle.resolution,
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2224
|
+
if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2225
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
|
|
2226
|
+
taskId,
|
|
2227
|
+
stepId,
|
|
2228
|
+
infrastructureFailures,
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2232
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2233
|
+
review,
|
|
2234
|
+
reviewerOutputHash: strictReviewInputHash(review),
|
|
2235
|
+
reviewedCommit: packet.reviewedCommit,
|
|
2236
|
+
isolationResult: 'verified',
|
|
2237
|
+
}, this.now());
|
|
2238
|
+
if (recorded.resolution === 'failed') {
|
|
2239
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2240
|
+
completionCommit: recorded.reviewRecordedEvent.completionCommit,
|
|
2241
|
+
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2242
|
+
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2243
|
+
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2244
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2245
|
+
}
|
|
2246
|
+
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2247
|
+
return {
|
|
2248
|
+
mode: 'external-sealed',
|
|
2249
|
+
packetHash: packet.packetHash,
|
|
2250
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2251
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2252
|
+
task: saved,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision) {
|
|
2256
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2257
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2258
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2259
|
+
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
2260
|
+
if (task.status !== 'validating' && !retryingUnverified) {
|
|
2261
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2262
|
+
}
|
|
2263
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2264
|
+
assertTaskBranch(identity.repositoryRoot, task);
|
|
2265
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
2266
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
|
|
2267
|
+
}
|
|
2268
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2269
|
+
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2270
|
+
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2271
|
+
}
|
|
2272
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
2273
|
+
}
|
|
2274
|
+
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2275
|
+
const context = this.#mutationContext(repository);
|
|
2276
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2277
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2278
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2279
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2280
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
|
|
2281
|
+
let review;
|
|
2282
|
+
try {
|
|
2283
|
+
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
2284
|
+
}
|
|
2285
|
+
catch (error) {
|
|
2286
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
|
|
2287
|
+
}
|
|
2288
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2289
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
|
|
2290
|
+
}
|
|
2291
|
+
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2292
|
+
return {
|
|
2293
|
+
mode: 'external-sealed',
|
|
2294
|
+
packetHash: packet.packetHash,
|
|
2295
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2296
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2297
|
+
task: saved,
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
1870
2300
|
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1871
|
-
const context = this
|
|
2301
|
+
const context = this.#mutationContext(repository);
|
|
1872
2302
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1873
2303
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1874
2304
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1900,11 +2330,11 @@ export class WorkflowService {
|
|
|
1900
2330
|
return saved;
|
|
1901
2331
|
}
|
|
1902
2332
|
recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
|
|
1903
|
-
const { identity } = this
|
|
2333
|
+
const { identity } = this.#mutationContext(repository);
|
|
1904
2334
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1905
2335
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1906
2336
|
assertExpectedRevision(task, expectedRevision);
|
|
1907
|
-
this.proveOptionalWriterLeaseToken(this
|
|
2337
|
+
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
1908
2338
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1909
2339
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
1910
2340
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
@@ -1942,30 +2372,71 @@ export class WorkflowService {
|
|
|
1942
2372
|
return saved;
|
|
1943
2373
|
}
|
|
1944
2374
|
recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1945
|
-
const context = this
|
|
2375
|
+
const context = this.#mutationContext(repository);
|
|
1946
2376
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1947
2377
|
assertExpectedRevision(task, expectedRevision);
|
|
1948
2378
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
1949
2379
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
1950
2380
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
|
|
1951
2381
|
}
|
|
1952
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
2382
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1953
2383
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1954
2384
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1955
2385
|
}
|
|
1956
2386
|
recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1957
|
-
const context = this
|
|
2387
|
+
const context = this.#mutationContext(repository);
|
|
1958
2388
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1959
2389
|
assertExpectedRevision(task, expectedRevision);
|
|
1960
2390
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
1961
2391
|
return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1962
2392
|
}
|
|
2393
|
+
recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
2394
|
+
const context = this.#mutationContext(repository);
|
|
2395
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2396
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2397
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2398
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2399
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2400
|
+
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2401
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2402
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2403
|
+
const handoff = readTaskC1Posture(this.store, task);
|
|
2404
|
+
const requiredActor = handoff.state === 'claimed'
|
|
2405
|
+
? handoff.claimant
|
|
2406
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
2407
|
+
if (actor.trim() !== requiredActor) {
|
|
2408
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
|
|
2409
|
+
taskId,
|
|
2410
|
+
stepId,
|
|
2411
|
+
requiredActor,
|
|
2412
|
+
actualActor: actor.trim(),
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2416
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2417
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
|
|
2418
|
+
taskId,
|
|
2419
|
+
stepId,
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
|
|
2423
|
+
if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
|
|
2424
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
|
|
2425
|
+
taskId,
|
|
2426
|
+
stepId,
|
|
2427
|
+
applicable: assessment.applicable,
|
|
2428
|
+
blockers: assessment.blockers,
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2432
|
+
return { assessment, event };
|
|
2433
|
+
}
|
|
1963
2434
|
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
1964
|
-
const { identity } = this
|
|
2435
|
+
const { identity } = this.#mutationContext(repository);
|
|
1965
2436
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1966
2437
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1967
2438
|
assertExpectedRevision(task, expectedRevision);
|
|
1968
|
-
this.proveOptionalWriterLeaseToken(this
|
|
2439
|
+
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
1969
2440
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1970
2441
|
if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
1971
2442
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
@@ -2007,7 +2478,7 @@ export class WorkflowService {
|
|
|
2007
2478
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
2008
2479
|
}
|
|
2009
2480
|
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
2010
|
-
const { identity } = this
|
|
2481
|
+
const { identity } = this.#mutationContext(repository);
|
|
2011
2482
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2012
2483
|
assertExpectedRevision(task, expectedRevision);
|
|
2013
2484
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
@@ -2021,7 +2492,7 @@ export class WorkflowService {
|
|
|
2021
2492
|
return saved;
|
|
2022
2493
|
}
|
|
2023
2494
|
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
2024
|
-
const { identity } = this
|
|
2495
|
+
const { identity } = this.#mutationContext(repository);
|
|
2025
2496
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2026
2497
|
assertExpectedRevision(task, expectedRevision);
|
|
2027
2498
|
if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
|
|
@@ -2059,7 +2530,7 @@ export class WorkflowService {
|
|
|
2059
2530
|
}, expectedRevision);
|
|
2060
2531
|
}
|
|
2061
2532
|
syncTaskBase(repository, taskId, expectedRevision) {
|
|
2062
|
-
const { identity } = this
|
|
2533
|
+
const { identity } = this.#mutationContext(repository);
|
|
2063
2534
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2064
2535
|
assertExpectedRevision(task, expectedRevision);
|
|
2065
2536
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
|
|
@@ -2111,7 +2582,7 @@ export class WorkflowService {
|
|
|
2111
2582
|
return saved;
|
|
2112
2583
|
}
|
|
2113
2584
|
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
2114
|
-
const context = this
|
|
2585
|
+
const context = this.#mutationContext(repository);
|
|
2115
2586
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2116
2587
|
assertExpectedRevision(task, expectedRevision);
|
|
2117
2588
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -2133,7 +2604,7 @@ export class WorkflowService {
|
|
|
2133
2604
|
return saved;
|
|
2134
2605
|
}
|
|
2135
2606
|
confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
|
|
2136
|
-
const context = this
|
|
2607
|
+
const context = this.#mutationContext(repository);
|
|
2137
2608
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2138
2609
|
assertExpectedRevision(task, expectedRevision);
|
|
2139
2610
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -2150,21 +2621,22 @@ export class WorkflowService {
|
|
|
2150
2621
|
return saved;
|
|
2151
2622
|
}
|
|
2152
2623
|
status(repository) {
|
|
2153
|
-
const
|
|
2154
|
-
const tasks = this.store.listTasks(identity.projectId);
|
|
2155
|
-
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
2156
|
-
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
|
|
2624
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2157
2625
|
return {
|
|
2158
|
-
projectId: identity.projectId,
|
|
2159
|
-
discoveries:
|
|
2160
|
-
tasks,
|
|
2161
|
-
milestones:
|
|
2162
|
-
|
|
2163
|
-
|
|
2626
|
+
projectId: snapshot.identity.projectId,
|
|
2627
|
+
discoveries: snapshot.discoveries,
|
|
2628
|
+
tasks: snapshot.tasks,
|
|
2629
|
+
milestones: snapshot.milestones,
|
|
2630
|
+
milestoneInitialAssembly: snapshot.milestones
|
|
2631
|
+
.map((milestone) => this.describeMilestoneInitialAssembly(snapshot.identity.projectId, milestone, snapshot.tasks))
|
|
2632
|
+
.filter((value) => value !== null),
|
|
2633
|
+
delegations: snapshot.delegations,
|
|
2634
|
+
adoption: snapshot.adoption,
|
|
2635
|
+
observation: snapshot.observation,
|
|
2164
2636
|
};
|
|
2165
2637
|
}
|
|
2166
2638
|
recoverMilestoneTransactions(repository) {
|
|
2167
|
-
const { identity } = this
|
|
2639
|
+
const { identity } = this.#mutationContext(repository);
|
|
2168
2640
|
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
2169
2641
|
return {
|
|
2170
2642
|
projectId: identity.projectId,
|
|
@@ -2172,11 +2644,11 @@ export class WorkflowService {
|
|
|
2172
2644
|
};
|
|
2173
2645
|
}
|
|
2174
2646
|
prepareAdoption(repository) {
|
|
2175
|
-
const { identity } = this
|
|
2647
|
+
const { identity } = this.#mutationContext(repository);
|
|
2176
2648
|
return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
|
|
2177
2649
|
}
|
|
2178
2650
|
applyAdoption(repository, actor, confirmationCode) {
|
|
2179
|
-
const context = this
|
|
2651
|
+
const context = this.#mutationContext(repository);
|
|
2180
2652
|
const tasks = context.store.listTasks(context.identity.projectId);
|
|
2181
2653
|
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
2182
2654
|
return applyAdoptionPosture(this.store, {
|
|
@@ -2193,19 +2665,19 @@ export class WorkflowService {
|
|
|
2193
2665
|
}, actor, confirmationCode);
|
|
2194
2666
|
}
|
|
2195
2667
|
updatePreflight(repository) {
|
|
2196
|
-
const
|
|
2197
|
-
const tasks =
|
|
2198
|
-
const milestones =
|
|
2199
|
-
const dirty = changedFiles(
|
|
2668
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2669
|
+
const tasks = snapshot.tasks;
|
|
2670
|
+
const milestones = snapshot.milestones;
|
|
2671
|
+
const dirty = changedFiles(snapshot.identity.repositoryRoot);
|
|
2200
2672
|
const runningSteps = tasks.flatMap((task) => task.steps
|
|
2201
2673
|
.filter((step) => step.status === 'in_progress')
|
|
2202
2674
|
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
2203
|
-
const leases =
|
|
2675
|
+
const leases = snapshot.leases;
|
|
2204
2676
|
const now = this.now().getTime();
|
|
2205
2677
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
2206
2678
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2207
2679
|
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2208
|
-
const inspectedVersions = inspectWorkflowVersions(
|
|
2680
|
+
const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
2209
2681
|
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
2210
2682
|
...base,
|
|
2211
2683
|
milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
|
|
@@ -2232,31 +2704,20 @@ export class WorkflowService {
|
|
|
2232
2704
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
2233
2705
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2234
2706
|
...versionBlockers,
|
|
2707
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
2235
2708
|
];
|
|
2236
|
-
const adoption = buildAdoptionPreparation({
|
|
2237
|
-
projectId: context.identity.projectId,
|
|
2238
|
-
repositoryIdentity: context.identity,
|
|
2239
|
-
packageVersion: PACKAGE_VERSION,
|
|
2240
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
2241
|
-
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
2242
|
-
tasks,
|
|
2243
|
-
milestones,
|
|
2244
|
-
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
2245
|
-
blockers,
|
|
2246
|
-
now: this.now(),
|
|
2247
|
-
}, readCurrentAdoptionPosture(this.store, context.identity.projectId));
|
|
2248
2709
|
return {
|
|
2249
2710
|
safe: blockers.length === 0,
|
|
2250
2711
|
packageVersion: PACKAGE_VERSION,
|
|
2251
2712
|
protocolVersion: PROTOCOL_VERSION,
|
|
2252
2713
|
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
2253
|
-
projectId:
|
|
2254
|
-
repositoryRoot:
|
|
2714
|
+
projectId: snapshot.identity.projectId,
|
|
2715
|
+
repositoryRoot: snapshot.identity.repositoryRoot,
|
|
2255
2716
|
clean: dirty.length === 0,
|
|
2256
2717
|
changedFiles: dirty,
|
|
2257
2718
|
runningSteps,
|
|
2258
|
-
activeLeases,
|
|
2259
|
-
staleLeases,
|
|
2719
|
+
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2720
|
+
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2260
2721
|
blockers,
|
|
2261
2722
|
workflowVersions: {
|
|
2262
2723
|
runtime: inspectedVersions.runtime,
|
|
@@ -2272,11 +2733,134 @@ export class WorkflowService {
|
|
|
2272
2733
|
? null
|
|
2273
2734
|
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
2274
2735
|
},
|
|
2275
|
-
adoption,
|
|
2736
|
+
adoption: snapshot.adoption,
|
|
2737
|
+
observation: snapshot.observation,
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
dependencyProvenanceRecoveryPreflight(repository, taskId) {
|
|
2741
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2742
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
2743
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency provenance recovery requires a coherent observation boundary.', {
|
|
2744
|
+
observation: snapshot.observation,
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2748
|
+
if (!task)
|
|
2749
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2750
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2751
|
+
}
|
|
2752
|
+
recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
2753
|
+
const normalizedActor = actor.trim();
|
|
2754
|
+
const normalizedReason = reason.trim();
|
|
2755
|
+
if (!normalizedActor)
|
|
2756
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires an actor.');
|
|
2757
|
+
if (!normalizedReason)
|
|
2758
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires a reason.');
|
|
2759
|
+
const context = this.#mutationContext(repository);
|
|
2760
|
+
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
2761
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2762
|
+
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead && record.packageVersion === PACKAGE_VERSION);
|
|
2763
|
+
if (prior && current.systemCommits.includes(currentHead))
|
|
2764
|
+
return current;
|
|
2765
|
+
assertExpectedRevision(current, expectedRevision);
|
|
2766
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2767
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2768
|
+
if (!task)
|
|
2769
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2770
|
+
const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2771
|
+
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
2772
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
2773
|
+
taskId,
|
|
2774
|
+
blockers: preflight.blockers,
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
return context.store.writeTask({
|
|
2778
|
+
...task,
|
|
2779
|
+
systemCommits: [...task.systemCommits, preflight.commitSha],
|
|
2780
|
+
dependencyProvenanceRecoveries: [
|
|
2781
|
+
...(task.dependencyProvenanceRecoveries ?? []),
|
|
2782
|
+
{
|
|
2783
|
+
commitSha: preflight.commitSha,
|
|
2784
|
+
parentCommitSha: preflight.parentCommitSha,
|
|
2785
|
+
packageName: PACKAGE_NAME,
|
|
2786
|
+
packageVersion: PACKAGE_VERSION,
|
|
2787
|
+
files: ['package-lock.json', 'package.json'],
|
|
2788
|
+
actor: normalizedActor,
|
|
2789
|
+
reason: normalizedReason,
|
|
2790
|
+
recordedAt: this.now().toISOString(),
|
|
2791
|
+
retainedHistoricalCommits: preflight.retainedHistoricalCommits,
|
|
2792
|
+
},
|
|
2793
|
+
],
|
|
2794
|
+
}, expectedRevision);
|
|
2795
|
+
}
|
|
2796
|
+
milestoneProgress(repository, milestoneId) {
|
|
2797
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2798
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
2799
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone progress requires a coherent read-only observation boundary.', {
|
|
2800
|
+
observation: snapshot.observation,
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
const milestone = snapshot.milestones.find((candidate) => candidate.id === milestoneId);
|
|
2804
|
+
if (!milestone)
|
|
2805
|
+
throw new WorkflowError('NOT_FOUND', `Milestone ${milestoneId} was not found in this repository.`);
|
|
2806
|
+
const tasksById = new Map(snapshot.tasks.map((task) => [task.id, task]));
|
|
2807
|
+
const tasks = milestone.memberships.map((membership, index) => {
|
|
2808
|
+
const task = tasksById.get(membership.taskId);
|
|
2809
|
+
if (!task)
|
|
2810
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
|
|
2811
|
+
const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
|
|
2812
|
+
const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
2813
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
2814
|
+
if (!candidate)
|
|
2815
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
2816
|
+
return candidate;
|
|
2817
|
+
});
|
|
2818
|
+
const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
|
|
2819
|
+
return {
|
|
2820
|
+
ordinal: `T${String(index + 1).padStart(2, '0')}`,
|
|
2821
|
+
taskId: task.id,
|
|
2822
|
+
title: task.title,
|
|
2823
|
+
status: task.status,
|
|
2824
|
+
revision: task.revision,
|
|
2825
|
+
disposition: membership.disposition,
|
|
2826
|
+
membershipReason: membership.reason,
|
|
2827
|
+
dependencyState: dependency.dependencyState,
|
|
2828
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
2829
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
2830
|
+
runnable: executionEligible && dependency.runnable,
|
|
2831
|
+
dependencyBlockReason: !executionEligible
|
|
2832
|
+
? milestone.status !== 'active'
|
|
2833
|
+
? `Milestone is ${milestone.status}, not active.`
|
|
2834
|
+
: `Task is terminal (${task.status}).`
|
|
2835
|
+
: dependency.blockReason,
|
|
2836
|
+
replacementForTaskId: task.replacementForTaskId ?? null,
|
|
2837
|
+
replacedByTaskId: task.replacedByTaskId ?? null,
|
|
2838
|
+
stepsCompleted,
|
|
2839
|
+
stepsTotal: task.steps.length,
|
|
2840
|
+
steps: task.steps.map((step, stepIndex) => ({
|
|
2841
|
+
ordinal: `S${String(stepIndex + 1).padStart(2, '0')}`,
|
|
2842
|
+
stepId: step.id,
|
|
2843
|
+
title: step.title,
|
|
2844
|
+
status: step.status,
|
|
2845
|
+
})),
|
|
2846
|
+
};
|
|
2847
|
+
});
|
|
2848
|
+
const required = tasks.filter((task) => task.disposition === 'required');
|
|
2849
|
+
return {
|
|
2850
|
+
readOnly: true,
|
|
2851
|
+
projectId: snapshot.identity.projectId,
|
|
2852
|
+
milestoneId: milestone.id,
|
|
2853
|
+
milestoneTitle: milestone.title,
|
|
2854
|
+
milestoneStatus: milestone.status,
|
|
2855
|
+
milestoneRevision: milestone.revision,
|
|
2856
|
+
membershipRevision: milestone.membershipRevision,
|
|
2857
|
+
tasksCompleted: required.filter((task) => task.status === 'merged').length,
|
|
2858
|
+
tasksRequired: required.length,
|
|
2859
|
+
tasks,
|
|
2276
2860
|
};
|
|
2277
2861
|
}
|
|
2278
2862
|
alpha6StrictReviewRescuePreflight(repository) {
|
|
2279
|
-
const context = this
|
|
2863
|
+
const context = this.#mutationContext(repository);
|
|
2280
2864
|
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
2281
2865
|
const tasks = context.store.listTasks(context.identity.projectId);
|
|
2282
2866
|
const leases = context.locks.list();
|
|
@@ -2371,15 +2955,15 @@ export class WorkflowService {
|
|
|
2371
2955
|
projectId: context.identity.projectId,
|
|
2372
2956
|
repositoryRoot: context.identity.repositoryRoot,
|
|
2373
2957
|
clean: dirty.length === 0,
|
|
2374
|
-
activeLeases,
|
|
2375
|
-
staleLeases,
|
|
2958
|
+
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2959
|
+
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2376
2960
|
candidate,
|
|
2377
2961
|
blockers,
|
|
2378
2962
|
requiredActions,
|
|
2379
2963
|
};
|
|
2380
2964
|
}
|
|
2381
2965
|
scanKnowledgeMap(repository) {
|
|
2382
|
-
const { identity } = this
|
|
2966
|
+
const { identity } = this.#mutationContext(repository);
|
|
2383
2967
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2384
2968
|
try {
|
|
2385
2969
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
@@ -2392,11 +2976,11 @@ export class WorkflowService {
|
|
|
2392
2976
|
}
|
|
2393
2977
|
}
|
|
2394
2978
|
showKnowledgeMap(repository) {
|
|
2395
|
-
const { identity } = this
|
|
2979
|
+
const { identity } = this.#mutationContext(repository);
|
|
2396
2980
|
return this.store.readKnowledgeMap(identity.projectId);
|
|
2397
2981
|
}
|
|
2398
2982
|
knowledgeMapStatus(repository) {
|
|
2399
|
-
const { identity } = this
|
|
2983
|
+
const { identity } = this.#mutationContext(repository);
|
|
2400
2984
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2401
2985
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2402
2986
|
return inspectKnowledgeMap(current, scanned);
|
|
@@ -2405,7 +2989,7 @@ export class WorkflowService {
|
|
|
2405
2989
|
return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
|
|
2406
2990
|
}
|
|
2407
2991
|
approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
|
|
2408
|
-
const { identity } = this
|
|
2992
|
+
const { identity } = this.#mutationContext(repository);
|
|
2409
2993
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2410
2994
|
assertExpectedRevision(current, expectedRevision);
|
|
2411
2995
|
const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
@@ -2422,43 +3006,35 @@ export class WorkflowService {
|
|
|
2422
3006
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
2423
3007
|
}
|
|
2424
3008
|
reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
|
|
2425
|
-
const { identity } = this
|
|
3009
|
+
const { identity } = this.#mutationContext(repository);
|
|
2426
3010
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2427
3011
|
assertExpectedRevision(current, expectedRevision);
|
|
2428
3012
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2429
3013
|
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2430
3014
|
const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
|
|
2439
|
-
if (assessment.eligible
|
|
2440
|
-
&& this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
|
|
2441
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2442
|
-
taskId: activeTask.id,
|
|
2443
|
-
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
3015
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
3016
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
|
|
3017
|
+
if (refreshOwner) {
|
|
3018
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
3019
|
+
taskId: refreshOwner.task.id,
|
|
3020
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
3021
|
+
});
|
|
2446
3022
|
}
|
|
2447
3023
|
}
|
|
2448
3024
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
2449
3025
|
}
|
|
2450
3026
|
graphRefreshRequest(repository, graphKind, mode = 'moderate') {
|
|
2451
|
-
const { identity } = this
|
|
3027
|
+
const { identity } = this.#mutationContext(repository);
|
|
2452
3028
|
return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2453
3029
|
}
|
|
2454
3030
|
graphStatus(repository, graphKind, mode = 'moderate') {
|
|
2455
|
-
const { identity } = this
|
|
3031
|
+
const { identity } = this.#mutationContext(repository);
|
|
2456
3032
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2457
3033
|
const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2458
3034
|
return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
|
|
2459
3035
|
}
|
|
2460
3036
|
bindGraph(repository, expectedRevision, request, evidence) {
|
|
2461
|
-
const { identity } = this
|
|
3037
|
+
const { identity } = this.#mutationContext(repository);
|
|
2462
3038
|
const currentRequest = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, request.graphKind, request.mode, this.now());
|
|
2463
3039
|
validateGraphRequestCurrent(request, currentRequest);
|
|
2464
3040
|
const existing = this.readGraphBindingOrNull(identity.projectId, request.graphKind);
|
|
@@ -2466,13 +3042,78 @@ export class WorkflowService {
|
|
|
2466
3042
|
return this.store.writeGraphBinding(binding, expectedRevision);
|
|
2467
3043
|
}
|
|
2468
3044
|
recordGraphFallback(repository, graphKind, mode, expectedRevision, reason) {
|
|
2469
|
-
const { identity } = this
|
|
3045
|
+
const { identity } = this.#mutationContext(repository);
|
|
2470
3046
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2471
3047
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2472
3048
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
2473
3049
|
}
|
|
2474
|
-
next(repository) {
|
|
3050
|
+
next(repository, taskId = null) {
|
|
2475
3051
|
const status = this.status(repository);
|
|
3052
|
+
if (status.observation.kind === 'blocked') {
|
|
3053
|
+
return {
|
|
3054
|
+
scope: 'project',
|
|
3055
|
+
projectId: status.projectId,
|
|
3056
|
+
status: 'observation-blocked',
|
|
3057
|
+
action: 'blocked',
|
|
3058
|
+
observation: status.observation,
|
|
3059
|
+
navigation: {
|
|
3060
|
+
kind: 'blocked',
|
|
3061
|
+
reasonCodes: status.observation.reasonCodes,
|
|
3062
|
+
availableRepairTransitionId: null,
|
|
3063
|
+
},
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
if (status.observation.kind === 'repair') {
|
|
3067
|
+
const repair = status.observation.repair;
|
|
3068
|
+
return {
|
|
3069
|
+
scope: 'project',
|
|
3070
|
+
projectId: status.projectId,
|
|
3071
|
+
status: 'repair-required',
|
|
3072
|
+
action: repair?.command ?? 'blocked',
|
|
3073
|
+
observation: status.observation,
|
|
3074
|
+
navigation: {
|
|
3075
|
+
kind: repair ? 'repair' : 'blocked',
|
|
3076
|
+
transitionId: null,
|
|
3077
|
+
availableRepairTransitionId: null,
|
|
3078
|
+
...(repair ? {
|
|
3079
|
+
command: repair.command,
|
|
3080
|
+
cataloged: repair.cataloged,
|
|
3081
|
+
...('available' in repair ? { available: repair.available } : {}),
|
|
3082
|
+
bindings: repair.command === 'milestone recover'
|
|
3083
|
+
? {
|
|
3084
|
+
projectId: repair.projectId,
|
|
3085
|
+
milestoneIds: repair.milestoneIds,
|
|
3086
|
+
transactionIds: repair.transactionIds,
|
|
3087
|
+
}
|
|
3088
|
+
: 'taskIds' in repair
|
|
3089
|
+
? {
|
|
3090
|
+
projectId: repair.projectId,
|
|
3091
|
+
taskIds: repair.taskIds,
|
|
3092
|
+
transactionIds: repair.transactionIds,
|
|
3093
|
+
}
|
|
3094
|
+
: {
|
|
3095
|
+
projectId: repair.projectId,
|
|
3096
|
+
entityIds: repair.entityIds,
|
|
3097
|
+
},
|
|
3098
|
+
} : {}),
|
|
3099
|
+
reasonCodes: status.observation.reasonCodes,
|
|
3100
|
+
},
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
if (!status.adoption) {
|
|
3104
|
+
return {
|
|
3105
|
+
scope: 'project',
|
|
3106
|
+
projectId: status.projectId,
|
|
3107
|
+
status: 'observation-blocked',
|
|
3108
|
+
action: 'blocked',
|
|
3109
|
+
observation: status.observation,
|
|
3110
|
+
navigation: {
|
|
3111
|
+
kind: 'blocked',
|
|
3112
|
+
reasonCodes: status.observation.reasonCodes,
|
|
3113
|
+
availableRepairTransitionId: null,
|
|
3114
|
+
},
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
2476
3117
|
const adoptionInfo = {
|
|
2477
3118
|
status: status.adoption.status,
|
|
2478
3119
|
safe: status.adoption.safe,
|
|
@@ -2490,19 +3131,130 @@ export class WorkflowService {
|
|
|
2490
3131
|
status: status.adoption.status,
|
|
2491
3132
|
action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2492
3133
|
adoption: adoptionInfo,
|
|
3134
|
+
observation: status.observation,
|
|
3135
|
+
navigation: status.adoption.safe
|
|
3136
|
+
? {
|
|
3137
|
+
kind: 'human-gate',
|
|
3138
|
+
transitionId: null,
|
|
3139
|
+
cataloged: false,
|
|
3140
|
+
command: 'state adoption-apply',
|
|
3141
|
+
bindings: {
|
|
3142
|
+
projectId: status.projectId,
|
|
3143
|
+
confirmationCode: status.adoption.confirmationCode,
|
|
3144
|
+
confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
|
|
3145
|
+
actorRequired: true,
|
|
3146
|
+
},
|
|
3147
|
+
}
|
|
3148
|
+
: {
|
|
3149
|
+
kind: 'blocked',
|
|
3150
|
+
transitionId: null,
|
|
3151
|
+
availableRepairTransitionId: null,
|
|
3152
|
+
reasonCodes: ['ADOPTION_MISSING'],
|
|
3153
|
+
blockers: status.adoption.blockers,
|
|
3154
|
+
},
|
|
2493
3155
|
};
|
|
2494
3156
|
}
|
|
2495
|
-
const withAdoption = (value) => ({
|
|
3157
|
+
const withAdoption = (value) => ({
|
|
3158
|
+
...value,
|
|
3159
|
+
adoption: adoptionInfo,
|
|
3160
|
+
observation: status.observation,
|
|
3161
|
+
});
|
|
2496
3162
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
3163
|
+
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
2497
3164
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
2498
|
-
const
|
|
3165
|
+
const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
|
|
3166
|
+
const requestedTask = taskId === null
|
|
3167
|
+
? null
|
|
3168
|
+
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
3169
|
+
if (taskId !== null && !requestedTask) {
|
|
3170
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3171
|
+
}
|
|
3172
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
|
|
3173
|
+
const requestedMilestone = requestedTask.milestoneId
|
|
3174
|
+
? milestonesById.get(requestedTask.milestoneId) ?? null
|
|
3175
|
+
: null;
|
|
3176
|
+
const dependency = requestedMilestone
|
|
3177
|
+
? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
|
|
3178
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
3179
|
+
if (!candidate)
|
|
3180
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3181
|
+
return candidate;
|
|
3182
|
+
})
|
|
3183
|
+
: null;
|
|
3184
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
3185
|
+
taskId: requestedTask.id,
|
|
3186
|
+
taskStatus: requestedTask.status,
|
|
3187
|
+
milestoneId: requestedTask.milestoneId,
|
|
3188
|
+
...(dependency ? { dependency } : {}),
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
const activeTask = requestedTask
|
|
3192
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
2499
3193
|
if (activeTask) {
|
|
2500
|
-
const
|
|
2501
|
-
|
|
3194
|
+
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3195
|
+
if (dependencyRecovery.eligible) {
|
|
3196
|
+
return withAdoption({
|
|
3197
|
+
scope: 'task',
|
|
3198
|
+
id: activeTask.id,
|
|
3199
|
+
status: activeTask.status,
|
|
3200
|
+
action: 'update dependency-provenance-recover',
|
|
3201
|
+
revision: activeTask.revision,
|
|
3202
|
+
dependencyProvenanceRecovery: dependencyRecovery,
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
const activeTaskMilestone = activeTask.milestoneId
|
|
3206
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
2502
3207
|
: null;
|
|
3208
|
+
const milestoneCancellationSafe = Boolean(activeTaskMilestone
|
|
3209
|
+
&& !activeTask.steps.some((step) => step.status === 'in_progress')
|
|
3210
|
+
&& status.observation.leases.length === 0
|
|
3211
|
+
&& status.observation.coreTaskOperations.length === 0
|
|
3212
|
+
&& status.observation.pendingProjectTransactions.length === 0
|
|
3213
|
+
&& status.observation.pendingMilestoneTransactions.length === 0
|
|
3214
|
+
&& status.observation.pendingTaskTransactions.length === 0
|
|
3215
|
+
&& status.observation.corruptTaskTransactions.length === 0);
|
|
3216
|
+
const withMilestoneCancellation = (value) => (milestoneCancellationSafe
|
|
3217
|
+
? {
|
|
3218
|
+
...value,
|
|
3219
|
+
milestoneCancellationOption: {
|
|
3220
|
+
command: 'milestone cancel',
|
|
3221
|
+
milestoneId: activeTaskMilestone.id,
|
|
3222
|
+
expectedRevision: activeTaskMilestone.revision,
|
|
3223
|
+
reasonRequired: true,
|
|
3224
|
+
taskDisposition: 'retained-historical',
|
|
3225
|
+
},
|
|
3226
|
+
}
|
|
3227
|
+
: value);
|
|
3228
|
+
let activePlanRiskAudit = null;
|
|
3229
|
+
let planRiskAuditRecovery = null;
|
|
3230
|
+
if (activeTask.planHash) {
|
|
3231
|
+
try {
|
|
3232
|
+
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
3233
|
+
}
|
|
3234
|
+
catch (error) {
|
|
3235
|
+
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3236
|
+
throw error;
|
|
3237
|
+
planRiskAuditRecovery = {
|
|
3238
|
+
scope: 'task',
|
|
3239
|
+
id: activeTask.id,
|
|
3240
|
+
status: activeTask.status,
|
|
3241
|
+
action: 'task plan-risk-audit',
|
|
3242
|
+
revision: activeTask.revision,
|
|
3243
|
+
planRiskAuditRecovery: {
|
|
3244
|
+
state: 'required',
|
|
3245
|
+
reason: error.message,
|
|
3246
|
+
currentBriefHash: activeTask.briefHash,
|
|
3247
|
+
currentPlanHash: activeTask.planHash,
|
|
3248
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
3249
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
3250
|
+
},
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
2503
3254
|
const taskNext = activeTask.status === 'planning'
|
|
2504
3255
|
? null
|
|
2505
|
-
:
|
|
3256
|
+
: planRiskAuditRecovery
|
|
3257
|
+
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
2506
3258
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
2507
3259
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2508
3260
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -2511,15 +3263,32 @@ export class WorkflowService {
|
|
|
2511
3263
|
if (taskNext?.action === 'task step-complete'
|
|
2512
3264
|
|| taskNext?.action === 'task step-review'
|
|
2513
3265
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
3266
|
+
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3267
|
+
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
2514
3268
|
|| hardStop
|
|
2515
3269
|
|| terminalCorrectiveDecision === 'split-required'
|
|
2516
3270
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2517
|
-
return withAdoption(taskNext);
|
|
3271
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
2518
3272
|
}
|
|
2519
3273
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
2520
|
-
return withAdoption(taskNext);
|
|
3274
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
3275
|
+
}
|
|
3276
|
+
let knowledgeMap;
|
|
3277
|
+
try {
|
|
3278
|
+
knowledgeMap = inspectCurrentKnowledgeMap();
|
|
3279
|
+
}
|
|
3280
|
+
catch (error) {
|
|
3281
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3282
|
+
throw error;
|
|
3283
|
+
return withAdoption(withMilestoneCancellation({
|
|
3284
|
+
scope: 'project-memory',
|
|
3285
|
+
status: 'missing',
|
|
3286
|
+
action: 'project-memory scan',
|
|
3287
|
+
stalenessOriginTaskId: activeTask.id,
|
|
3288
|
+
authorizesTask: false,
|
|
3289
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
3290
|
+
}));
|
|
2521
3291
|
}
|
|
2522
|
-
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
2523
3292
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
2524
3293
|
const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
|
|
2525
3294
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
@@ -2535,7 +3304,17 @@ export class WorkflowService {
|
|
|
2535
3304
|
const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
|
|
2536
3305
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2537
3306
|
: [];
|
|
2538
|
-
|
|
3307
|
+
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
3308
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
|
|
3309
|
+
if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
|
|
3310
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
|
|
3311
|
+
taskId: requestedTask.id,
|
|
3312
|
+
blockingTaskId: refreshOwner.task.id,
|
|
3313
|
+
requiredAction: 'Run repository next and its exact task context-refresh action first.',
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return withAdoption(withMilestoneCancellation({
|
|
2539
3318
|
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2540
3319
|
status: knowledgeMap.status,
|
|
2541
3320
|
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
@@ -2567,14 +3346,14 @@ export class WorkflowService {
|
|
|
2567
3346
|
},
|
|
2568
3347
|
}
|
|
2569
3348
|
: {}),
|
|
2570
|
-
});
|
|
3349
|
+
}));
|
|
2571
3350
|
}
|
|
2572
3351
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2573
3352
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
2574
3353
|
activeTask.planHash &&
|
|
2575
3354
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
2576
3355
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
2577
|
-
return withAdoption({
|
|
3356
|
+
return withAdoption(withMilestoneCancellation({
|
|
2578
3357
|
scope: 'task',
|
|
2579
3358
|
id: activeTask.id,
|
|
2580
3359
|
status: activeTask.status,
|
|
@@ -2582,12 +3361,69 @@ export class WorkflowService {
|
|
|
2582
3361
|
revision: activeTask.revision,
|
|
2583
3362
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
2584
3363
|
knowledgeMapHash,
|
|
2585
|
-
});
|
|
3364
|
+
}));
|
|
2586
3365
|
}
|
|
2587
|
-
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
3366
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
|
|
3367
|
+
}
|
|
3368
|
+
let repositoryKnowledgeMap;
|
|
3369
|
+
try {
|
|
3370
|
+
repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
|
|
3371
|
+
}
|
|
3372
|
+
catch (error) {
|
|
3373
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3374
|
+
throw error;
|
|
3375
|
+
return withAdoption({
|
|
3376
|
+
scope: 'project-memory',
|
|
3377
|
+
status: 'missing',
|
|
3378
|
+
action: 'project-memory scan',
|
|
3379
|
+
authorizesMilestone: false,
|
|
3380
|
+
provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
|
|
3384
|
+
const action = repositoryKnowledgeMap.status === 'stale'
|
|
3385
|
+
? 'project-memory reconcile'
|
|
3386
|
+
: 'project-memory approve';
|
|
3387
|
+
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
3388
|
+
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
|
|
3389
|
+
: [];
|
|
3390
|
+
return withAdoption({
|
|
3391
|
+
scope: 'project-memory',
|
|
3392
|
+
status: repositoryKnowledgeMap.status,
|
|
3393
|
+
action,
|
|
3394
|
+
revision: repositoryKnowledgeMap.revision,
|
|
3395
|
+
authorizesMilestone: false,
|
|
3396
|
+
provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
|
|
3397
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3398
|
+
});
|
|
2588
3399
|
}
|
|
2589
3400
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2590
3401
|
if (activeMilestone) {
|
|
3402
|
+
const initialAssembly = status.milestoneInitialAssembly
|
|
3403
|
+
.find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
|
|
3404
|
+
if (initialAssembly) {
|
|
3405
|
+
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
3406
|
+
if (activeDiscovery) {
|
|
3407
|
+
return withAdoption({
|
|
3408
|
+
scope: 'discovery',
|
|
3409
|
+
id: activeDiscovery.id,
|
|
3410
|
+
status: activeDiscovery.status,
|
|
3411
|
+
action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
|
|
3412
|
+
readiness: assessDiscovery(activeDiscovery),
|
|
3413
|
+
milestoneInitialAssembly: initialAssembly,
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
return withAdoption({
|
|
3417
|
+
scope: 'milestone',
|
|
3418
|
+
id: activeMilestone.id,
|
|
3419
|
+
status: activeMilestone.status,
|
|
3420
|
+
action: 'milestone initial-assembly',
|
|
3421
|
+
revision: activeMilestone.revision,
|
|
3422
|
+
linkedTaskIds: initialAssembly.linkedTaskIds,
|
|
3423
|
+
executionAvailable: initialAssembly.executionAvailable,
|
|
3424
|
+
milestoneInitialAssembly: initialAssembly,
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
2591
3427
|
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
2592
3428
|
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
2593
3429
|
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
@@ -2638,6 +3474,19 @@ export class WorkflowService {
|
|
|
2638
3474
|
status: activeMilestone.status,
|
|
2639
3475
|
action: nextForMilestone(activeMilestone),
|
|
2640
3476
|
revision: activeMilestone.revision,
|
|
3477
|
+
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
3478
|
+
? {
|
|
3479
|
+
milestoneAuthorizationOptions: [
|
|
3480
|
+
{ action: 'milestone authorize', mode: 'direct-human' },
|
|
3481
|
+
{
|
|
3482
|
+
action: 'milestone autonomy-prepare',
|
|
3483
|
+
mode: 'delegated',
|
|
3484
|
+
requiresExactHumanConfirmation: true,
|
|
3485
|
+
maximumDurationHours: 72,
|
|
3486
|
+
},
|
|
3487
|
+
],
|
|
3488
|
+
}
|
|
3489
|
+
: {}),
|
|
2641
3490
|
...(activeMilestone.status === 'active'
|
|
2642
3491
|
? {
|
|
2643
3492
|
completionPolicy: {
|
|
@@ -2649,7 +3498,21 @@ export class WorkflowService {
|
|
|
2649
3498
|
.filter((membership) => membership.disposition === 'required')
|
|
2650
3499
|
.map((membership) => {
|
|
2651
3500
|
const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
|
|
2652
|
-
|
|
3501
|
+
const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
|
|
3502
|
+
const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
|
|
3503
|
+
if (!candidate)
|
|
3504
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3505
|
+
return candidate;
|
|
3506
|
+
});
|
|
3507
|
+
return {
|
|
3508
|
+
taskId: membership.taskId,
|
|
3509
|
+
status: task?.status ?? 'missing',
|
|
3510
|
+
dependencyState: dependency.dependencyState,
|
|
3511
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
3512
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
3513
|
+
runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
|
|
3514
|
+
dependencyBlockReason: dependency.blockReason,
|
|
3515
|
+
};
|
|
2653
3516
|
}),
|
|
2654
3517
|
},
|
|
2655
3518
|
}
|
|
@@ -2772,7 +3635,21 @@ export class WorkflowService {
|
|
|
2772
3635
|
}
|
|
2773
3636
|
}
|
|
2774
3637
|
}
|
|
2775
|
-
|
|
3638
|
+
currentMilestoneDependencyBinding(milestone, taskId) {
|
|
3639
|
+
assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
|
|
3640
|
+
if (!milestone.planHash) {
|
|
3641
|
+
throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
|
|
3642
|
+
milestoneId: milestone.id,
|
|
3643
|
+
taskId,
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
milestoneRevision: milestone.revision,
|
|
3648
|
+
membershipRevision: milestone.membershipRevision,
|
|
3649
|
+
milestonePlanHash: milestone.planHash,
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
2776
3653
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
2777
3654
|
? 'task.execution_authorize'
|
|
2778
3655
|
: task.status === 'awaiting_final_acceptance'
|
|
@@ -2794,8 +3671,73 @@ export class WorkflowService {
|
|
|
2794
3671
|
throw error;
|
|
2795
3672
|
}
|
|
2796
3673
|
}
|
|
2797
|
-
|
|
3674
|
+
let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
|
|
3675
|
+
? guardedCorrectivePosture
|
|
3676
|
+
: null;
|
|
3677
|
+
if (!remediationModeRecovery && planRiskAudit && guardedCorrectivePosture === null) {
|
|
3678
|
+
try {
|
|
3679
|
+
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
3680
|
+
}
|
|
3681
|
+
catch (error) {
|
|
3682
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
3683
|
+
if (!remediationModeRecovery)
|
|
3684
|
+
throw error;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
3688
|
+
let mechanicalFeasibilityOverlay = {};
|
|
3689
|
+
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
3690
|
+
try {
|
|
3691
|
+
const taskRoot = this.store.taskRoot(projectId, task.id);
|
|
3692
|
+
const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
|
|
3693
|
+
const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
|
|
3694
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
3695
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
3696
|
+
taskId: task.id,
|
|
3697
|
+
stateBriefHash: task.briefHash,
|
|
3698
|
+
artifactBriefHash: briefHash,
|
|
3699
|
+
statePlanHash: task.planHash,
|
|
3700
|
+
artifactPlanHash: planHash,
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
|
|
3704
|
+
mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
|
|
3705
|
+
? {
|
|
3706
|
+
action: 'task plan-set',
|
|
3707
|
+
blockedAction: 'task authorize',
|
|
3708
|
+
mechanicalFeasibility,
|
|
3709
|
+
mechanicalFeasibilityGate: {
|
|
3710
|
+
state: 'blocked',
|
|
3711
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
3712
|
+
requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
|
|
3713
|
+
},
|
|
3714
|
+
}
|
|
3715
|
+
: {
|
|
3716
|
+
mechanicalFeasibility,
|
|
3717
|
+
mechanicalFeasibilityGate: {
|
|
3718
|
+
state: 'clear',
|
|
3719
|
+
meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
|
|
3720
|
+
},
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
catch (error) {
|
|
3724
|
+
mechanicalFeasibilityOverlay = {
|
|
3725
|
+
action: 'doctor',
|
|
3726
|
+
blockedAction: 'task authorize',
|
|
3727
|
+
mechanicalFeasibilityGate: {
|
|
3728
|
+
state: 'unavailable',
|
|
3729
|
+
diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
|
|
3730
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3731
|
+
requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
|
|
3732
|
+
},
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
2798
3736
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
3737
|
+
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
3738
|
+
const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
|
|
3739
|
+
? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
|
|
3740
|
+
: null;
|
|
2799
3741
|
const strictStepReviewStep = planRiskAudit
|
|
2800
3742
|
? task.steps.find((step) => {
|
|
2801
3743
|
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
@@ -2806,6 +3748,11 @@ export class WorkflowService {
|
|
|
2806
3748
|
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
2807
3749
|
}) ?? null
|
|
2808
3750
|
: null;
|
|
3751
|
+
const strictReviewInfrastructureFailures = strictStepReviewStep
|
|
3752
|
+
? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
|
|
3753
|
+
: 0;
|
|
3754
|
+
const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
|
|
3755
|
+
|| (task.status === 'blocked' && task.review.status === 'unverified');
|
|
2809
3756
|
const handoffOverlay = handoffPosture.state === 'pending'
|
|
2810
3757
|
? {
|
|
2811
3758
|
action: 'task claim',
|
|
@@ -2814,6 +3761,15 @@ export class WorkflowService {
|
|
|
2814
3761
|
state: 'pending',
|
|
2815
3762
|
targetActor: handoffPosture.targetActor,
|
|
2816
3763
|
eventId: handoffPosture.latestEvent.eventId,
|
|
3764
|
+
credentialRecovery: {
|
|
3765
|
+
action: 'task handoff-replace',
|
|
3766
|
+
when: 'claim credential is unavailable',
|
|
3767
|
+
requiredActor: handoffPosture.latestEvent.actor,
|
|
3768
|
+
targetActor: handoffPosture.targetActor,
|
|
3769
|
+
grantId: handoffPosture.latestEvent.grantId,
|
|
3770
|
+
expectedRevision: task.revision,
|
|
3771
|
+
invalidatesPriorCredential: true,
|
|
3772
|
+
},
|
|
2817
3773
|
},
|
|
2818
3774
|
}
|
|
2819
3775
|
: handoffPosture.state === 'claimed'
|
|
@@ -2836,7 +3792,97 @@ export class WorkflowService {
|
|
|
2836
3792
|
},
|
|
2837
3793
|
}
|
|
2838
3794
|
: {};
|
|
3795
|
+
const correctiveReplanOverlay = (posture) => {
|
|
3796
|
+
const evaluation = evaluateTaskCorrectiveReplanAvailability(task, posture);
|
|
3797
|
+
const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
3798
|
+
const command = handoffPosture.state === 'claimed'
|
|
3799
|
+
? 'task corrective-yield'
|
|
3800
|
+
: writerLease
|
|
3801
|
+
? 'doctor'
|
|
3802
|
+
: 'task corrective-replan-prepare';
|
|
3803
|
+
if (evaluation.allowed && handoffPosture.state === 'none' && writerLease) {
|
|
3804
|
+
const navigation = correctiveReplanNavigation(evaluation);
|
|
3805
|
+
return {
|
|
3806
|
+
action: command,
|
|
3807
|
+
blockedAction: 'task corrective-replan-prepare',
|
|
3808
|
+
stepId: posture.stepId,
|
|
3809
|
+
navigation: {
|
|
3810
|
+
kind: 'blocked',
|
|
3811
|
+
transitionId: navigation.transitionId,
|
|
3812
|
+
semanticFingerprint: navigation.semanticFingerprint,
|
|
3813
|
+
command: navigation.command,
|
|
3814
|
+
blockingGuardIds: ['task.corrective-replan.no-writer-lease'],
|
|
3815
|
+
pendingInputGuardIds: navigation.pendingInputGuardIds,
|
|
3816
|
+
reasonCode: 'ACTIVE_WRITER_LEASE',
|
|
3817
|
+
},
|
|
3818
|
+
correctiveDecisionGate: {
|
|
3819
|
+
attemptCount: posture.attemptCount,
|
|
3820
|
+
decision: posture.correctiveDecision?.decision ?? null,
|
|
3821
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3822
|
+
requiredAction: 'The active non-C1 writer lease must expire and be explicitly repaired before corrective replan preparation.',
|
|
3823
|
+
writerLease: {
|
|
3824
|
+
owner: writerLease.owner,
|
|
3825
|
+
expiresAt: writerLease.expiresAt,
|
|
3826
|
+
},
|
|
3827
|
+
},
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
return evaluation.allowed
|
|
3831
|
+
? {
|
|
3832
|
+
action: command,
|
|
3833
|
+
blockedAction: 'task plan-set',
|
|
3834
|
+
stepId: posture.stepId,
|
|
3835
|
+
navigation: { ...correctiveReplanNavigation(evaluation), command },
|
|
3836
|
+
correctiveDecisionGate: {
|
|
3837
|
+
attemptCount: posture.attemptCount,
|
|
3838
|
+
decision: posture.correctiveDecision?.decision ?? null,
|
|
3839
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3840
|
+
requiredAction: handoffPosture.state === 'claimed'
|
|
3841
|
+
? 'Yield the exact C1 claim and writer lease before preparing replacement authority.'
|
|
3842
|
+
: 'Prepare a state-bound candidate, confirm the Human gate, then execute its exact receipt.',
|
|
3843
|
+
},
|
|
3844
|
+
}
|
|
3845
|
+
: {
|
|
3846
|
+
action: 'doctor',
|
|
3847
|
+
blockedAction: 'task plan-set',
|
|
3848
|
+
stepId: posture.stepId,
|
|
3849
|
+
navigation: correctiveReplanBlockedNavigation(evaluation),
|
|
3850
|
+
};
|
|
3851
|
+
};
|
|
2839
3852
|
let guardedCorrectiveOverlay = {};
|
|
3853
|
+
if (planIntegrityAssessment?.applicable) {
|
|
3854
|
+
const requiredActor = handoffPosture.state === 'claimed'
|
|
3855
|
+
? handoffPosture.claimant
|
|
3856
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
3857
|
+
guardedCorrectiveOverlay = planIntegrityAssessment.eligible
|
|
3858
|
+
? {
|
|
3859
|
+
action: 'task plan-integrity-recover',
|
|
3860
|
+
blockedAction: 'task run',
|
|
3861
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3862
|
+
planIntegrityRecovery: {
|
|
3863
|
+
state: 'eligible',
|
|
3864
|
+
reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
|
|
3865
|
+
expectedRevision: task.revision,
|
|
3866
|
+
requiredActor,
|
|
3867
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3868
|
+
boundedEffect: 'record-replan-required-decision',
|
|
3869
|
+
preservesWorktree: true,
|
|
3870
|
+
},
|
|
3871
|
+
}
|
|
3872
|
+
: {
|
|
3873
|
+
action: 'doctor',
|
|
3874
|
+
blockedAction: 'task plan-integrity-recover',
|
|
3875
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3876
|
+
planIntegrityRecovery: {
|
|
3877
|
+
state: 'blocked',
|
|
3878
|
+
reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
|
|
3879
|
+
expectedRevision: task.revision,
|
|
3880
|
+
requiredActor,
|
|
3881
|
+
blockers: planIntegrityAssessment.blockers,
|
|
3882
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3883
|
+
},
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
2840
3886
|
if (remediationModeRecovery) {
|
|
2841
3887
|
guardedCorrectiveOverlay = {
|
|
2842
3888
|
action: 'task remediation-mode-recover',
|
|
@@ -2854,7 +3900,12 @@ export class WorkflowService {
|
|
|
2854
3900
|
},
|
|
2855
3901
|
};
|
|
2856
3902
|
}
|
|
2857
|
-
if (!remediationModeRecovery &&
|
|
3903
|
+
if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
3904
|
+
guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
|
|
3905
|
+
}
|
|
3906
|
+
if (!remediationModeRecovery
|
|
3907
|
+
&& guardedCorrectivePosture
|
|
3908
|
+
&& (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
|
|
2858
3909
|
switch (guardedCorrectivePosture.posture) {
|
|
2859
3910
|
case 'decision-required':
|
|
2860
3911
|
if (guardedCorrectivePosture.correctiveDecision
|
|
@@ -2929,28 +3980,35 @@ export class WorkflowService {
|
|
|
2929
3980
|
};
|
|
2930
3981
|
break;
|
|
2931
3982
|
case 'replan-required':
|
|
2932
|
-
guardedCorrectiveOverlay =
|
|
2933
|
-
action: 'task plan-set',
|
|
2934
|
-
stepId: guardedCorrectivePosture.stepId,
|
|
2935
|
-
correctiveDecisionGate: {
|
|
2936
|
-
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2937
|
-
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2938
|
-
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2939
|
-
requiredAction: 'Record an actual new Plan posture before another remediation run.',
|
|
2940
|
-
},
|
|
2941
|
-
};
|
|
3983
|
+
guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
|
|
2942
3984
|
break;
|
|
2943
3985
|
case 'split-required':
|
|
2944
3986
|
{
|
|
2945
3987
|
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
2946
3988
|
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
2947
|
-
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
2948
3989
|
const replacementMilestone = task.milestoneId
|
|
2949
3990
|
? this.store.readMilestone(projectId, task.milestoneId)
|
|
2950
3991
|
: null;
|
|
3992
|
+
const incidentDependencies = replacementMilestone
|
|
3993
|
+
? replacementMilestone.memberships.flatMap((membership) => {
|
|
3994
|
+
if (!Array.isArray(membership.dependsOnTaskIds))
|
|
3995
|
+
return [];
|
|
3996
|
+
return [
|
|
3997
|
+
...(membership.taskId === task.id
|
|
3998
|
+
? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
|
|
3999
|
+
predecessorTaskId,
|
|
4000
|
+
consumerTaskId: membership.taskId,
|
|
4001
|
+
}))
|
|
4002
|
+
: []),
|
|
4003
|
+
...(membership.dependsOnTaskIds.includes(task.id)
|
|
4004
|
+
? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
|
|
4005
|
+
: []),
|
|
4006
|
+
];
|
|
4007
|
+
})
|
|
4008
|
+
: [];
|
|
2951
4009
|
guardedCorrectiveOverlay = {
|
|
2952
|
-
action:
|
|
2953
|
-
|
|
4010
|
+
action: 'doctor',
|
|
4011
|
+
blockedAction: 'task replacement-materialize',
|
|
2954
4012
|
stepId: guardedCorrectivePosture.stepId,
|
|
2955
4013
|
correctiveDecisionGate: {
|
|
2956
4014
|
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
@@ -2964,29 +4022,17 @@ export class WorkflowService {
|
|
|
2964
4022
|
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
2965
4023
|
oldMembershipDisposition: 'cancelled',
|
|
2966
4024
|
replacementMembershipDisposition: 'required',
|
|
2967
|
-
transaction: '
|
|
4025
|
+
transaction: 'disabled',
|
|
4026
|
+
retainedImplementation: 'journal-recoverable-1-to-1',
|
|
4027
|
+
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
4028
|
+
structuralReplacementAvailable: false,
|
|
4029
|
+
incidentDependencies,
|
|
2968
4030
|
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
2969
4031
|
id: discovery.id,
|
|
2970
4032
|
revision: discovery.revision,
|
|
2971
4033
|
goal: discovery.goal,
|
|
2972
4034
|
})),
|
|
2973
|
-
|
|
2974
|
-
? {
|
|
2975
|
-
exactTransition: {
|
|
2976
|
-
command: 'task replacement-materialize',
|
|
2977
|
-
taskId: task.id,
|
|
2978
|
-
expectedTaskRevision: task.revision,
|
|
2979
|
-
discoveryId: replacementDiscovery.id,
|
|
2980
|
-
expectedDiscoveryRevision: replacementDiscovery.revision,
|
|
2981
|
-
milestoneId: replacementMilestone.id,
|
|
2982
|
-
expectedMilestoneRevision: replacementMilestone.revision,
|
|
2983
|
-
},
|
|
2984
|
-
}
|
|
2985
|
-
: {
|
|
2986
|
-
requiredAction: replacementDiscoveries.length === 0
|
|
2987
|
-
? 'Create and complete one thin replacement Discovery, then run next again.'
|
|
2988
|
-
: 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
|
|
2989
|
-
}),
|
|
4035
|
+
requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
|
|
2990
4036
|
},
|
|
2991
4037
|
};
|
|
2992
4038
|
}
|
|
@@ -3022,6 +4068,30 @@ export class WorkflowService {
|
|
|
3022
4068
|
strictStepReview: {
|
|
3023
4069
|
reviewRequired: true,
|
|
3024
4070
|
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
4071
|
+
infrastructureFailures: strictReviewInfrastructureFailures,
|
|
4072
|
+
maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
|
|
4073
|
+
recommendedMode: 'external-sealed',
|
|
4074
|
+
externalSealedReview: {
|
|
4075
|
+
packetCommand: 'task step-review-packet',
|
|
4076
|
+
recordCommand: 'task step-review-record',
|
|
4077
|
+
reviewerMustUseSeparateChat: true,
|
|
4078
|
+
repositorySealRequired: true,
|
|
4079
|
+
},
|
|
4080
|
+
},
|
|
4081
|
+
}
|
|
4082
|
+
: {}),
|
|
4083
|
+
...(strictTaskReviewRequired
|
|
4084
|
+
? {
|
|
4085
|
+
strictTaskReview: {
|
|
4086
|
+
reviewRequired: true,
|
|
4087
|
+
localInfrastructureFailureObserved: task.review.status === 'unverified',
|
|
4088
|
+
recommendedMode: 'external-sealed',
|
|
4089
|
+
externalSealedReview: {
|
|
4090
|
+
packetCommand: 'task review-packet',
|
|
4091
|
+
recordCommand: 'task review-sealed-record',
|
|
4092
|
+
reviewerMustUseSeparateChat: true,
|
|
4093
|
+
repositorySealRequired: true,
|
|
4094
|
+
},
|
|
3025
4095
|
},
|
|
3026
4096
|
}
|
|
3027
4097
|
: {}),
|
|
@@ -3048,16 +4118,41 @@ export class WorkflowService {
|
|
|
3048
4118
|
}
|
|
3049
4119
|
: {}),
|
|
3050
4120
|
...guardedCorrectiveOverlay,
|
|
4121
|
+
...mechanicalFeasibilityOverlay,
|
|
3051
4122
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3052
4123
|
};
|
|
3053
4124
|
if (handoffPosture.state === 'pending') {
|
|
3054
|
-
return {
|
|
4125
|
+
return this.withTaskNavigationContracts(task, {
|
|
3055
4126
|
...result,
|
|
3056
4127
|
action: 'task claim',
|
|
3057
4128
|
blockedAction: next.action,
|
|
3058
|
-
};
|
|
4129
|
+
});
|
|
3059
4130
|
}
|
|
3060
|
-
|
|
4131
|
+
const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
|
|
4132
|
+
.includes(String(result.action))
|
|
4133
|
+
? {
|
|
4134
|
+
...result,
|
|
4135
|
+
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
4136
|
+
}
|
|
4137
|
+
: result;
|
|
4138
|
+
return this.withTaskNavigationContracts(task, navigableResult);
|
|
4139
|
+
}
|
|
4140
|
+
taskPlanContract(projectId, task) {
|
|
4141
|
+
const map = this.store.readKnowledgeMap(projectId);
|
|
4142
|
+
return {
|
|
4143
|
+
requiredRequirementIds: [...task.requirements],
|
|
4144
|
+
requiredAcceptanceIds: [...task.acceptance],
|
|
4145
|
+
currentRisks: [...task.risks],
|
|
4146
|
+
exactInclusionRequired: true,
|
|
4147
|
+
briefHash: task.briefHash,
|
|
4148
|
+
knowledgeMap: knowledgeSummary(map),
|
|
4149
|
+
planRiskAuditContract: {
|
|
4150
|
+
required: true,
|
|
4151
|
+
option: '--risk-audit-file',
|
|
4152
|
+
atomicWithPlanSet: true,
|
|
4153
|
+
plannerAuditorSeparationRequired: true,
|
|
4154
|
+
},
|
|
4155
|
+
};
|
|
3061
4156
|
}
|
|
3062
4157
|
assertTaskContextChangeAllowed(task) {
|
|
3063
4158
|
const posture = this.taskTerminalCorrectivePosture(task);
|
|
@@ -3136,6 +4231,23 @@ export class WorkflowService {
|
|
|
3136
4231
|
policyHash: grant.policyHash,
|
|
3137
4232
|
}));
|
|
3138
4233
|
}
|
|
4234
|
+
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
4235
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
4236
|
+
for (const task of tasks) {
|
|
4237
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4238
|
+
|| !task.planHash
|
|
4239
|
+
|| !taskCanRebindKnowledge(task)
|
|
4240
|
+
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
4241
|
+
continue;
|
|
4242
|
+
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
4243
|
+
if (!assessment.eligible)
|
|
4244
|
+
continue;
|
|
4245
|
+
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
4246
|
+
if (options.length > 0)
|
|
4247
|
+
return { task, assessment, options };
|
|
4248
|
+
}
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
3139
4251
|
assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
|
|
3140
4252
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
3141
4253
|
if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
@@ -3161,7 +4273,7 @@ export class WorkflowService {
|
|
|
3161
4273
|
if (policy.scope.kind === 'milestone')
|
|
3162
4274
|
this.store.readMilestone(projectId, policy.scope.id);
|
|
3163
4275
|
}
|
|
3164
|
-
assertExecutionAuthorizationFresh(task) {
|
|
4276
|
+
assertExecutionAuthorizationFresh(task, repositoryRoot) {
|
|
3165
4277
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
3166
4278
|
const authorization = [...task.authorizations]
|
|
3167
4279
|
.reverse()
|
|
@@ -3170,14 +4282,35 @@ export class WorkflowService {
|
|
|
3170
4282
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
|
|
3171
4283
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
3172
4284
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
3173
|
-
if (authorization.briefHash !== briefHash
|
|
4285
|
+
if (authorization.briefHash !== briefHash
|
|
4286
|
+
|| authorization.planHash !== planHash
|
|
4287
|
+
|| task.briefHash !== briefHash
|
|
4288
|
+
|| task.planHash !== planHash) {
|
|
3174
4289
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
|
|
3175
4290
|
authorizedBriefHash: authorization.briefHash,
|
|
3176
4291
|
currentBriefHash: briefHash,
|
|
3177
4292
|
authorizedPlanHash: authorization.planHash,
|
|
3178
4293
|
currentPlanHash: planHash,
|
|
4294
|
+
stateBriefHash: task.briefHash,
|
|
4295
|
+
statePlanHash: task.planHash,
|
|
3179
4296
|
});
|
|
3180
4297
|
}
|
|
4298
|
+
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4299
|
+
if (!mechanicalFeasibility)
|
|
4300
|
+
return authorization;
|
|
4301
|
+
const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
|
|
4302
|
+
if (authorization.evidenceHash !== evidenceHash
|
|
4303
|
+
|| authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
|
|
4304
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
|
|
4305
|
+
taskId: task.id,
|
|
4306
|
+
authorizedEvidenceHash: authorization.evidenceHash,
|
|
4307
|
+
currentEvidenceHash: evidenceHash,
|
|
4308
|
+
authorizedHeadCommit: authorization.headCommit,
|
|
4309
|
+
evaluatedHead: mechanicalFeasibility.evaluatedHead,
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
|
|
4313
|
+
return authorization;
|
|
3181
4314
|
}
|
|
3182
4315
|
assertMilestoneAuthorizationFresh(milestone) {
|
|
3183
4316
|
const authorization = [...milestone.authorizations]
|
|
@@ -3205,6 +4338,130 @@ export class WorkflowService {
|
|
|
3205
4338
|
? milestone
|
|
3206
4339
|
: readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
3207
4340
|
}
|
|
4341
|
+
listMilestonesObserved(projectId, milestones, posture) {
|
|
4342
|
+
if (!posture)
|
|
4343
|
+
return milestones;
|
|
4344
|
+
const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
|
|
4345
|
+
return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
|
|
4346
|
+
? milestone
|
|
4347
|
+
: inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
4348
|
+
}
|
|
4349
|
+
taskGitNavigationOverride(task) {
|
|
4350
|
+
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
|
|
4351
|
+
return null;
|
|
4352
|
+
}
|
|
4353
|
+
const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4354
|
+
if (currentBase === task.baseCommit)
|
|
4355
|
+
return null;
|
|
4356
|
+
return {
|
|
4357
|
+
scope: 'task',
|
|
4358
|
+
id: task.id,
|
|
4359
|
+
status: task.status,
|
|
4360
|
+
action: 'task sync-base',
|
|
4361
|
+
revision: task.revision,
|
|
4362
|
+
baseSyncRequired: {
|
|
4363
|
+
baseBranch: task.baseBranch,
|
|
4364
|
+
recordedBaseCommit: task.baseCommit,
|
|
4365
|
+
currentBaseCommit: currentBase,
|
|
4366
|
+
},
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
inspectDependencyProvenanceCandidate(snapshot, task) {
|
|
4370
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4371
|
+
const now = this.now().getTime();
|
|
4372
|
+
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
4373
|
+
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
4374
|
+
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
4375
|
+
pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
|
|
4376
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4377
|
+
+ snapshot.observation.pendingTaskTransactions.length,
|
|
4378
|
+
corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
|
|
4379
|
+
coreOperationCount: snapshot.observation.coreTaskOperations.length,
|
|
4380
|
+
verifiedHistoricalStepCommits: verifiedHistoricalStepReviewCommits(this.store, task),
|
|
4381
|
+
versions: {
|
|
4382
|
+
runtime: versions.runtime,
|
|
4383
|
+
declared: versions.declared,
|
|
4384
|
+
locked: versions.locked,
|
|
4385
|
+
installed: versions.installed,
|
|
4386
|
+
currentBranch: versions.currentBranch,
|
|
4387
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4388
|
+
},
|
|
4389
|
+
});
|
|
4390
|
+
}
|
|
4391
|
+
withTaskNavigationContracts(task, next) {
|
|
4392
|
+
const action = typeof next.action === 'string' ? next.action : null;
|
|
4393
|
+
if (!action)
|
|
4394
|
+
return next;
|
|
4395
|
+
const policy = gitMutationPolicyForAction(task, action);
|
|
4396
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
4397
|
+
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
4398
|
+
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
|
|
4399
|
+
const activeLease = writerTokenContract
|
|
4400
|
+
? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
4401
|
+
: null;
|
|
4402
|
+
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
4403
|
+
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
4404
|
+
? {
|
|
4405
|
+
action: 'task writer-credential-replace',
|
|
4406
|
+
when: 'the active writer credential is unavailable',
|
|
4407
|
+
requiredActor: writerCredentialRecoveryActor,
|
|
4408
|
+
expectedRevision: task.revision,
|
|
4409
|
+
invalidatesPriorCredential: true,
|
|
4410
|
+
acquiresLeaseWhenAbsent: true,
|
|
4411
|
+
}
|
|
4412
|
+
: null;
|
|
4413
|
+
return {
|
|
4414
|
+
...next,
|
|
4415
|
+
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
4416
|
+
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
4417
|
+
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
|
|
4421
|
+
if (claimedActor)
|
|
4422
|
+
return claimedActor;
|
|
4423
|
+
if (activeLeaseOwner)
|
|
4424
|
+
return activeLeaseOwner;
|
|
4425
|
+
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
4426
|
+
? 'final_acceptance'
|
|
4427
|
+
: ['task step-complete', 'task submit'].includes(action)
|
|
4428
|
+
? 'execution'
|
|
4429
|
+
: null;
|
|
4430
|
+
if (!authorizationKind)
|
|
4431
|
+
return null;
|
|
4432
|
+
return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
|
|
4433
|
+
}
|
|
4434
|
+
describeMilestoneInitialAssembly(projectId, milestone, tasks) {
|
|
4435
|
+
const assessment = assessMilestoneInitialAssembly({
|
|
4436
|
+
store: this.store,
|
|
4437
|
+
projectId,
|
|
4438
|
+
milestone,
|
|
4439
|
+
listTasks: () => tasks,
|
|
4440
|
+
});
|
|
4441
|
+
if (!assessment.eligible)
|
|
4442
|
+
return null;
|
|
4443
|
+
const options = [
|
|
4444
|
+
{
|
|
4445
|
+
action: 'linked-task-assembly',
|
|
4446
|
+
commands: ['discovery start', 'discovery materialize'],
|
|
4447
|
+
description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
|
|
4448
|
+
},
|
|
4449
|
+
];
|
|
4450
|
+
if (assessment.orphanTaskIds.length > 0) {
|
|
4451
|
+
options.push({
|
|
4452
|
+
action: 'milestone plan-set',
|
|
4453
|
+
commands: ['milestone plan-set'],
|
|
4454
|
+
description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
return {
|
|
4458
|
+
milestoneId: milestone.id,
|
|
4459
|
+
revision: milestone.revision,
|
|
4460
|
+
linkedTaskIds: assessment.orphanTaskIds,
|
|
4461
|
+
executionAvailable: false,
|
|
4462
|
+
options,
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
3208
4465
|
prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
|
|
3209
4466
|
validateMilestonePlan(plan);
|
|
3210
4467
|
return prepareMilestoneScopeChangeCandidate({
|
|
@@ -3214,6 +4471,12 @@ export class WorkflowService {
|
|
|
3214
4471
|
actor,
|
|
3215
4472
|
});
|
|
3216
4473
|
}
|
|
4474
|
+
publicWriterLeaseDiagnostic(lease) {
|
|
4475
|
+
return {
|
|
4476
|
+
...toPublicWriterLease(lease, this.now()),
|
|
4477
|
+
tokenFingerprint: createHash('sha256').update(lease.token).digest('hex').slice(0, 16),
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
3217
4480
|
proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
|
|
3218
4481
|
const currentLease = context.locks.inspect(taskId);
|
|
3219
4482
|
if (!currentLease) {
|
|
@@ -3303,9 +4566,12 @@ export class WorkflowService {
|
|
|
3303
4566
|
storedKnowledgeMapHash: storedHash,
|
|
3304
4567
|
});
|
|
3305
4568
|
}
|
|
3306
|
-
inspectAdoptionPreparation(identity, tasks, milestones) {
|
|
3307
|
-
const
|
|
3308
|
-
|
|
4569
|
+
inspectAdoptionPreparation(identity, tasks, milestones, observedLeases, observedPosture) {
|
|
4570
|
+
const leases = observedLeases
|
|
4571
|
+
?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
|
|
4572
|
+
const currentPosture = observedPosture === undefined
|
|
4573
|
+
? readCurrentAdoptionPosture(this.store, identity.projectId)
|
|
4574
|
+
: observedPosture;
|
|
3309
4575
|
if (currentPosture) {
|
|
3310
4576
|
assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
|
|
3311
4577
|
}
|
|
@@ -3318,7 +4584,7 @@ export class WorkflowService {
|
|
|
3318
4584
|
tasks,
|
|
3319
4585
|
milestones,
|
|
3320
4586
|
sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
|
|
3321
|
-
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks,
|
|
4587
|
+
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, leases),
|
|
3322
4588
|
now: this.now(),
|
|
3323
4589
|
}, currentPosture);
|
|
3324
4590
|
}
|
|
@@ -3765,12 +5031,25 @@ function validateMilestonePlan(plan) {
|
|
|
3765
5031
|
if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
|
|
3766
5032
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
|
|
3767
5033
|
}
|
|
3768
|
-
if (plan.memberships.length === 0) {
|
|
5034
|
+
if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
|
|
3769
5035
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
|
|
3770
5036
|
}
|
|
3771
5037
|
for (const membership of plan.memberships) {
|
|
3772
|
-
if (!membership.
|
|
5038
|
+
if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
|
|
5039
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
|
|
5040
|
+
}
|
|
5041
|
+
if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
|
|
3773
5042
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
|
|
5043
|
+
}
|
|
5044
|
+
if (typeof membership.reason !== 'string') {
|
|
5045
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
|
|
5046
|
+
}
|
|
5047
|
+
if (!Array.isArray(membership.dependsOnTaskIds)) {
|
|
5048
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
|
|
5049
|
+
}
|
|
5050
|
+
if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
|
|
5051
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
|
|
5052
|
+
}
|
|
3774
5053
|
if (membership.disposition !== 'required' && !membership.reason.trim()) {
|
|
3775
5054
|
throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
|
|
3776
5055
|
}
|
|
@@ -3800,7 +5079,7 @@ function taskCanRebindKnowledge(task) {
|
|
|
3800
5079
|
'awaiting_final_acceptance',
|
|
3801
5080
|
].includes(task.status);
|
|
3802
5081
|
}
|
|
3803
|
-
function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
5082
|
+
function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
|
|
3804
5083
|
if (task.status === 'merged' || task.status === 'cancelled')
|
|
3805
5084
|
return false;
|
|
3806
5085
|
if (!task.milestoneId)
|
|
@@ -3808,10 +5087,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
|
3808
5087
|
const milestone = milestonesById.get(task.milestoneId);
|
|
3809
5088
|
if (!milestone)
|
|
3810
5089
|
return false;
|
|
3811
|
-
if (
|
|
3812
|
-
return
|
|
3813
|
-
const
|
|
3814
|
-
|
|
5090
|
+
if (milestone.status !== 'active')
|
|
5091
|
+
return false;
|
|
5092
|
+
const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
5093
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
5094
|
+
if (!candidate)
|
|
5095
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
5096
|
+
return candidate;
|
|
5097
|
+
});
|
|
5098
|
+
return assessment.runnable;
|
|
5099
|
+
}
|
|
5100
|
+
function structuralReplacementIsDisabled() {
|
|
5101
|
+
return true;
|
|
3815
5102
|
}
|
|
3816
5103
|
function milestoneFinalAcceptanceGate(milestone, headCommit) {
|
|
3817
5104
|
if (!milestone.resultHash || !milestone.evidenceHash) {
|
|
@@ -3860,13 +5147,13 @@ function nextForTask(task) {
|
|
|
3860
5147
|
awaiting_execution_authorization: 'task authorize',
|
|
3861
5148
|
ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
|
|
3862
5149
|
in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
|
|
3863
|
-
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-
|
|
5150
|
+
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
|
|
3864
5151
|
needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
|
|
3865
5152
|
awaiting_final_acceptance: 'task accept',
|
|
3866
5153
|
merging: 'task merge',
|
|
3867
5154
|
ready_to_merge: 'task merge-confirm',
|
|
3868
5155
|
merged: 'none',
|
|
3869
|
-
blocked: task.review.status === 'unverified' ? 'task review-
|
|
5156
|
+
blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
|
|
3870
5157
|
cancelled: 'none',
|
|
3871
5158
|
};
|
|
3872
5159
|
const selectedStep = activeStep ?? failedStep ?? runnableStep;
|
|
@@ -3888,6 +5175,96 @@ function nextForTask(task) {
|
|
|
3888
5175
|
: {}),
|
|
3889
5176
|
};
|
|
3890
5177
|
}
|
|
5178
|
+
function gitMutationPolicyForAction(task, action) {
|
|
5179
|
+
const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
|
|
5180
|
+
switch (action) {
|
|
5181
|
+
case 'task start':
|
|
5182
|
+
return {
|
|
5183
|
+
permission: 'git-metadata-write',
|
|
5184
|
+
requiredBeforeFirstCall: true,
|
|
5185
|
+
boundedToTransition: true,
|
|
5186
|
+
transition: 'task start',
|
|
5187
|
+
appliesWhen: { workspaceOwner: 'local' },
|
|
5188
|
+
metadataWrites,
|
|
5189
|
+
};
|
|
5190
|
+
case 'task step-complete':
|
|
5191
|
+
return {
|
|
5192
|
+
permission: 'git-metadata-write',
|
|
5193
|
+
requiredBeforeFirstCall: true,
|
|
5194
|
+
boundedToTransition: true,
|
|
5195
|
+
transition: 'task step-complete',
|
|
5196
|
+
metadataWrites,
|
|
5197
|
+
};
|
|
5198
|
+
case 'task merge':
|
|
5199
|
+
if (task.workspaceOwner !== 'local')
|
|
5200
|
+
return null;
|
|
5201
|
+
return {
|
|
5202
|
+
permission: 'git-metadata-write',
|
|
5203
|
+
requiredBeforeFirstCall: true,
|
|
5204
|
+
boundedToTransition: true,
|
|
5205
|
+
transition: 'task merge',
|
|
5206
|
+
metadataWrites,
|
|
5207
|
+
};
|
|
5208
|
+
case 'task sync-base':
|
|
5209
|
+
return {
|
|
5210
|
+
permission: 'git-metadata-write',
|
|
5211
|
+
requiredBeforeFirstCall: true,
|
|
5212
|
+
boundedToTransition: true,
|
|
5213
|
+
transition: 'task sync-base',
|
|
5214
|
+
metadataWrites,
|
|
5215
|
+
};
|
|
5216
|
+
default:
|
|
5217
|
+
return null;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
5221
|
+
const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
|
|
5222
|
+
const claimedC1LeaseBound = [
|
|
5223
|
+
'task run',
|
|
5224
|
+
'task step-review',
|
|
5225
|
+
'task step-review-record',
|
|
5226
|
+
'task review-record',
|
|
5227
|
+
'task review-launch',
|
|
5228
|
+
'task review-sealed-record',
|
|
5229
|
+
'task corrective-decision-recover',
|
|
5230
|
+
'task remediation-mode-recover',
|
|
5231
|
+
'task plan-integrity-recover',
|
|
5232
|
+
'task corrective-decision',
|
|
5233
|
+
'task corrective-yield',
|
|
5234
|
+
'task handoff',
|
|
5235
|
+
'task handoff-prepare',
|
|
5236
|
+
'task handback',
|
|
5237
|
+
'task handback-create',
|
|
5238
|
+
];
|
|
5239
|
+
if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
|
|
5240
|
+
return null;
|
|
5241
|
+
}
|
|
5242
|
+
return {
|
|
5243
|
+
required: true,
|
|
5244
|
+
option: '--writer-token',
|
|
5245
|
+
source: 'active-writer-lease',
|
|
5246
|
+
sensitive: true,
|
|
5247
|
+
renewalDisposition: 'reuse-presented-token',
|
|
5248
|
+
tokenReissuedOnRenewal: false,
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
function observationPreflightBlockers(observation) {
|
|
5252
|
+
const messages = {
|
|
5253
|
+
PROJECT_REGISTRATION_MISSING: 'Workflow project registration is missing.',
|
|
5254
|
+
PROJECT_REPOSITORY_ROOT_UNBOUND: 'The current repository root is not bound to the Workflow project.',
|
|
5255
|
+
PROJECT_RECOVERY_REQUIRED: 'A project transaction requires explicit recovery.',
|
|
5256
|
+
MILESTONE_RECOVERY_REQUIRED: 'A Milestone transaction requires explicit recovery.',
|
|
5257
|
+
TASK_RECOVERY_REQUIRED: 'A Task corrective-replan transaction requires explicit recovery.',
|
|
5258
|
+
TASK_TRANSACTION_INTEGRITY_FAILED: 'A Task transaction journal failed integrity validation.',
|
|
5259
|
+
TASK_OPERATION_IN_PROGRESS: 'A Core Task operation is in progress.',
|
|
5260
|
+
STALE_TASK_OPERATION: 'A stale Core Task operation requires bounded repair.',
|
|
5261
|
+
ADOPTION_MISSING: 'Workflow lifecycle adoption is missing.',
|
|
5262
|
+
};
|
|
5263
|
+
return observation.reasonCodes.flatMap((reasonCode) => {
|
|
5264
|
+
const message = messages[reasonCode];
|
|
5265
|
+
return message ? [message] : [];
|
|
5266
|
+
});
|
|
5267
|
+
}
|
|
3891
5268
|
function knowledgeSummary(map) {
|
|
3892
5269
|
return {
|
|
3893
5270
|
revision: map.revision,
|