codex-workflow-v2 2.0.0-alpha.7.2.1 → 2.0.0-beta.10
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 +63 -7
- package/dist/src/alpha6/adoption.js +13 -3
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/handoff.d.ts +23 -0
- package/dist/src/alpha6/handoff.js +221 -28
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/milestone.d.ts +29 -0
- package/dist/src/alpha6/milestone.js +167 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- 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.js +18 -6
- 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/beta1/project-transaction.d.ts +52 -0
- package/dist/src/beta1/project-transaction.js +297 -0
- package/dist/src/beta1/project-transaction.js.map +1 -0
- package/dist/src/cli.js +478 -32
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +189 -5
- 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/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +54 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/git.js +7 -9
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -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 +548 -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 +93 -25
- package/dist/src/workflow.js +1712 -218
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +43 -3
- package/docs/beta1-stabilization-brief.md +165 -0
- package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
- package/docs/delegated-approval.md +30 -4
- package/docs/development-flow.md +38 -6
- package/docs/lifecycle/state-machine-stabilization.md +641 -0
- package/docs/project-memory.md +7 -5
- package/docs/release.md +25 -3
- package/docs/split-required-recovery.md +54 -0
- package/docs/stable-release-defect-register.md +730 -0
- package/docs/updating-existing-project.md +59 -4
- package/docs/validation-report.md +85 -56
- package/package.json +6 -2
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
- package/plugins/codex-workflow-gateway/references/protocol.md +80 -5
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +255 -3
- package/references/state-machine.md +4 -1
- package/roles/delivery-coordinator.md +39 -0
- package/schemas/adoption-posture-event.schema.json +1 -1
- package/schemas/context-envelope.schema.json +1 -1
- 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/remediation-event.schema.json +11 -1
- package/schemas/task-handoff-event.schema.json +1 -1
- package/schemas/task.schema.json +25 -0
- package/schemas/transition-definition.schema.json +2609 -0
- package/schemas/transition-payloads.schema.json +140 -0
package/dist/src/workflow.js
CHANGED
|
@@ -2,30 +2,40 @@ 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';
|
|
8
9
|
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
|
-
import { inspectLocalDependency } from './diagnostics.js';
|
|
12
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
12
|
+
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.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
16
|
import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
|
-
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
|
-
import {
|
|
17
|
+
import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
18
|
+
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
19
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
18
20
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
19
21
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
20
22
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
21
23
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
24
|
+
import { ensureDirectory } from './fs-utils.js';
|
|
22
25
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
23
26
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
24
|
-
import { launchStrictStepReviewer } from './reviewer.js';
|
|
25
|
-
import { WriterLockManager } from './state/lock.js';
|
|
27
|
+
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
28
|
+
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
26
29
|
import { FileStateStore } from './state/store.js';
|
|
30
|
+
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
31
|
+
import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
|
|
32
|
+
import { createCorrectiveYieldExecutor } from './state/corrective-yield-executor.js';
|
|
33
|
+
import { createCorrectiveReplanPublicController, } from './state/corrective-replan-public.js';
|
|
34
|
+
import { inspectWorkflowObservationBoundary, } from './observation.js';
|
|
27
35
|
import { createEntityId } from './ulid.js';
|
|
28
|
-
import { PACKAGE_VERSION } from './version.js';
|
|
36
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
37
|
+
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
38
|
+
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
29
39
|
export class WorkflowService {
|
|
30
40
|
store;
|
|
31
41
|
now;
|
|
@@ -33,11 +43,42 @@ export class WorkflowService {
|
|
|
33
43
|
this.store = store;
|
|
34
44
|
this.now = now;
|
|
35
45
|
}
|
|
36
|
-
|
|
46
|
+
#mutationContext(repository) {
|
|
37
47
|
const identity = resolveRepositoryIdentity(repository);
|
|
38
|
-
const
|
|
48
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
49
|
+
const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
|
|
50
|
+
const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
|
|
51
|
+
const taskTransactions = {
|
|
52
|
+
corrupt: [...replanTransactions.corrupt, ...yieldTransactions.corrupt],
|
|
53
|
+
pending: [...replanTransactions.pending, ...yieldTransactions.pending],
|
|
54
|
+
};
|
|
55
|
+
if (taskTransactions.corrupt.length > 0) {
|
|
56
|
+
throw new WorkflowError('STATE_CORRUPT', 'A Task transaction journal failed integrity validation.', {
|
|
57
|
+
transactions: taskTransactions.corrupt,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (taskTransactions.pending.length > 0) {
|
|
61
|
+
throw new WorkflowError('TASK_RECOVERY_REQUIRED', 'A Task transaction requires explicit recovery.', {
|
|
62
|
+
taskIds: [...new Set(taskTransactions.pending.map((transaction) => transaction.taskId))].sort(),
|
|
63
|
+
transactionIds: taskTransactions.pending.map((transaction) => transaction.transactionId).sort(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const coreTaskOperations = listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root);
|
|
67
|
+
if (coreTaskOperations.length > 0) {
|
|
68
|
+
throw new WorkflowError('LOCKED', 'A Core Task operation owns the project mutation boundary.', {
|
|
69
|
+
taskIds: coreTaskOperations.map((operation) => operation.entityId).sort(),
|
|
70
|
+
operationIds: coreTaskOperations.map((operation) => operation.operationId).sort(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
recoverProjectTransactions(projectRoot, identity.projectId);
|
|
74
|
+
const projectFile = path.join(projectRoot, 'project.json');
|
|
39
75
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
40
76
|
if (!projectAlreadyRegistered) {
|
|
77
|
+
// The registration posture is a sidecar under the project root. A completely
|
|
78
|
+
// fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
|
|
79
|
+
// only that directory before writing the posture; project.json remains the
|
|
80
|
+
// final registration record written below.
|
|
81
|
+
ensureDirectory(projectRoot);
|
|
41
82
|
initializeProjectRegistrationAdoption(this.store, {
|
|
42
83
|
projectId: identity.projectId,
|
|
43
84
|
repositoryIdentity: identity,
|
|
@@ -51,17 +92,103 @@ export class WorkflowService {
|
|
|
51
92
|
return {
|
|
52
93
|
identity,
|
|
53
94
|
store: this.store,
|
|
54
|
-
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
|
|
95
|
+
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
inspectWriterLease(repository, entityId) {
|
|
99
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
100
|
+
const lease = inspectRawWriterLease(this.store.lockRoot(identity.projectId), entityId, this.store.root);
|
|
101
|
+
return lease ? this.publicWriterLeaseDiagnostic(lease) : null;
|
|
102
|
+
}
|
|
103
|
+
repairStaleWriterLease(repository, entityId) {
|
|
104
|
+
const context = this.#mutationContext(repository);
|
|
105
|
+
const lease = context.locks.repairStale(entityId);
|
|
106
|
+
return {
|
|
107
|
+
...this.publicWriterLeaseDiagnostic(lease),
|
|
108
|
+
repaired: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
prepareCorrectiveReplan(repository, taskId, candidate) {
|
|
112
|
+
return this.correctiveReplanController(repository).prepare({ taskId, candidate });
|
|
113
|
+
}
|
|
114
|
+
confirmCorrectiveReplan(repository, taskId, candidate, gate, confirmationCode) {
|
|
115
|
+
return this.correctiveReplanController(repository).confirm({ taskId, candidate, gate, confirmationCode });
|
|
116
|
+
}
|
|
117
|
+
executeCorrectiveReplan(repository, taskId, candidate, credential) {
|
|
118
|
+
return this.correctiveReplanController(repository).execute({ taskId, candidate, credential });
|
|
119
|
+
}
|
|
120
|
+
recoverCorrectiveReplan(repository, taskId, transactionId) {
|
|
121
|
+
return this.correctiveReplanController(repository).recover({ taskId, transactionId });
|
|
122
|
+
}
|
|
123
|
+
executeCorrectiveYield(repository, taskId, expectedTaskRevision, actor, writerToken, reason) {
|
|
124
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
125
|
+
return createCorrectiveYieldExecutor({
|
|
126
|
+
store: this.store,
|
|
127
|
+
projectId: identity.projectId,
|
|
128
|
+
now: this.now,
|
|
129
|
+
}).execute({
|
|
130
|
+
projectId: identity.projectId,
|
|
131
|
+
taskId,
|
|
132
|
+
expectedTaskRevision,
|
|
133
|
+
actor,
|
|
134
|
+
writerToken,
|
|
135
|
+
reason,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
recoverCorrectiveYield(repository, taskId, transactionId) {
|
|
139
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
140
|
+
return createCorrectiveYieldExecutor({
|
|
141
|
+
store: this.store,
|
|
142
|
+
projectId: identity.projectId,
|
|
143
|
+
now: this.now,
|
|
144
|
+
}).recover({ taskId, transactionId });
|
|
145
|
+
}
|
|
146
|
+
correctiveReplanController(repository) {
|
|
147
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
148
|
+
return createCorrectiveReplanPublicController({
|
|
149
|
+
store: this.store,
|
|
150
|
+
projectId: identity.projectId,
|
|
151
|
+
repositoryRoot: identity.repositoryRoot,
|
|
152
|
+
now: this.now,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
observationSnapshot(repository) {
|
|
156
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
157
|
+
const boundary = inspectWorkflowObservationBoundary({
|
|
158
|
+
store: this.store,
|
|
159
|
+
identity,
|
|
160
|
+
now: this.now(),
|
|
161
|
+
});
|
|
162
|
+
if (boundary.observation.kind !== 'assessment') {
|
|
163
|
+
return {
|
|
164
|
+
...boundary,
|
|
165
|
+
discoveries: [],
|
|
166
|
+
tasks: [],
|
|
167
|
+
milestones: [],
|
|
168
|
+
delegations: [],
|
|
169
|
+
adoption: null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
173
|
+
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
174
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones, boundary.leases, boundary.currentAdoptionPosture);
|
|
175
|
+
return {
|
|
176
|
+
...boundary,
|
|
177
|
+
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
178
|
+
tasks,
|
|
179
|
+
milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
|
|
180
|
+
delegations: this.store.listDelegations(identity.projectId),
|
|
181
|
+
adoption,
|
|
55
182
|
};
|
|
56
183
|
}
|
|
57
184
|
prepareDelegation(repository, input) {
|
|
58
|
-
const { identity } = this
|
|
185
|
+
const { identity } = this.#mutationContext(repository);
|
|
59
186
|
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
60
187
|
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
61
188
|
return gate;
|
|
62
189
|
}
|
|
63
190
|
grantDelegation(repository, input, confirmationCode) {
|
|
64
|
-
const { identity } = this
|
|
191
|
+
const { identity } = this.#mutationContext(repository);
|
|
65
192
|
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
66
193
|
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
67
194
|
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
@@ -97,7 +224,7 @@ export class WorkflowService {
|
|
|
97
224
|
}, null);
|
|
98
225
|
}
|
|
99
226
|
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
100
|
-
const { identity } = this
|
|
227
|
+
const { identity } = this.#mutationContext(repository);
|
|
101
228
|
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
102
229
|
assertExpectedRevision(grant, expectedRevision);
|
|
103
230
|
if (grant.status !== 'active') {
|
|
@@ -121,7 +248,7 @@ export class WorkflowService {
|
|
|
121
248
|
startDiscovery(repository, input) {
|
|
122
249
|
if (!input.goal.trim())
|
|
123
250
|
throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
|
|
124
|
-
const { identity } = this
|
|
251
|
+
const { identity } = this.#mutationContext(repository);
|
|
125
252
|
const timestamp = this.now().toISOString();
|
|
126
253
|
const initial = {
|
|
127
254
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
@@ -149,7 +276,7 @@ export class WorkflowService {
|
|
|
149
276
|
return this.store.writeDiscovery(initial, null);
|
|
150
277
|
}
|
|
151
278
|
updateDiscovery(repository, discoveryId, expectedRevision, patch) {
|
|
152
|
-
const { identity } = this
|
|
279
|
+
const { identity } = this.#mutationContext(repository);
|
|
153
280
|
const current = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
154
281
|
if (current.status === 'materialized' || current.status === 'abandoned') {
|
|
155
282
|
throw new WorkflowError('TRANSITION_BLOCKED', `Discovery ${discoveryId} is ${current.status}.`);
|
|
@@ -173,7 +300,7 @@ export class WorkflowService {
|
|
|
173
300
|
return this.store.writeDiscovery(updated, expectedRevision);
|
|
174
301
|
}
|
|
175
302
|
materializeTask(repository, discoveryId, expectedRevision, title, baseBranch = 'main', milestoneId = null) {
|
|
176
|
-
const { identity } = this
|
|
303
|
+
const { identity } = this.#mutationContext(repository);
|
|
177
304
|
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
178
305
|
assertExpectedRevision(discovery, expectedRevision);
|
|
179
306
|
const readiness = assessDiscovery(discovery);
|
|
@@ -249,8 +376,259 @@ export class WorkflowService {
|
|
|
249
376
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
250
377
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
251
378
|
}
|
|
379
|
+
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
380
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
381
|
+
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
382
|
+
assertExpectedRevision(oldTask, expectedOldTaskRevision);
|
|
383
|
+
if (!oldTask.milestoneId) {
|
|
384
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A split-required replacement requires a Milestone-linked Task.');
|
|
385
|
+
}
|
|
386
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, oldTask.milestoneId);
|
|
387
|
+
assertExpectedRevision(milestone, expectedMilestoneRevision);
|
|
388
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
389
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === oldTask.id);
|
|
390
|
+
if (membership?.disposition !== 'required') {
|
|
391
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The terminal Task must still be a required Milestone member.', {
|
|
392
|
+
taskId: oldTask.id,
|
|
393
|
+
milestoneId: milestone.id,
|
|
394
|
+
disposition: membership?.disposition ?? null,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
|
|
398
|
+
if (!riskAudit)
|
|
399
|
+
throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
|
|
400
|
+
const posture = findCurrentGuardedRemediationPosture(this.store, oldTask, riskAudit);
|
|
401
|
+
if (posture?.posture !== 'split-required' || posture.correctiveDecision?.decision !== 'split-required') {
|
|
402
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task replacement is available only for the exact split-required posture.', {
|
|
403
|
+
taskId: oldTask.id,
|
|
404
|
+
posture: posture?.posture ?? null,
|
|
405
|
+
decision: posture?.correctiveDecision?.decision ?? null,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
409
|
+
assertExpectedRevision(discovery, expectedDiscoveryRevision);
|
|
410
|
+
const readiness = assessDiscovery(discovery);
|
|
411
|
+
if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
|
|
412
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Discovery is not ready to materialize.', {
|
|
413
|
+
discoveryId,
|
|
414
|
+
missing: readiness.missing,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const normalizedTitle = title.trim();
|
|
418
|
+
const normalizedActor = actor.trim();
|
|
419
|
+
if (!normalizedTitle)
|
|
420
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Replacement Task title is required.');
|
|
421
|
+
if (!normalizedActor)
|
|
422
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Replacement actor is required.');
|
|
423
|
+
let autonomyContractEventHash;
|
|
424
|
+
let activeAutonomy = null;
|
|
425
|
+
if (normalizedActor.startsWith('agent:')) {
|
|
426
|
+
activeAutonomy = requireActiveMilestoneAutonomy({
|
|
427
|
+
store: this.store,
|
|
428
|
+
projectId: identity.projectId,
|
|
429
|
+
milestone,
|
|
430
|
+
actor: normalizedActor,
|
|
431
|
+
now: this.now(),
|
|
432
|
+
});
|
|
433
|
+
autonomyContractEventHash = activeAutonomy.contract.eventHash;
|
|
434
|
+
}
|
|
435
|
+
const now = this.now();
|
|
436
|
+
const timestamp = now.toISOString();
|
|
437
|
+
const replacementTaskId = createEntityId('TASK', now.getTime());
|
|
438
|
+
const briefContent = renderBrief(discovery, normalizedTitle);
|
|
439
|
+
const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
|
|
440
|
+
const briefHash = sha256Hex(normalizedBriefContent);
|
|
441
|
+
const replacementTask = {
|
|
442
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
443
|
+
revision: 1,
|
|
444
|
+
id: replacementTaskId,
|
|
445
|
+
projectId: identity.projectId,
|
|
446
|
+
createdAt: timestamp,
|
|
447
|
+
updatedAt: timestamp,
|
|
448
|
+
kind: 'task',
|
|
449
|
+
status: 'planning',
|
|
450
|
+
title: normalizedTitle,
|
|
451
|
+
slug: slugify(normalizedTitle),
|
|
452
|
+
discoveryId,
|
|
453
|
+
milestoneId: milestone.id,
|
|
454
|
+
repositoryRoot: identity.repositoryRoot,
|
|
455
|
+
baseBranch: milestone.baseBranch,
|
|
456
|
+
baseCommit: null,
|
|
457
|
+
taskBranch: null,
|
|
458
|
+
workspaceOwner: null,
|
|
459
|
+
briefHash,
|
|
460
|
+
planHash: null,
|
|
461
|
+
planObjective: null,
|
|
462
|
+
resultHash: null,
|
|
463
|
+
evidenceHash: null,
|
|
464
|
+
requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
|
|
465
|
+
acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
|
|
466
|
+
risks: discovery.assumptions,
|
|
467
|
+
knowledgeImpact: null,
|
|
468
|
+
knowledgeImpactReason: null,
|
|
469
|
+
knowledgeTargets: [],
|
|
470
|
+
knowledgeMapRevision: null,
|
|
471
|
+
knowledgeMapHash: null,
|
|
472
|
+
graphUse: null,
|
|
473
|
+
steps: [],
|
|
474
|
+
authorizations: [],
|
|
475
|
+
knowledgeRebinds: [],
|
|
476
|
+
review: emptyReview(),
|
|
477
|
+
blockReason: null,
|
|
478
|
+
mergedCommit: null,
|
|
479
|
+
systemCommits: [],
|
|
480
|
+
replacementForTaskId: oldTask.id,
|
|
481
|
+
replacedByTaskId: null,
|
|
482
|
+
};
|
|
483
|
+
const cancelledOldTask = {
|
|
484
|
+
...oldTask,
|
|
485
|
+
revision: oldTask.revision + 1,
|
|
486
|
+
updatedAt: timestamp,
|
|
487
|
+
status: 'cancelled',
|
|
488
|
+
blockReason: `Replaced after split-required by ${replacementTaskId}.`,
|
|
489
|
+
replacedByTaskId: replacementTaskId,
|
|
490
|
+
};
|
|
491
|
+
const consumedDiscovery = {
|
|
492
|
+
...discovery,
|
|
493
|
+
revision: discovery.revision + 1,
|
|
494
|
+
updatedAt: timestamp,
|
|
495
|
+
status: 'materialized',
|
|
496
|
+
materializedEntityId: replacementTaskId,
|
|
497
|
+
};
|
|
498
|
+
const replacementReason = `Replacement for ${oldTask.id} after split-required decision ${posture.correctiveDecision.eventId}.`;
|
|
499
|
+
const plan = {
|
|
500
|
+
outcome: milestone.outcome,
|
|
501
|
+
successSignal: milestone.successSignal,
|
|
502
|
+
acceptance: milestone.acceptance,
|
|
503
|
+
checks: milestone.checks,
|
|
504
|
+
memberships: [
|
|
505
|
+
...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
|
|
506
|
+
? { ...candidate, disposition: 'cancelled', reason: replacementReason }
|
|
507
|
+
: candidate),
|
|
508
|
+
{ taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
if (autonomyContractEventHash) {
|
|
512
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
513
|
+
store: this.store,
|
|
514
|
+
projectId: identity.projectId,
|
|
515
|
+
milestone,
|
|
516
|
+
actor: normalizedActor,
|
|
517
|
+
now,
|
|
518
|
+
});
|
|
519
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
520
|
+
}
|
|
521
|
+
const readCandidateTask = (taskId) => {
|
|
522
|
+
if (taskId === oldTask.id)
|
|
523
|
+
return cancelledOldTask;
|
|
524
|
+
if (taskId === replacementTaskId)
|
|
525
|
+
return replacementTask;
|
|
526
|
+
return this.store.readTask(identity.projectId, taskId);
|
|
527
|
+
};
|
|
528
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
529
|
+
milestone,
|
|
530
|
+
plan,
|
|
531
|
+
readTask: readCandidateTask,
|
|
532
|
+
actor: normalizedActor,
|
|
533
|
+
...(autonomyContractEventHash ? { autonomyContractEventHash } : {}),
|
|
534
|
+
});
|
|
535
|
+
const bundle = prepareMilestoneScopeChangeBundle({
|
|
536
|
+
store: this.store,
|
|
537
|
+
milestone,
|
|
538
|
+
prepared,
|
|
539
|
+
now,
|
|
540
|
+
readTask: readCandidateTask,
|
|
541
|
+
});
|
|
542
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
543
|
+
const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
|
|
544
|
+
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
545
|
+
let refreshedKnowledgeMap = null;
|
|
546
|
+
if (activeAutonomy
|
|
547
|
+
&& currentKnowledgeMap.status === 'active'
|
|
548
|
+
&& currentKnowledgeMap.approval
|
|
549
|
+
&& (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval)) {
|
|
550
|
+
const refreshAssessment = assessDelegatedKnowledgeRefresh(currentKnowledgeMap, inspectedKnowledgeMap, oldTask, oldTask.planHash
|
|
551
|
+
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, oldTask.id), 'plan.md')
|
|
552
|
+
: null);
|
|
553
|
+
if (refreshAssessment.eligible) {
|
|
554
|
+
if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
|
|
555
|
+
this.assertPlanBoundedSupportingSourceGrant(identity.projectId, activeAutonomy.grant.id, normalizedActor, oldTask);
|
|
556
|
+
}
|
|
557
|
+
const delegation = this.delegatedAuthorization(identity.projectId, activeAutonomy.grant.id, 'project_memory.approve', normalizedActor, inspectedKnowledgeMap, oldTask.id);
|
|
558
|
+
if (!delegation) {
|
|
559
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Knowledge refresh requires delegated approval evidence.');
|
|
560
|
+
}
|
|
561
|
+
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
562
|
+
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
563
|
+
if (!approved.approval) {
|
|
564
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement Knowledge refresh did not produce approval evidence.');
|
|
565
|
+
}
|
|
566
|
+
refreshedKnowledgeMap = {
|
|
567
|
+
...approved,
|
|
568
|
+
revision: currentKnowledgeMap.revision + 2,
|
|
569
|
+
approval: {
|
|
570
|
+
...approved.approval,
|
|
571
|
+
authorizationMode: 'delegated',
|
|
572
|
+
delegation,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
578
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
579
|
+
applyProjectTransaction({
|
|
580
|
+
projectRoot,
|
|
581
|
+
projectId: identity.projectId,
|
|
582
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
583
|
+
kind: 'split-required-task-replacement',
|
|
584
|
+
now,
|
|
585
|
+
targets: [
|
|
586
|
+
{ path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')), content: `${JSON.stringify(consumedDiscovery, null, 2)}\n` },
|
|
587
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'brief.md')), content: normalizedBriefContent },
|
|
588
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'state.json')), content: `${JSON.stringify(replacementTask, null, 2)}\n` },
|
|
589
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, oldTask.id), 'state.json')), content: `${JSON.stringify(cancelledOldTask, null, 2)}\n` },
|
|
590
|
+
{ path: relative(path.join(this.store.lockRoot(identity.projectId), `${oldTask.id}.json`)), content: null },
|
|
591
|
+
...(refreshedKnowledgeMap
|
|
592
|
+
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
593
|
+
: []),
|
|
594
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')), content: bundle.planContent },
|
|
595
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')), content: bundle.sidecarContent },
|
|
596
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')), content: bundle.stateContent },
|
|
597
|
+
],
|
|
598
|
+
validate: () => {
|
|
599
|
+
const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
|
|
600
|
+
const savedOldTask = this.store.readTask(identity.projectId, oldTask.id);
|
|
601
|
+
const savedReplacement = this.store.readTask(identity.projectId, replacementTaskId);
|
|
602
|
+
const savedMilestone = this.store.readMilestone(identity.projectId, milestone.id);
|
|
603
|
+
if (savedDiscovery.materializedEntityId !== replacementTaskId
|
|
604
|
+
|| savedOldTask.status !== 'cancelled'
|
|
605
|
+
|| savedOldTask.replacedByTaskId !== replacementTaskId
|
|
606
|
+
|| savedReplacement.replacementForTaskId !== oldTask.id) {
|
|
607
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction relationship validation failed.');
|
|
608
|
+
}
|
|
609
|
+
if (locks.inspect(oldTask.id)) {
|
|
610
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction retained authority for the cancelled Task.');
|
|
611
|
+
}
|
|
612
|
+
if (refreshedKnowledgeMap) {
|
|
613
|
+
const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
614
|
+
if (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
|
|
615
|
+
|| savedKnowledgeMap.status !== 'active'
|
|
616
|
+
|| savedKnowledgeMap.approval?.authorizationMode !== 'delegated') {
|
|
617
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction Knowledge refresh validation failed.');
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
assertMilestoneMembershipIntegrity(savedMilestone, (taskId) => this.store.readTask(identity.projectId, taskId));
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
return {
|
|
624
|
+
oldTask: this.store.readTask(identity.projectId, oldTask.id),
|
|
625
|
+
replacementTask: this.store.readTask(identity.projectId, replacementTaskId),
|
|
626
|
+
discovery: this.store.readDiscovery(identity.projectId, discovery.id),
|
|
627
|
+
milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
252
630
|
materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
|
|
253
|
-
const { identity } = this
|
|
631
|
+
const { identity } = this.#mutationContext(repository);
|
|
254
632
|
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
255
633
|
assertExpectedRevision(discovery, expectedRevision);
|
|
256
634
|
const readiness = assessDiscovery(discovery);
|
|
@@ -298,7 +676,7 @@ export class WorkflowService {
|
|
|
298
676
|
};
|
|
299
677
|
}
|
|
300
678
|
setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
|
|
301
|
-
const { identity } = this
|
|
679
|
+
const { identity } = this.#mutationContext(repository);
|
|
302
680
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
303
681
|
assertExpectedRevision(milestone, expectedRevision);
|
|
304
682
|
if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
|
|
@@ -370,7 +748,7 @@ export class WorkflowService {
|
|
|
370
748
|
return saved;
|
|
371
749
|
}
|
|
372
750
|
prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
|
|
373
|
-
const { identity } = this
|
|
751
|
+
const { identity } = this.#mutationContext(repository);
|
|
374
752
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
375
753
|
assertExpectedRevision(milestone, expectedRevision);
|
|
376
754
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -387,7 +765,7 @@ export class WorkflowService {
|
|
|
387
765
|
};
|
|
388
766
|
}
|
|
389
767
|
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
390
|
-
const { identity } = this
|
|
768
|
+
const { identity } = this.#mutationContext(repository);
|
|
391
769
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
392
770
|
assertExpectedRevision(milestone, expectedRevision);
|
|
393
771
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -410,9 +788,10 @@ export class WorkflowService {
|
|
|
410
788
|
});
|
|
411
789
|
}
|
|
412
790
|
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
413
|
-
const { identity } = this
|
|
791
|
+
const { identity } = this.#mutationContext(repository);
|
|
414
792
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
415
793
|
assertExpectedRevision(milestone, expectedRevision);
|
|
794
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
416
795
|
return prepareMilestoneAutonomyContract({
|
|
417
796
|
projectId: identity.projectId,
|
|
418
797
|
milestone,
|
|
@@ -423,9 +802,10 @@ export class WorkflowService {
|
|
|
423
802
|
});
|
|
424
803
|
}
|
|
425
804
|
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
426
|
-
const { identity } = this
|
|
805
|
+
const { identity } = this.#mutationContext(repository);
|
|
427
806
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
428
807
|
assertExpectedRevision(milestone, expectedRevision);
|
|
808
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
429
809
|
return applyMilestoneAutonomyContract({
|
|
430
810
|
store: this.store,
|
|
431
811
|
projectId: identity.projectId,
|
|
@@ -438,7 +818,7 @@ export class WorkflowService {
|
|
|
438
818
|
});
|
|
439
819
|
}
|
|
440
820
|
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
441
|
-
const { identity } = this
|
|
821
|
+
const { identity } = this.#mutationContext(repository);
|
|
442
822
|
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
443
823
|
assertExpectedRevision(milestone, expectedRevision);
|
|
444
824
|
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
@@ -468,12 +848,13 @@ export class WorkflowService {
|
|
|
468
848
|
});
|
|
469
849
|
}
|
|
470
850
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
471
|
-
const { identity } = this
|
|
851
|
+
const { identity } = this.#mutationContext(repository);
|
|
472
852
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
473
853
|
assertExpectedRevision(milestone, expectedRevision);
|
|
474
854
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
475
855
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
476
856
|
}
|
|
857
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
477
858
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
478
859
|
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
479
860
|
if (planHash !== milestone.planHash) {
|
|
@@ -497,7 +878,7 @@ export class WorkflowService {
|
|
|
497
878
|
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
498
879
|
}
|
|
499
880
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
500
|
-
const { identity } = this
|
|
881
|
+
const { identity } = this.#mutationContext(repository);
|
|
501
882
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
502
883
|
assertExpectedRevision(milestone, expectedRevision);
|
|
503
884
|
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
@@ -560,7 +941,7 @@ export class WorkflowService {
|
|
|
560
941
|
return saved;
|
|
561
942
|
}
|
|
562
943
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
563
|
-
const { identity } = this
|
|
944
|
+
const { identity } = this.#mutationContext(repository);
|
|
564
945
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
565
946
|
assertExpectedRevision(milestone, expectedRevision);
|
|
566
947
|
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
@@ -599,7 +980,7 @@ export class WorkflowService {
|
|
|
599
980
|
resultHash,
|
|
600
981
|
evidenceHash,
|
|
601
982
|
headCommit: currentHead,
|
|
602
|
-
reason:
|
|
983
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
603
984
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
604
985
|
...(delegation ? { delegation } : {}),
|
|
605
986
|
};
|
|
@@ -615,7 +996,7 @@ export class WorkflowService {
|
|
|
615
996
|
cancelMilestone(repository, milestoneId, expectedRevision, reason) {
|
|
616
997
|
if (!reason.trim())
|
|
617
998
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
618
|
-
const { identity } = this
|
|
999
|
+
const { identity } = this.#mutationContext(repository);
|
|
619
1000
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
620
1001
|
assertExpectedRevision(milestone, expectedRevision);
|
|
621
1002
|
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
@@ -625,21 +1006,59 @@ export class WorkflowService {
|
|
|
625
1006
|
}
|
|
626
1007
|
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
627
1008
|
validatePlan(plan);
|
|
628
|
-
const { identity } = this
|
|
1009
|
+
const { identity } = this.#mutationContext(repository);
|
|
629
1010
|
const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
|
|
630
1011
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
631
1012
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
632
1013
|
assertExpectedRevision(task, expectedRevision);
|
|
633
|
-
|
|
634
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
635
|
-
}
|
|
1014
|
+
const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
|
|
636
1015
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
637
|
-
?
|
|
1016
|
+
? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
|
|
638
1017
|
: [];
|
|
639
1018
|
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
640
|
-
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) =>
|
|
1019
|
+
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => {
|
|
1020
|
+
if (event.decision !== 'continue-fix')
|
|
1021
|
+
return false;
|
|
1022
|
+
const passedBoundary = latestPassedVerifiedStepReviewEvent(this.store, task, event.stepId);
|
|
1023
|
+
return !passedBoundary || passedBoundary.recordedAt <= event.recordedAt;
|
|
1024
|
+
});
|
|
641
1025
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
642
|
-
const
|
|
1026
|
+
const currentPlanRiskAudit = task.status === 'needs_fix' || currentReplanDecisions.length > 0
|
|
1027
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
1028
|
+
: null;
|
|
1029
|
+
const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
|
|
1030
|
+
? findCurrentGuardedRemediationPosture(this.store, task, currentPlanRiskAudit)
|
|
1031
|
+
: null;
|
|
1032
|
+
const correctiveReplanPosture = classifiedCorrectivePosture?.posture === 'replan-required'
|
|
1033
|
+
? classifiedCorrectivePosture
|
|
1034
|
+
: currentPlanRiskAudit
|
|
1035
|
+
? identifyCorrectiveReplanPosture(task, currentPlanRiskAudit.reviewRequiredStepIds, correctiveDecisionEvents, readRemediationEvents(this.store, task))
|
|
1036
|
+
: null;
|
|
1037
|
+
const correctiveReplanAvailability = correctiveReplanPosture?.posture === 'replan-required'
|
|
1038
|
+
? evaluateTaskCorrectiveReplanAvailability(task, correctiveReplanPosture)
|
|
1039
|
+
: null;
|
|
1040
|
+
if (currentReplanDecisions.length > 0) {
|
|
1041
|
+
if (!correctiveReplanAvailability?.allowed) {
|
|
1042
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} is blocked for Task ${task.id}.`, {
|
|
1043
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1044
|
+
posture: correctiveReplanPosture?.posture ?? null,
|
|
1045
|
+
evaluationPhase: correctiveReplanAvailability?.phase ?? 'availability',
|
|
1046
|
+
blockingGuardIds: correctiveReplanAvailability?.blockingGuardIds ?? [],
|
|
1047
|
+
pendingGuardIds: correctiveReplanAvailability?.pendingGuardIds ?? [],
|
|
1048
|
+
guardResults: correctiveReplanAvailability?.guardResults ?? [],
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Legacy task plan-set cannot execute ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID}.`, {
|
|
1052
|
+
taskId: task.id,
|
|
1053
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1054
|
+
requiredAction: readTaskC1Posture(this.store, task).state === 'claimed'
|
|
1055
|
+
? 'Run task corrective-yield first, then use task corrective-replan-prepare.'
|
|
1056
|
+
: 'Use task corrective-replan-prepare, confirm, and execute.',
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
else if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
1060
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
1061
|
+
}
|
|
643
1062
|
if (currentHardBlockingDecisions.length > 0) {
|
|
644
1063
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
|
|
645
1064
|
taskId: task.id,
|
|
@@ -669,9 +1088,6 @@ export class WorkflowService {
|
|
|
669
1088
|
})),
|
|
670
1089
|
});
|
|
671
1090
|
}
|
|
672
|
-
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
673
|
-
? readCurrentPlanRiskAudit(this.store, task)
|
|
674
|
-
: null;
|
|
675
1091
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
676
1092
|
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
677
1093
|
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
@@ -714,7 +1130,7 @@ export class WorkflowService {
|
|
|
714
1130
|
}
|
|
715
1131
|
}
|
|
716
1132
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
717
|
-
const
|
|
1133
|
+
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
718
1134
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
719
1135
|
? { ...authorization, decision: 'superseded' }
|
|
720
1136
|
: authorization);
|
|
@@ -731,12 +1147,17 @@ export class WorkflowService {
|
|
|
731
1147
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
732
1148
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
733
1149
|
}
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1150
|
+
if (correctiveReplanPosture?.posture === 'replan-required') {
|
|
1151
|
+
const executionEvaluation = evaluateTaskCorrectiveReplanCandidate(task, correctiveReplanPosture, preparedPlan);
|
|
1152
|
+
if (!executionEvaluation.allowed) {
|
|
1153
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Transition ${TASK_CORRECTIVE_REPLAN_TRANSITION_ID} rejected the candidate Plan for Task ${task.id}.`, {
|
|
1154
|
+
transitionId: TASK_CORRECTIVE_REPLAN_TRANSITION_ID,
|
|
1155
|
+
evaluationPhase: executionEvaluation.phase,
|
|
1156
|
+
blockingGuardIds: executionEvaluation.blockingGuardIds,
|
|
1157
|
+
pendingGuardIds: executionEvaluation.pendingGuardIds,
|
|
1158
|
+
guardResults: executionEvaluation.guardResults,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
740
1161
|
}
|
|
741
1162
|
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
742
1163
|
throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
|
|
@@ -744,14 +1165,10 @@ export class WorkflowService {
|
|
|
744
1165
|
stepId: guardedRemediationGate.stepId,
|
|
745
1166
|
});
|
|
746
1167
|
}
|
|
747
|
-
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
748
|
-
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
749
|
-
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
750
|
-
}
|
|
751
1168
|
const candidate = {
|
|
752
1169
|
...task,
|
|
753
1170
|
status: 'awaiting_execution_authorization',
|
|
754
|
-
planHash:
|
|
1171
|
+
planHash: preparedPlan.planHash,
|
|
755
1172
|
planObjective: plan.objective,
|
|
756
1173
|
requirements: plan.requirements,
|
|
757
1174
|
acceptance: plan.acceptance,
|
|
@@ -771,6 +1188,18 @@ export class WorkflowService {
|
|
|
771
1188
|
};
|
|
772
1189
|
if (planRiskAudit)
|
|
773
1190
|
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
1191
|
+
const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
|
|
1192
|
+
if (stagedPlan.hash !== preparedPlan.planHash) {
|
|
1193
|
+
throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
|
|
1194
|
+
taskId: task.id,
|
|
1195
|
+
preparedPlanHash: preparedPlan.planHash,
|
|
1196
|
+
stagedPlanHash: stagedPlan.hash,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
1200
|
+
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
1201
|
+
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, preparedPlan.planHash, this.now());
|
|
1202
|
+
}
|
|
774
1203
|
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
775
1204
|
this.store.publishArtifact(stagedPlan);
|
|
776
1205
|
if (planRiskAudit)
|
|
@@ -781,9 +1210,10 @@ export class WorkflowService {
|
|
|
781
1210
|
return saved;
|
|
782
1211
|
}
|
|
783
1212
|
rebindTaskKnowledge(repository, taskId, expectedRevision) {
|
|
784
|
-
const { identity } = this
|
|
1213
|
+
const { identity } = this.#mutationContext(repository);
|
|
785
1214
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
786
1215
|
assertExpectedRevision(task, expectedRevision);
|
|
1216
|
+
this.assertTaskContextChangeAllowed(task);
|
|
787
1217
|
if (!taskCanRebindKnowledge(task)) {
|
|
788
1218
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while Task is ${task.status}.`);
|
|
789
1219
|
}
|
|
@@ -865,11 +1295,12 @@ export class WorkflowService {
|
|
|
865
1295
|
if (!delegationGrantId) {
|
|
866
1296
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
|
|
867
1297
|
}
|
|
868
|
-
const { identity } = this
|
|
1298
|
+
const { identity } = this.#mutationContext(repository);
|
|
869
1299
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
870
1300
|
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
871
1301
|
assertExpectedRevision(task, expectedTaskRevision);
|
|
872
1302
|
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
1303
|
+
this.assertTaskContextChangeAllowed(task);
|
|
873
1304
|
if (!taskCanRebindKnowledge(task)) {
|
|
874
1305
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
|
|
875
1306
|
}
|
|
@@ -887,48 +1318,75 @@ export class WorkflowService {
|
|
|
887
1318
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
888
1319
|
// leave the composite operation half-applied.
|
|
889
1320
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
this.
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1321
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
1322
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
1323
|
+
return runRollbackProjectTransaction({
|
|
1324
|
+
projectRoot,
|
|
1325
|
+
projectId: identity.projectId,
|
|
1326
|
+
transactionId: `tx-${createEntityId('TASK', this.now().getTime()).toLowerCase()}`,
|
|
1327
|
+
kind: 'delegated-task-context-refresh',
|
|
1328
|
+
now: this.now(),
|
|
1329
|
+
targetPaths: [
|
|
1330
|
+
relative(path.join(projectRoot, 'knowledge-map.json')),
|
|
1331
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'state.json')),
|
|
1332
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan.md')),
|
|
1333
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan-risk-audits.jsonl')),
|
|
1334
|
+
],
|
|
1335
|
+
mutate: () => {
|
|
1336
|
+
let knowledgeMap = currentMap;
|
|
1337
|
+
if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
|
|
1338
|
+
if (currentMap.status !== 'active' || !currentMap.approval) {
|
|
1339
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
1340
|
+
}
|
|
1341
|
+
assertDelegatedKnowledgeRefresh(refreshAssessment);
|
|
1342
|
+
if (inspectedMap.status !== 'stale') {
|
|
1343
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
1344
|
+
}
|
|
1345
|
+
if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
|
|
1346
|
+
this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
|
|
1347
|
+
}
|
|
1348
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
|
|
1349
|
+
knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision, true);
|
|
1350
|
+
transitions.push('project-memory reconcile');
|
|
1351
|
+
knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
|
|
1352
|
+
transitions.push('project-memory approve');
|
|
1353
|
+
}
|
|
1354
|
+
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
1355
|
+
if (task.knowledgeMapRevision === knowledgeMap.revision
|
|
1356
|
+
&& task.knowledgeMapHash === knowledgeMapHash) {
|
|
1357
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
1358
|
+
}
|
|
1359
|
+
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
1360
|
+
if (currentPlanRiskAudit) {
|
|
1361
|
+
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
1362
|
+
}
|
|
1363
|
+
transitions.push('task knowledge-rebind');
|
|
1364
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
|
|
1365
|
+
? 'Delegated Plan-bounded supporting-source context refresh.'
|
|
1366
|
+
: 'Delegated content-only context refresh.', delegationGrantId);
|
|
1367
|
+
transitions.push('task execution authorize');
|
|
1368
|
+
return {
|
|
1369
|
+
mode: refreshAssessment.mode,
|
|
1370
|
+
transitions,
|
|
1371
|
+
addedSources: refreshAssessment.addedSources,
|
|
1372
|
+
knowledgeMap,
|
|
1373
|
+
task: refreshedTask,
|
|
1374
|
+
};
|
|
1375
|
+
},
|
|
1376
|
+
validate: (result) => {
|
|
1377
|
+
const currentTask = this.store.readTask(identity.projectId, task.id);
|
|
1378
|
+
if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
|
|
1379
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
|
|
1380
|
+
}
|
|
1381
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
1382
|
+
if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
|
|
1383
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction Knowledge Map revision mismatch.');
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
});
|
|
929
1387
|
}
|
|
930
1388
|
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
931
|
-
const { identity } = this
|
|
1389
|
+
const { identity } = this.#mutationContext(repository);
|
|
932
1390
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
933
1391
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
934
1392
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -958,7 +1416,7 @@ export class WorkflowService {
|
|
|
958
1416
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
959
1417
|
}
|
|
960
1418
|
recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
|
|
961
|
-
const { identity } = this
|
|
1419
|
+
const { identity } = this.#mutationContext(repository);
|
|
962
1420
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
963
1421
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
964
1422
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -966,7 +1424,7 @@ export class WorkflowService {
|
|
|
966
1424
|
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
967
1425
|
}
|
|
968
1426
|
showTaskHandoff(repository, taskId) {
|
|
969
|
-
const { identity } = this
|
|
1427
|
+
const { identity } = this.#mutationContext(repository);
|
|
970
1428
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
971
1429
|
return {
|
|
972
1430
|
task,
|
|
@@ -974,8 +1432,8 @@ export class WorkflowService {
|
|
|
974
1432
|
claimTokenAvailable: false,
|
|
975
1433
|
};
|
|
976
1434
|
}
|
|
977
|
-
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
978
|
-
const context = this
|
|
1435
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
1436
|
+
const context = this.#mutationContext(repository);
|
|
979
1437
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
980
1438
|
assertExpectedRevision(task, expectedRevision);
|
|
981
1439
|
const normalizedActor = actor.trim();
|
|
@@ -1033,6 +1491,7 @@ export class WorkflowService {
|
|
|
1033
1491
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1034
1492
|
grantId,
|
|
1035
1493
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
1494
|
+
replacePending,
|
|
1036
1495
|
}, this.now());
|
|
1037
1496
|
releaseLease();
|
|
1038
1497
|
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
@@ -1044,15 +1503,15 @@ export class WorkflowService {
|
|
|
1044
1503
|
}
|
|
1045
1504
|
}
|
|
1046
1505
|
claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
|
|
1047
|
-
const context = this
|
|
1506
|
+
const context = this.#mutationContext(repository);
|
|
1048
1507
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1049
1508
|
assertExpectedRevision(task, expectedRevision);
|
|
1050
1509
|
const normalizedActor = actor.trim();
|
|
1051
1510
|
if (!normalizedActor) {
|
|
1052
1511
|
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
1053
1512
|
}
|
|
1054
|
-
const pendingHandoff =
|
|
1055
|
-
if (pendingHandoff.
|
|
1513
|
+
const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now());
|
|
1514
|
+
if (pendingHandoff.latestEvent.grantId) {
|
|
1056
1515
|
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
1057
1516
|
}
|
|
1058
1517
|
const existingLease = context.locks.inspect(taskId);
|
|
@@ -1085,7 +1544,14 @@ export class WorkflowService {
|
|
|
1085
1544
|
}
|
|
1086
1545
|
try {
|
|
1087
1546
|
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1088
|
-
return {
|
|
1547
|
+
return {
|
|
1548
|
+
task,
|
|
1549
|
+
event,
|
|
1550
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1551
|
+
writerCredential: acquiredHere
|
|
1552
|
+
? { disposition: 'issued', token: lease.token }
|
|
1553
|
+
: { disposition: 'renewed' },
|
|
1554
|
+
};
|
|
1089
1555
|
}
|
|
1090
1556
|
catch (error) {
|
|
1091
1557
|
if (acquiredHere)
|
|
@@ -1093,8 +1559,45 @@ export class WorkflowService {
|
|
|
1093
1559
|
throw error;
|
|
1094
1560
|
}
|
|
1095
1561
|
}
|
|
1562
|
+
replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
|
|
1563
|
+
const context = this.#mutationContext(repository);
|
|
1564
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1565
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1566
|
+
const normalizedActor = actor.trim();
|
|
1567
|
+
if (!normalizedActor)
|
|
1568
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1569
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1570
|
+
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1571
|
+
const action = String(navigation.action);
|
|
1572
|
+
const currentLease = context.locks.inspect(taskId);
|
|
1573
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
1574
|
+
const requiredActor = writerTokenContract
|
|
1575
|
+
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1576
|
+
: null;
|
|
1577
|
+
if (!requiredActor || requiredActor !== normalizedActor) {
|
|
1578
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
|
|
1579
|
+
taskId,
|
|
1580
|
+
posture: posture.state,
|
|
1581
|
+
claimant: posture.state === 'claimed' ? posture.claimant : null,
|
|
1582
|
+
actor: normalizedActor,
|
|
1583
|
+
requiredActor,
|
|
1584
|
+
nextAction: action,
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
const lease = currentLease
|
|
1588
|
+
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1589
|
+
: context.locks.acquire(taskId, normalizedActor);
|
|
1590
|
+
if (posture.state === 'claimed') {
|
|
1591
|
+
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
task,
|
|
1595
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1596
|
+
writerCredential: { disposition: 'issued', token: lease.token },
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1096
1599
|
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1097
|
-
const context = this
|
|
1600
|
+
const context = this.#mutationContext(repository);
|
|
1098
1601
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1099
1602
|
assertExpectedRevision(task, expectedRevision);
|
|
1100
1603
|
const lease = task.status === 'merged' || task.status === 'cancelled'
|
|
@@ -1106,7 +1609,7 @@ export class WorkflowService {
|
|
|
1106
1609
|
return { task, event };
|
|
1107
1610
|
}
|
|
1108
1611
|
startTask(repository, taskId, expectedRevision, workspaceOwner) {
|
|
1109
|
-
const { identity } = this
|
|
1612
|
+
const { identity } = this.#mutationContext(repository);
|
|
1110
1613
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1111
1614
|
assertExpectedRevision(task, expectedRevision);
|
|
1112
1615
|
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
@@ -1158,7 +1661,7 @@ export class WorkflowService {
|
|
|
1158
1661
|
return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
|
|
1159
1662
|
}
|
|
1160
1663
|
runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1161
|
-
const context = this
|
|
1664
|
+
const context = this.#mutationContext(repository);
|
|
1162
1665
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1163
1666
|
assertExpectedRevision(task, expectedRevision);
|
|
1164
1667
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
@@ -1187,6 +1690,7 @@ export class WorkflowService {
|
|
|
1187
1690
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
1188
1691
|
}
|
|
1189
1692
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1693
|
+
let resumesCorrectiveReplan = false;
|
|
1190
1694
|
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1191
1695
|
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.`, {
|
|
1192
1696
|
taskId,
|
|
@@ -1201,20 +1705,21 @@ export class WorkflowService {
|
|
|
1201
1705
|
&& guardedPosture.correctiveDecision.planHash !== task.planHash
|
|
1202
1706
|
? guardedPosture.correctiveDecision
|
|
1203
1707
|
: null;
|
|
1204
|
-
const recovery = staleDecision
|
|
1708
|
+
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1205
1709
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1206
1710
|
: null;
|
|
1207
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1711
|
+
const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1712
|
+
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1208
1713
|
}
|
|
1209
1714
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1210
1715
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1211
|
-
if (step.status === 'planned' && dirty.length > 0) {
|
|
1716
|
+
if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
|
|
1212
1717
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1213
1718
|
changedFiles: dirty,
|
|
1214
1719
|
});
|
|
1215
1720
|
}
|
|
1216
1721
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
1217
|
-
if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
|
|
1722
|
+
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
1218
1723
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
1219
1724
|
changedFiles: dirtyOutsideStep,
|
|
1220
1725
|
});
|
|
@@ -1272,7 +1777,8 @@ export class WorkflowService {
|
|
|
1272
1777
|
}
|
|
1273
1778
|
return {
|
|
1274
1779
|
task: saved,
|
|
1275
|
-
lease,
|
|
1780
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1781
|
+
writerCredential: { disposition: acquiredHere ? 'issued' : 'renewed' },
|
|
1276
1782
|
envelope: {
|
|
1277
1783
|
protocolVersion: PROTOCOL_VERSION,
|
|
1278
1784
|
packageVersion: PACKAGE_VERSION,
|
|
@@ -1308,14 +1814,14 @@ export class WorkflowService {
|
|
|
1308
1814
|
taskId,
|
|
1309
1815
|
stepId,
|
|
1310
1816
|
expectedRevision: saved.revision,
|
|
1311
|
-
writerToken: lease.token,
|
|
1817
|
+
writerToken: acquiredHere ? lease.token : null,
|
|
1312
1818
|
knowledgeMap: knowledgeSummary(knowledgeMap),
|
|
1313
1819
|
knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
|
|
1314
1820
|
},
|
|
1315
1821
|
};
|
|
1316
1822
|
}
|
|
1317
1823
|
taskContext(repository, taskId, role) {
|
|
1318
|
-
const { identity } = this
|
|
1824
|
+
const { identity } = this.#mutationContext(repository);
|
|
1319
1825
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1320
1826
|
const historicalStrictReview = role === 'independent-reviewer'
|
|
1321
1827
|
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
@@ -1394,7 +1900,7 @@ export class WorkflowService {
|
|
|
1394
1900
|
};
|
|
1395
1901
|
}
|
|
1396
1902
|
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
|
|
1397
|
-
const context = this
|
|
1903
|
+
const context = this.#mutationContext(repository);
|
|
1398
1904
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1399
1905
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1400
1906
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1480,7 +1986,7 @@ export class WorkflowService {
|
|
|
1480
1986
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
1481
1987
|
}
|
|
1482
1988
|
reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
|
|
1483
|
-
const context = this
|
|
1989
|
+
const context = this.#mutationContext(repository);
|
|
1484
1990
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1485
1991
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1486
1992
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1536,10 +2042,18 @@ export class WorkflowService {
|
|
|
1536
2042
|
resolution: currentCycle.resolution,
|
|
1537
2043
|
});
|
|
1538
2044
|
}
|
|
1539
|
-
|
|
2045
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2046
|
+
if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2047
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
|
|
2048
|
+
taskId: task.id,
|
|
2049
|
+
stepId,
|
|
2050
|
+
infrastructureFailures,
|
|
2051
|
+
requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
1540
2054
|
}
|
|
1541
2055
|
else if (currentCycle.resolution !== 'pending') {
|
|
1542
|
-
if (currentCycle.resolution === 'failed'
|
|
2056
|
+
if (currentCycle.resolution === 'failed') {
|
|
1543
2057
|
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1544
2058
|
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1545
2059
|
taskId: task.id,
|
|
@@ -1547,7 +2061,7 @@ export class WorkflowService {
|
|
|
1547
2061
|
resolution: currentCycle.resolution,
|
|
1548
2062
|
});
|
|
1549
2063
|
}
|
|
1550
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2064
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
1551
2065
|
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
1552
2066
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
1553
2067
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
@@ -1565,7 +2079,7 @@ export class WorkflowService {
|
|
|
1565
2079
|
reviewedCommit: launch.reviewedCommit,
|
|
1566
2080
|
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
1567
2081
|
}, this.now());
|
|
1568
|
-
if (recorded.resolution === 'failed'
|
|
2082
|
+
if (recorded.resolution === 'failed') {
|
|
1569
2083
|
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
1570
2084
|
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
1571
2085
|
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
@@ -1575,7 +2089,7 @@ export class WorkflowService {
|
|
|
1575
2089
|
resolution: recorded.resolution,
|
|
1576
2090
|
});
|
|
1577
2091
|
}
|
|
1578
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2092
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
1579
2093
|
completionCommit: reviewRecordedEvent.completionCommit,
|
|
1580
2094
|
checksHash: reviewRecordedEvent.checksHash,
|
|
1581
2095
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
@@ -1584,8 +2098,132 @@ export class WorkflowService {
|
|
|
1584
2098
|
}
|
|
1585
2099
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
1586
2100
|
}
|
|
2101
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2102
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2103
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2104
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2105
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2106
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2107
|
+
}
|
|
2108
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2109
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2110
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
|
|
2111
|
+
}
|
|
2112
|
+
const step = requireStep(task, stepId);
|
|
2113
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
2114
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
|
|
2115
|
+
}
|
|
2116
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2117
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
|
|
2118
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2119
|
+
}
|
|
2120
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2121
|
+
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
|
|
2122
|
+
}
|
|
2123
|
+
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2124
|
+
const context = this.#mutationContext(repository);
|
|
2125
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2126
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2127
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2128
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2129
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
|
|
2130
|
+
let review;
|
|
2131
|
+
try {
|
|
2132
|
+
review = validateExternalStrictStepReviewInput(input, packet);
|
|
2133
|
+
}
|
|
2134
|
+
catch (error) {
|
|
2135
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
|
|
2136
|
+
}
|
|
2137
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2138
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
|
|
2139
|
+
}
|
|
2140
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
2141
|
+
if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
|
|
2142
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
|
|
2143
|
+
taskId,
|
|
2144
|
+
stepId,
|
|
2145
|
+
resolution: currentCycle.resolution,
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2149
|
+
if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2150
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
|
|
2151
|
+
taskId,
|
|
2152
|
+
stepId,
|
|
2153
|
+
infrastructureFailures,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2157
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2158
|
+
review,
|
|
2159
|
+
reviewerOutputHash: strictReviewInputHash(review),
|
|
2160
|
+
reviewedCommit: packet.reviewedCommit,
|
|
2161
|
+
isolationResult: 'verified',
|
|
2162
|
+
}, this.now());
|
|
2163
|
+
if (recorded.resolution === 'failed') {
|
|
2164
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2165
|
+
completionCommit: recorded.reviewRecordedEvent.completionCommit,
|
|
2166
|
+
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2167
|
+
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2168
|
+
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2169
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2170
|
+
}
|
|
2171
|
+
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2172
|
+
return {
|
|
2173
|
+
mode: 'external-sealed',
|
|
2174
|
+
packetHash: packet.packetHash,
|
|
2175
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2176
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2177
|
+
task: saved,
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision) {
|
|
2181
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2182
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2183
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2184
|
+
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
2185
|
+
if (task.status !== 'validating' && !retryingUnverified) {
|
|
2186
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2187
|
+
}
|
|
2188
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2189
|
+
assertTaskBranch(identity.repositoryRoot, task);
|
|
2190
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
2191
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
|
|
2192
|
+
}
|
|
2193
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2194
|
+
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2195
|
+
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2196
|
+
}
|
|
2197
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
2198
|
+
}
|
|
2199
|
+
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2200
|
+
const context = this.#mutationContext(repository);
|
|
2201
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2202
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2203
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2204
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2205
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
|
|
2206
|
+
let review;
|
|
2207
|
+
try {
|
|
2208
|
+
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
2209
|
+
}
|
|
2210
|
+
catch (error) {
|
|
2211
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
|
|
2212
|
+
}
|
|
2213
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2214
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
|
|
2215
|
+
}
|
|
2216
|
+
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2217
|
+
return {
|
|
2218
|
+
mode: 'external-sealed',
|
|
2219
|
+
packetHash: packet.packetHash,
|
|
2220
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2221
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2222
|
+
task: saved,
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
1587
2225
|
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1588
|
-
const context = this
|
|
2226
|
+
const context = this.#mutationContext(repository);
|
|
1589
2227
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1590
2228
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1591
2229
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1617,11 +2255,11 @@ export class WorkflowService {
|
|
|
1617
2255
|
return saved;
|
|
1618
2256
|
}
|
|
1619
2257
|
recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
|
|
1620
|
-
const { identity } = this
|
|
2258
|
+
const { identity } = this.#mutationContext(repository);
|
|
1621
2259
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1622
2260
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1623
2261
|
assertExpectedRevision(task, expectedRevision);
|
|
1624
|
-
this.proveOptionalWriterLeaseToken(this
|
|
2262
|
+
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
1625
2263
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1626
2264
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
1627
2265
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
@@ -1659,7 +2297,7 @@ export class WorkflowService {
|
|
|
1659
2297
|
return saved;
|
|
1660
2298
|
}
|
|
1661
2299
|
recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1662
|
-
const context = this
|
|
2300
|
+
const context = this.#mutationContext(repository);
|
|
1663
2301
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1664
2302
|
assertExpectedRevision(task, expectedRevision);
|
|
1665
2303
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
@@ -1671,18 +2309,18 @@ export class WorkflowService {
|
|
|
1671
2309
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1672
2310
|
}
|
|
1673
2311
|
recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1674
|
-
const context = this
|
|
2312
|
+
const context = this.#mutationContext(repository);
|
|
1675
2313
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1676
2314
|
assertExpectedRevision(task, expectedRevision);
|
|
1677
2315
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
1678
2316
|
return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1679
2317
|
}
|
|
1680
2318
|
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
1681
|
-
const { identity } = this
|
|
2319
|
+
const { identity } = this.#mutationContext(repository);
|
|
1682
2320
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1683
2321
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1684
2322
|
assertExpectedRevision(task, expectedRevision);
|
|
1685
|
-
this.proveOptionalWriterLeaseToken(this
|
|
2323
|
+
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
1686
2324
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1687
2325
|
if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
1688
2326
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
@@ -1724,7 +2362,7 @@ export class WorkflowService {
|
|
|
1724
2362
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
1725
2363
|
}
|
|
1726
2364
|
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
1727
|
-
const { identity } = this
|
|
2365
|
+
const { identity } = this.#mutationContext(repository);
|
|
1728
2366
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1729
2367
|
assertExpectedRevision(task, expectedRevision);
|
|
1730
2368
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
@@ -1738,7 +2376,7 @@ export class WorkflowService {
|
|
|
1738
2376
|
return saved;
|
|
1739
2377
|
}
|
|
1740
2378
|
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
1741
|
-
const { identity } = this
|
|
2379
|
+
const { identity } = this.#mutationContext(repository);
|
|
1742
2380
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1743
2381
|
assertExpectedRevision(task, expectedRevision);
|
|
1744
2382
|
if (task.status !== 'awaiting_final_acceptance' || !task.resultHash || !task.evidenceHash) {
|
|
@@ -1776,7 +2414,7 @@ export class WorkflowService {
|
|
|
1776
2414
|
}, expectedRevision);
|
|
1777
2415
|
}
|
|
1778
2416
|
syncTaskBase(repository, taskId, expectedRevision) {
|
|
1779
|
-
const { identity } = this
|
|
2417
|
+
const { identity } = this.#mutationContext(repository);
|
|
1780
2418
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1781
2419
|
assertExpectedRevision(task, expectedRevision);
|
|
1782
2420
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
|
|
@@ -1828,7 +2466,7 @@ export class WorkflowService {
|
|
|
1828
2466
|
return saved;
|
|
1829
2467
|
}
|
|
1830
2468
|
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1831
|
-
const context = this
|
|
2469
|
+
const context = this.#mutationContext(repository);
|
|
1832
2470
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1833
2471
|
assertExpectedRevision(task, expectedRevision);
|
|
1834
2472
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -1850,7 +2488,7 @@ export class WorkflowService {
|
|
|
1850
2488
|
return saved;
|
|
1851
2489
|
}
|
|
1852
2490
|
confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1853
|
-
const context = this
|
|
2491
|
+
const context = this.#mutationContext(repository);
|
|
1854
2492
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1855
2493
|
assertExpectedRevision(task, expectedRevision);
|
|
1856
2494
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -1867,21 +2505,22 @@ export class WorkflowService {
|
|
|
1867
2505
|
return saved;
|
|
1868
2506
|
}
|
|
1869
2507
|
status(repository) {
|
|
1870
|
-
const
|
|
1871
|
-
const tasks = this.store.listTasks(identity.projectId);
|
|
1872
|
-
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
1873
|
-
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
|
|
2508
|
+
const snapshot = this.observationSnapshot(repository);
|
|
1874
2509
|
return {
|
|
1875
|
-
projectId: identity.projectId,
|
|
1876
|
-
discoveries:
|
|
1877
|
-
tasks,
|
|
1878
|
-
milestones:
|
|
1879
|
-
|
|
1880
|
-
|
|
2510
|
+
projectId: snapshot.identity.projectId,
|
|
2511
|
+
discoveries: snapshot.discoveries,
|
|
2512
|
+
tasks: snapshot.tasks,
|
|
2513
|
+
milestones: snapshot.milestones,
|
|
2514
|
+
milestoneInitialAssembly: snapshot.milestones
|
|
2515
|
+
.map((milestone) => this.describeMilestoneInitialAssembly(snapshot.identity.projectId, milestone, snapshot.tasks))
|
|
2516
|
+
.filter((value) => value !== null),
|
|
2517
|
+
delegations: snapshot.delegations,
|
|
2518
|
+
adoption: snapshot.adoption,
|
|
2519
|
+
observation: snapshot.observation,
|
|
1881
2520
|
};
|
|
1882
2521
|
}
|
|
1883
2522
|
recoverMilestoneTransactions(repository) {
|
|
1884
|
-
const { identity } = this
|
|
2523
|
+
const { identity } = this.#mutationContext(repository);
|
|
1885
2524
|
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
1886
2525
|
return {
|
|
1887
2526
|
projectId: identity.projectId,
|
|
@@ -1889,11 +2528,11 @@ export class WorkflowService {
|
|
|
1889
2528
|
};
|
|
1890
2529
|
}
|
|
1891
2530
|
prepareAdoption(repository) {
|
|
1892
|
-
const { identity } = this
|
|
2531
|
+
const { identity } = this.#mutationContext(repository);
|
|
1893
2532
|
return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
|
|
1894
2533
|
}
|
|
1895
2534
|
applyAdoption(repository, actor, confirmationCode) {
|
|
1896
|
-
const context = this
|
|
2535
|
+
const context = this.#mutationContext(repository);
|
|
1897
2536
|
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1898
2537
|
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
1899
2538
|
return applyAdoptionPosture(this.store, {
|
|
@@ -1910,53 +2549,186 @@ export class WorkflowService {
|
|
|
1910
2549
|
}, actor, confirmationCode);
|
|
1911
2550
|
}
|
|
1912
2551
|
updatePreflight(repository) {
|
|
1913
|
-
const
|
|
1914
|
-
const tasks =
|
|
1915
|
-
const milestones =
|
|
1916
|
-
const dirty = changedFiles(
|
|
2552
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2553
|
+
const tasks = snapshot.tasks;
|
|
2554
|
+
const milestones = snapshot.milestones;
|
|
2555
|
+
const dirty = changedFiles(snapshot.identity.repositoryRoot);
|
|
1917
2556
|
const runningSteps = tasks.flatMap((task) => task.steps
|
|
1918
2557
|
.filter((step) => step.status === 'in_progress')
|
|
1919
2558
|
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
1920
|
-
const leases =
|
|
2559
|
+
const leases = snapshot.leases;
|
|
1921
2560
|
const now = this.now().getTime();
|
|
1922
2561
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1923
2562
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2563
|
+
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2564
|
+
const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
2565
|
+
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
2566
|
+
...base,
|
|
2567
|
+
milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
|
|
2568
|
+
}));
|
|
2569
|
+
const dependencyPresent = inspectedVersions.declared !== null
|
|
2570
|
+
|| inspectedVersions.locked !== null
|
|
2571
|
+
|| inspectedVersions.installed !== null;
|
|
2572
|
+
const versionValues = dependencyPresent
|
|
2573
|
+
? [
|
|
2574
|
+
inspectedVersions.declared,
|
|
2575
|
+
inspectedVersions.locked,
|
|
2576
|
+
inspectedVersions.installed,
|
|
2577
|
+
inspectedVersions.currentBranch,
|
|
2578
|
+
...milestoneBases.map((base) => base.version),
|
|
2579
|
+
]
|
|
2580
|
+
: [];
|
|
2581
|
+
const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
|
|
2582
|
+
const versionBlockers = dependencyPresent && !versionsAligned
|
|
2583
|
+
? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
|
|
2584
|
+
: [];
|
|
1924
2585
|
const blockers = [
|
|
1925
2586
|
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1926
2587
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1927
2588
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1928
|
-
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
repositoryIdentity: context.identity,
|
|
1933
|
-
packageVersion: PACKAGE_VERSION,
|
|
1934
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
1935
|
-
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1936
|
-
tasks,
|
|
1937
|
-
milestones,
|
|
1938
|
-
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
1939
|
-
blockers,
|
|
1940
|
-
now: this.now(),
|
|
1941
|
-
}, readCurrentAdoptionPosture(this.store, context.identity.projectId));
|
|
2589
|
+
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2590
|
+
...versionBlockers,
|
|
2591
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
2592
|
+
];
|
|
1942
2593
|
return {
|
|
1943
2594
|
safe: blockers.length === 0,
|
|
1944
2595
|
packageVersion: PACKAGE_VERSION,
|
|
1945
2596
|
protocolVersion: PROTOCOL_VERSION,
|
|
1946
2597
|
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1947
|
-
projectId:
|
|
1948
|
-
repositoryRoot:
|
|
2598
|
+
projectId: snapshot.identity.projectId,
|
|
2599
|
+
repositoryRoot: snapshot.identity.repositoryRoot,
|
|
1949
2600
|
clean: dirty.length === 0,
|
|
1950
2601
|
changedFiles: dirty,
|
|
1951
2602
|
runningSteps,
|
|
1952
|
-
activeLeases,
|
|
1953
|
-
staleLeases,
|
|
2603
|
+
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2604
|
+
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
1954
2605
|
blockers,
|
|
1955
|
-
|
|
2606
|
+
workflowVersions: {
|
|
2607
|
+
runtime: inspectedVersions.runtime,
|
|
2608
|
+
declared: inspectedVersions.declared,
|
|
2609
|
+
locked: inspectedVersions.locked,
|
|
2610
|
+
installed: inspectedVersions.installed,
|
|
2611
|
+
currentBranch: inspectedVersions.currentBranch,
|
|
2612
|
+
milestoneBases,
|
|
2613
|
+
},
|
|
2614
|
+
dependencyTransport: {
|
|
2615
|
+
safe: versionBlockers.length === 0,
|
|
2616
|
+
requiredAction: versionBlockers.length === 0
|
|
2617
|
+
? null
|
|
2618
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
2619
|
+
},
|
|
2620
|
+
adoption: snapshot.adoption,
|
|
2621
|
+
observation: snapshot.observation,
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
dependencyProvenanceRecoveryPreflight(repository, taskId) {
|
|
2625
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2626
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
2627
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency provenance recovery requires a coherent observation boundary.', {
|
|
2628
|
+
observation: snapshot.observation,
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2632
|
+
if (!task)
|
|
2633
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2634
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2635
|
+
}
|
|
2636
|
+
recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
2637
|
+
const normalizedActor = actor.trim();
|
|
2638
|
+
const normalizedReason = reason.trim();
|
|
2639
|
+
if (!normalizedActor)
|
|
2640
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires an actor.');
|
|
2641
|
+
if (!normalizedReason)
|
|
2642
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires a reason.');
|
|
2643
|
+
const context = this.#mutationContext(repository);
|
|
2644
|
+
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
2645
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2646
|
+
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead && record.packageVersion === PACKAGE_VERSION);
|
|
2647
|
+
if (prior && current.systemCommits.includes(currentHead))
|
|
2648
|
+
return current;
|
|
2649
|
+
assertExpectedRevision(current, expectedRevision);
|
|
2650
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2651
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2652
|
+
if (!task)
|
|
2653
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2654
|
+
const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2655
|
+
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
2656
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
2657
|
+
taskId,
|
|
2658
|
+
blockers: preflight.blockers,
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
return context.store.writeTask({
|
|
2662
|
+
...task,
|
|
2663
|
+
systemCommits: [...task.systemCommits, preflight.commitSha],
|
|
2664
|
+
dependencyProvenanceRecoveries: [
|
|
2665
|
+
...(task.dependencyProvenanceRecoveries ?? []),
|
|
2666
|
+
{
|
|
2667
|
+
commitSha: preflight.commitSha,
|
|
2668
|
+
parentCommitSha: preflight.parentCommitSha,
|
|
2669
|
+
packageName: PACKAGE_NAME,
|
|
2670
|
+
packageVersion: PACKAGE_VERSION,
|
|
2671
|
+
files: ['package-lock.json', 'package.json'],
|
|
2672
|
+
actor: normalizedActor,
|
|
2673
|
+
reason: normalizedReason,
|
|
2674
|
+
recordedAt: this.now().toISOString(),
|
|
2675
|
+
retainedHistoricalCommits: preflight.retainedHistoricalCommits,
|
|
2676
|
+
},
|
|
2677
|
+
],
|
|
2678
|
+
}, expectedRevision);
|
|
2679
|
+
}
|
|
2680
|
+
milestoneProgress(repository, milestoneId) {
|
|
2681
|
+
const snapshot = this.observationSnapshot(repository);
|
|
2682
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
2683
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone progress requires a coherent read-only observation boundary.', {
|
|
2684
|
+
observation: snapshot.observation,
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
const milestone = snapshot.milestones.find((candidate) => candidate.id === milestoneId);
|
|
2688
|
+
if (!milestone)
|
|
2689
|
+
throw new WorkflowError('NOT_FOUND', `Milestone ${milestoneId} was not found in this repository.`);
|
|
2690
|
+
const tasksById = new Map(snapshot.tasks.map((task) => [task.id, task]));
|
|
2691
|
+
const tasks = milestone.memberships.map((membership, index) => {
|
|
2692
|
+
const task = tasksById.get(membership.taskId);
|
|
2693
|
+
if (!task)
|
|
2694
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
|
|
2695
|
+
const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
|
|
2696
|
+
return {
|
|
2697
|
+
ordinal: `T${String(index + 1).padStart(2, '0')}`,
|
|
2698
|
+
taskId: task.id,
|
|
2699
|
+
title: task.title,
|
|
2700
|
+
status: task.status,
|
|
2701
|
+
revision: task.revision,
|
|
2702
|
+
disposition: membership.disposition,
|
|
2703
|
+
membershipReason: membership.reason,
|
|
2704
|
+
replacementForTaskId: task.replacementForTaskId ?? null,
|
|
2705
|
+
replacedByTaskId: task.replacedByTaskId ?? null,
|
|
2706
|
+
stepsCompleted,
|
|
2707
|
+
stepsTotal: task.steps.length,
|
|
2708
|
+
steps: task.steps.map((step, stepIndex) => ({
|
|
2709
|
+
ordinal: `S${String(stepIndex + 1).padStart(2, '0')}`,
|
|
2710
|
+
stepId: step.id,
|
|
2711
|
+
title: step.title,
|
|
2712
|
+
status: step.status,
|
|
2713
|
+
})),
|
|
2714
|
+
};
|
|
2715
|
+
});
|
|
2716
|
+
const required = tasks.filter((task) => task.disposition === 'required');
|
|
2717
|
+
return {
|
|
2718
|
+
readOnly: true,
|
|
2719
|
+
projectId: snapshot.identity.projectId,
|
|
2720
|
+
milestoneId: milestone.id,
|
|
2721
|
+
milestoneTitle: milestone.title,
|
|
2722
|
+
milestoneStatus: milestone.status,
|
|
2723
|
+
milestoneRevision: milestone.revision,
|
|
2724
|
+
membershipRevision: milestone.membershipRevision,
|
|
2725
|
+
tasksCompleted: required.filter((task) => task.status === 'merged').length,
|
|
2726
|
+
tasksRequired: required.length,
|
|
2727
|
+
tasks,
|
|
1956
2728
|
};
|
|
1957
2729
|
}
|
|
1958
2730
|
alpha6StrictReviewRescuePreflight(repository) {
|
|
1959
|
-
const context = this
|
|
2731
|
+
const context = this.#mutationContext(repository);
|
|
1960
2732
|
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
1961
2733
|
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1962
2734
|
const leases = context.locks.list();
|
|
@@ -2051,15 +2823,15 @@ export class WorkflowService {
|
|
|
2051
2823
|
projectId: context.identity.projectId,
|
|
2052
2824
|
repositoryRoot: context.identity.repositoryRoot,
|
|
2053
2825
|
clean: dirty.length === 0,
|
|
2054
|
-
activeLeases,
|
|
2055
|
-
staleLeases,
|
|
2826
|
+
activeLeases: activeLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2827
|
+
staleLeases: staleLeases.map((lease) => toPublicWriterLease(lease, new Date(now))),
|
|
2056
2828
|
candidate,
|
|
2057
2829
|
blockers,
|
|
2058
2830
|
requiredActions,
|
|
2059
2831
|
};
|
|
2060
2832
|
}
|
|
2061
2833
|
scanKnowledgeMap(repository) {
|
|
2062
|
-
const { identity } = this
|
|
2834
|
+
const { identity } = this.#mutationContext(repository);
|
|
2063
2835
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2064
2836
|
try {
|
|
2065
2837
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
@@ -2072,11 +2844,11 @@ export class WorkflowService {
|
|
|
2072
2844
|
}
|
|
2073
2845
|
}
|
|
2074
2846
|
showKnowledgeMap(repository) {
|
|
2075
|
-
const { identity } = this
|
|
2847
|
+
const { identity } = this.#mutationContext(repository);
|
|
2076
2848
|
return this.store.readKnowledgeMap(identity.projectId);
|
|
2077
2849
|
}
|
|
2078
2850
|
knowledgeMapStatus(repository) {
|
|
2079
|
-
const { identity } = this
|
|
2851
|
+
const { identity } = this.#mutationContext(repository);
|
|
2080
2852
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2081
2853
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2082
2854
|
return inspectKnowledgeMap(current, scanned);
|
|
@@ -2085,7 +2857,7 @@ export class WorkflowService {
|
|
|
2085
2857
|
return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
|
|
2086
2858
|
}
|
|
2087
2859
|
approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
|
|
2088
|
-
const { identity } = this
|
|
2860
|
+
const { identity } = this.#mutationContext(repository);
|
|
2089
2861
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2090
2862
|
assertExpectedRevision(current, expectedRevision);
|
|
2091
2863
|
const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
@@ -2101,25 +2873,36 @@ export class WorkflowService {
|
|
|
2101
2873
|
};
|
|
2102
2874
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
2103
2875
|
}
|
|
2104
|
-
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
2105
|
-
const { identity } = this
|
|
2876
|
+
reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
|
|
2877
|
+
const { identity } = this.#mutationContext(repository);
|
|
2106
2878
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2107
2879
|
assertExpectedRevision(current, expectedRevision);
|
|
2108
2880
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2881
|
+
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2882
|
+
const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
|
|
2883
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
2884
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
|
|
2885
|
+
if (refreshOwner) {
|
|
2886
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2887
|
+
taskId: refreshOwner.task.id,
|
|
2888
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2109
2892
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
2110
2893
|
}
|
|
2111
2894
|
graphRefreshRequest(repository, graphKind, mode = 'moderate') {
|
|
2112
|
-
const { identity } = this
|
|
2895
|
+
const { identity } = this.#mutationContext(repository);
|
|
2113
2896
|
return createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2114
2897
|
}
|
|
2115
2898
|
graphStatus(repository, graphKind, mode = 'moderate') {
|
|
2116
|
-
const { identity } = this
|
|
2899
|
+
const { identity } = this.#mutationContext(repository);
|
|
2117
2900
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2118
2901
|
const binding = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2119
2902
|
return { request, binding: binding ? inspectGraphBinding(binding, request) : null };
|
|
2120
2903
|
}
|
|
2121
2904
|
bindGraph(repository, expectedRevision, request, evidence) {
|
|
2122
|
-
const { identity } = this
|
|
2905
|
+
const { identity } = this.#mutationContext(repository);
|
|
2123
2906
|
const currentRequest = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, request.graphKind, request.mode, this.now());
|
|
2124
2907
|
validateGraphRequestCurrent(request, currentRequest);
|
|
2125
2908
|
const existing = this.readGraphBindingOrNull(identity.projectId, request.graphKind);
|
|
@@ -2127,13 +2910,78 @@ export class WorkflowService {
|
|
|
2127
2910
|
return this.store.writeGraphBinding(binding, expectedRevision);
|
|
2128
2911
|
}
|
|
2129
2912
|
recordGraphFallback(repository, graphKind, mode, expectedRevision, reason) {
|
|
2130
|
-
const { identity } = this
|
|
2913
|
+
const { identity } = this.#mutationContext(repository);
|
|
2131
2914
|
const request = createGraphRefreshRequest(identity.repositoryRoot, identity.projectId, graphKind, mode, this.now());
|
|
2132
2915
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2133
2916
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
2134
2917
|
}
|
|
2135
|
-
next(repository) {
|
|
2918
|
+
next(repository, taskId = null) {
|
|
2136
2919
|
const status = this.status(repository);
|
|
2920
|
+
if (status.observation.kind === 'blocked') {
|
|
2921
|
+
return {
|
|
2922
|
+
scope: 'project',
|
|
2923
|
+
projectId: status.projectId,
|
|
2924
|
+
status: 'observation-blocked',
|
|
2925
|
+
action: 'blocked',
|
|
2926
|
+
observation: status.observation,
|
|
2927
|
+
navigation: {
|
|
2928
|
+
kind: 'blocked',
|
|
2929
|
+
reasonCodes: status.observation.reasonCodes,
|
|
2930
|
+
availableRepairTransitionId: null,
|
|
2931
|
+
},
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
if (status.observation.kind === 'repair') {
|
|
2935
|
+
const repair = status.observation.repair;
|
|
2936
|
+
return {
|
|
2937
|
+
scope: 'project',
|
|
2938
|
+
projectId: status.projectId,
|
|
2939
|
+
status: 'repair-required',
|
|
2940
|
+
action: repair?.command ?? 'blocked',
|
|
2941
|
+
observation: status.observation,
|
|
2942
|
+
navigation: {
|
|
2943
|
+
kind: repair ? 'repair' : 'blocked',
|
|
2944
|
+
transitionId: null,
|
|
2945
|
+
availableRepairTransitionId: null,
|
|
2946
|
+
...(repair ? {
|
|
2947
|
+
command: repair.command,
|
|
2948
|
+
cataloged: repair.cataloged,
|
|
2949
|
+
...('available' in repair ? { available: repair.available } : {}),
|
|
2950
|
+
bindings: repair.command === 'milestone recover'
|
|
2951
|
+
? {
|
|
2952
|
+
projectId: repair.projectId,
|
|
2953
|
+
milestoneIds: repair.milestoneIds,
|
|
2954
|
+
transactionIds: repair.transactionIds,
|
|
2955
|
+
}
|
|
2956
|
+
: 'taskIds' in repair
|
|
2957
|
+
? {
|
|
2958
|
+
projectId: repair.projectId,
|
|
2959
|
+
taskIds: repair.taskIds,
|
|
2960
|
+
transactionIds: repair.transactionIds,
|
|
2961
|
+
}
|
|
2962
|
+
: {
|
|
2963
|
+
projectId: repair.projectId,
|
|
2964
|
+
entityIds: repair.entityIds,
|
|
2965
|
+
},
|
|
2966
|
+
} : {}),
|
|
2967
|
+
reasonCodes: status.observation.reasonCodes,
|
|
2968
|
+
},
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
if (!status.adoption) {
|
|
2972
|
+
return {
|
|
2973
|
+
scope: 'project',
|
|
2974
|
+
projectId: status.projectId,
|
|
2975
|
+
status: 'observation-blocked',
|
|
2976
|
+
action: 'blocked',
|
|
2977
|
+
observation: status.observation,
|
|
2978
|
+
navigation: {
|
|
2979
|
+
kind: 'blocked',
|
|
2980
|
+
reasonCodes: status.observation.reasonCodes,
|
|
2981
|
+
availableRepairTransitionId: null,
|
|
2982
|
+
},
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2137
2985
|
const adoptionInfo = {
|
|
2138
2986
|
status: status.adoption.status,
|
|
2139
2987
|
safe: status.adoption.safe,
|
|
@@ -2151,33 +2999,149 @@ export class WorkflowService {
|
|
|
2151
2999
|
status: status.adoption.status,
|
|
2152
3000
|
action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2153
3001
|
adoption: adoptionInfo,
|
|
3002
|
+
observation: status.observation,
|
|
3003
|
+
navigation: status.adoption.safe
|
|
3004
|
+
? {
|
|
3005
|
+
kind: 'human-gate',
|
|
3006
|
+
transitionId: null,
|
|
3007
|
+
cataloged: false,
|
|
3008
|
+
command: 'state adoption-apply',
|
|
3009
|
+
bindings: {
|
|
3010
|
+
projectId: status.projectId,
|
|
3011
|
+
confirmationCode: status.adoption.confirmationCode,
|
|
3012
|
+
confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
|
|
3013
|
+
actorRequired: true,
|
|
3014
|
+
},
|
|
3015
|
+
}
|
|
3016
|
+
: {
|
|
3017
|
+
kind: 'blocked',
|
|
3018
|
+
transitionId: null,
|
|
3019
|
+
availableRepairTransitionId: null,
|
|
3020
|
+
reasonCodes: ['ADOPTION_MISSING'],
|
|
3021
|
+
blockers: status.adoption.blockers,
|
|
3022
|
+
},
|
|
2154
3023
|
};
|
|
2155
3024
|
}
|
|
2156
|
-
const withAdoption = (value) => ({
|
|
3025
|
+
const withAdoption = (value) => ({
|
|
3026
|
+
...value,
|
|
3027
|
+
adoption: adoptionInfo,
|
|
3028
|
+
observation: status.observation,
|
|
3029
|
+
});
|
|
2157
3030
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
3031
|
+
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
2158
3032
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
2159
|
-
const
|
|
3033
|
+
const requestedTask = taskId === null
|
|
3034
|
+
? null
|
|
3035
|
+
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
3036
|
+
if (taskId !== null && !requestedTask) {
|
|
3037
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3038
|
+
}
|
|
3039
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
|
|
3040
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
3041
|
+
taskId: requestedTask.id,
|
|
3042
|
+
taskStatus: requestedTask.status,
|
|
3043
|
+
milestoneId: requestedTask.milestoneId,
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
const activeTask = requestedTask
|
|
3047
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2160
3048
|
if (activeTask) {
|
|
2161
|
-
const
|
|
2162
|
-
|
|
3049
|
+
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3050
|
+
if (dependencyRecovery.eligible) {
|
|
3051
|
+
return withAdoption({
|
|
3052
|
+
scope: 'task',
|
|
3053
|
+
id: activeTask.id,
|
|
3054
|
+
status: activeTask.status,
|
|
3055
|
+
action: 'update dependency-provenance-recover',
|
|
3056
|
+
revision: activeTask.revision,
|
|
3057
|
+
dependencyProvenanceRecovery: dependencyRecovery,
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
const activeTaskMilestone = activeTask.milestoneId
|
|
3061
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
2163
3062
|
: null;
|
|
3063
|
+
const milestoneCancellationSafe = Boolean(activeTaskMilestone
|
|
3064
|
+
&& !activeTask.steps.some((step) => step.status === 'in_progress')
|
|
3065
|
+
&& status.observation.leases.length === 0
|
|
3066
|
+
&& status.observation.coreTaskOperations.length === 0
|
|
3067
|
+
&& status.observation.pendingProjectTransactions.length === 0
|
|
3068
|
+
&& status.observation.pendingMilestoneTransactions.length === 0
|
|
3069
|
+
&& status.observation.pendingTaskTransactions.length === 0
|
|
3070
|
+
&& status.observation.corruptTaskTransactions.length === 0);
|
|
3071
|
+
const withMilestoneCancellation = (value) => (milestoneCancellationSafe
|
|
3072
|
+
? {
|
|
3073
|
+
...value,
|
|
3074
|
+
milestoneCancellationOption: {
|
|
3075
|
+
command: 'milestone cancel',
|
|
3076
|
+
milestoneId: activeTaskMilestone.id,
|
|
3077
|
+
expectedRevision: activeTaskMilestone.revision,
|
|
3078
|
+
reasonRequired: true,
|
|
3079
|
+
taskDisposition: 'retained-historical',
|
|
3080
|
+
},
|
|
3081
|
+
}
|
|
3082
|
+
: value);
|
|
3083
|
+
let activePlanRiskAudit = null;
|
|
3084
|
+
let planRiskAuditRecovery = null;
|
|
3085
|
+
if (activeTask.planHash) {
|
|
3086
|
+
try {
|
|
3087
|
+
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
3088
|
+
}
|
|
3089
|
+
catch (error) {
|
|
3090
|
+
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3091
|
+
throw error;
|
|
3092
|
+
planRiskAuditRecovery = {
|
|
3093
|
+
scope: 'task',
|
|
3094
|
+
id: activeTask.id,
|
|
3095
|
+
status: activeTask.status,
|
|
3096
|
+
action: 'task plan-risk-audit',
|
|
3097
|
+
revision: activeTask.revision,
|
|
3098
|
+
planRiskAuditRecovery: {
|
|
3099
|
+
state: 'required',
|
|
3100
|
+
reason: error.message,
|
|
3101
|
+
currentBriefHash: activeTask.briefHash,
|
|
3102
|
+
currentPlanHash: activeTask.planHash,
|
|
3103
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
3104
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
3105
|
+
},
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
2164
3109
|
const taskNext = activeTask.status === 'planning'
|
|
2165
3110
|
? null
|
|
2166
|
-
:
|
|
3111
|
+
: planRiskAuditRecovery
|
|
3112
|
+
?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2167
3113
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
2168
3114
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2169
3115
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
2170
3116
|
&& taskNext.correctiveDecisionGate?.hardStop === true;
|
|
3117
|
+
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
2171
3118
|
if (taskNext?.action === 'task step-complete'
|
|
2172
3119
|
|| taskNext?.action === 'task step-review'
|
|
2173
3120
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
2174
|
-
|| hardStop
|
|
2175
|
-
|
|
3121
|
+
|| hardStop
|
|
3122
|
+
|| terminalCorrectiveDecision === 'split-required'
|
|
3123
|
+
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
3124
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
2176
3125
|
}
|
|
2177
3126
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
2178
|
-
return withAdoption(taskNext);
|
|
3127
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
3128
|
+
}
|
|
3129
|
+
let knowledgeMap;
|
|
3130
|
+
try {
|
|
3131
|
+
knowledgeMap = inspectCurrentKnowledgeMap();
|
|
3132
|
+
}
|
|
3133
|
+
catch (error) {
|
|
3134
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3135
|
+
throw error;
|
|
3136
|
+
return withAdoption(withMilestoneCancellation({
|
|
3137
|
+
scope: 'project-memory',
|
|
3138
|
+
status: 'missing',
|
|
3139
|
+
action: 'project-memory scan',
|
|
3140
|
+
stalenessOriginTaskId: activeTask.id,
|
|
3141
|
+
authorizesTask: false,
|
|
3142
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
3143
|
+
}));
|
|
2179
3144
|
}
|
|
2180
|
-
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
2181
3145
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
2182
3146
|
const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
|
|
2183
3147
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
@@ -2193,12 +3157,28 @@ export class WorkflowService {
|
|
|
2193
3157
|
const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
|
|
2194
3158
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2195
3159
|
: [];
|
|
2196
|
-
|
|
2197
|
-
|
|
3160
|
+
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
3161
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
|
|
3162
|
+
if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
|
|
3163
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
|
|
3164
|
+
taskId: requestedTask.id,
|
|
3165
|
+
blockingTaskId: refreshOwner.task.id,
|
|
3166
|
+
requiredAction: 'Run repository next and its exact task context-refresh action first.',
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return withAdoption(withMilestoneCancellation({
|
|
3171
|
+
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2198
3172
|
status: knowledgeMap.status,
|
|
2199
|
-
action,
|
|
3173
|
+
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
2200
3174
|
revision: knowledgeMap.revision,
|
|
2201
|
-
|
|
3175
|
+
...(contextRefreshOptions.length > 0
|
|
3176
|
+
? { taskId: activeTask.id }
|
|
3177
|
+
: {
|
|
3178
|
+
stalenessOriginTaskId: activeTask.id,
|
|
3179
|
+
authorizesTask: false,
|
|
3180
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
3181
|
+
}),
|
|
2202
3182
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2203
3183
|
...(contextRefreshOptions.length > 0
|
|
2204
3184
|
? {
|
|
@@ -2219,14 +3199,14 @@ export class WorkflowService {
|
|
|
2219
3199
|
},
|
|
2220
3200
|
}
|
|
2221
3201
|
: {}),
|
|
2222
|
-
});
|
|
3202
|
+
}));
|
|
2223
3203
|
}
|
|
2224
3204
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2225
3205
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
2226
3206
|
activeTask.planHash &&
|
|
2227
3207
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
2228
3208
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
2229
|
-
return withAdoption({
|
|
3209
|
+
return withAdoption(withMilestoneCancellation({
|
|
2230
3210
|
scope: 'task',
|
|
2231
3211
|
id: activeTask.id,
|
|
2232
3212
|
status: activeTask.status,
|
|
@@ -2234,12 +3214,69 @@ export class WorkflowService {
|
|
|
2234
3214
|
revision: activeTask.revision,
|
|
2235
3215
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
2236
3216
|
knowledgeMapHash,
|
|
2237
|
-
});
|
|
3217
|
+
}));
|
|
2238
3218
|
}
|
|
2239
|
-
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
3219
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
|
|
3220
|
+
}
|
|
3221
|
+
let repositoryKnowledgeMap;
|
|
3222
|
+
try {
|
|
3223
|
+
repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
|
|
3224
|
+
}
|
|
3225
|
+
catch (error) {
|
|
3226
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3227
|
+
throw error;
|
|
3228
|
+
return withAdoption({
|
|
3229
|
+
scope: 'project-memory',
|
|
3230
|
+
status: 'missing',
|
|
3231
|
+
action: 'project-memory scan',
|
|
3232
|
+
authorizesMilestone: false,
|
|
3233
|
+
provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
|
|
3237
|
+
const action = repositoryKnowledgeMap.status === 'stale'
|
|
3238
|
+
? 'project-memory reconcile'
|
|
3239
|
+
: 'project-memory approve';
|
|
3240
|
+
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
3241
|
+
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
|
|
3242
|
+
: [];
|
|
3243
|
+
return withAdoption({
|
|
3244
|
+
scope: 'project-memory',
|
|
3245
|
+
status: repositoryKnowledgeMap.status,
|
|
3246
|
+
action,
|
|
3247
|
+
revision: repositoryKnowledgeMap.revision,
|
|
3248
|
+
authorizesMilestone: false,
|
|
3249
|
+
provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
|
|
3250
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3251
|
+
});
|
|
2240
3252
|
}
|
|
2241
3253
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2242
3254
|
if (activeMilestone) {
|
|
3255
|
+
const initialAssembly = status.milestoneInitialAssembly
|
|
3256
|
+
.find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
|
|
3257
|
+
if (initialAssembly) {
|
|
3258
|
+
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
3259
|
+
if (activeDiscovery) {
|
|
3260
|
+
return withAdoption({
|
|
3261
|
+
scope: 'discovery',
|
|
3262
|
+
id: activeDiscovery.id,
|
|
3263
|
+
status: activeDiscovery.status,
|
|
3264
|
+
action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
|
|
3265
|
+
readiness: assessDiscovery(activeDiscovery),
|
|
3266
|
+
milestoneInitialAssembly: initialAssembly,
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
return withAdoption({
|
|
3270
|
+
scope: 'milestone',
|
|
3271
|
+
id: activeMilestone.id,
|
|
3272
|
+
status: activeMilestone.status,
|
|
3273
|
+
action: 'milestone initial-assembly',
|
|
3274
|
+
revision: activeMilestone.revision,
|
|
3275
|
+
linkedTaskIds: initialAssembly.linkedTaskIds,
|
|
3276
|
+
executionAvailable: initialAssembly.executionAvailable,
|
|
3277
|
+
milestoneInitialAssembly: initialAssembly,
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
2243
3280
|
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
2244
3281
|
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
2245
3282
|
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
@@ -2290,6 +3327,19 @@ export class WorkflowService {
|
|
|
2290
3327
|
status: activeMilestone.status,
|
|
2291
3328
|
action: nextForMilestone(activeMilestone),
|
|
2292
3329
|
revision: activeMilestone.revision,
|
|
3330
|
+
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
3331
|
+
? {
|
|
3332
|
+
milestoneAuthorizationOptions: [
|
|
3333
|
+
{ action: 'milestone authorize', mode: 'direct-human' },
|
|
3334
|
+
{
|
|
3335
|
+
action: 'milestone autonomy-prepare',
|
|
3336
|
+
mode: 'delegated',
|
|
3337
|
+
requiresExactHumanConfirmation: true,
|
|
3338
|
+
maximumDurationHours: 72,
|
|
3339
|
+
},
|
|
3340
|
+
],
|
|
3341
|
+
}
|
|
3342
|
+
: {}),
|
|
2293
3343
|
...(activeMilestone.status === 'active'
|
|
2294
3344
|
? {
|
|
2295
3345
|
completionPolicy: {
|
|
@@ -2446,7 +3496,20 @@ export class WorkflowService {
|
|
|
2446
3496
|
throw error;
|
|
2447
3497
|
}
|
|
2448
3498
|
}
|
|
2449
|
-
|
|
3499
|
+
let catalogCorrectiveReplanPosture = guardedCorrectivePosture?.posture === 'replan-required'
|
|
3500
|
+
? guardedCorrectivePosture
|
|
3501
|
+
: null;
|
|
3502
|
+
if (!remediationModeRecovery && planRiskAudit && guardedCorrectivePosture === null) {
|
|
3503
|
+
try {
|
|
3504
|
+
catalogCorrectiveReplanPosture = identifyCorrectiveReplanPosture(task, planRiskAudit.reviewRequiredStepIds, readCorrectiveDecisionEvents(this.store, task), readRemediationEvents(this.store, task));
|
|
3505
|
+
}
|
|
3506
|
+
catch (error) {
|
|
3507
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
3508
|
+
if (!remediationModeRecovery)
|
|
3509
|
+
throw error;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
2450
3513
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2451
3514
|
const strictStepReviewStep = planRiskAudit
|
|
2452
3515
|
? task.steps.find((step) => {
|
|
@@ -2458,6 +3521,11 @@ export class WorkflowService {
|
|
|
2458
3521
|
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
2459
3522
|
}) ?? null
|
|
2460
3523
|
: null;
|
|
3524
|
+
const strictReviewInfrastructureFailures = strictStepReviewStep
|
|
3525
|
+
? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
|
|
3526
|
+
: 0;
|
|
3527
|
+
const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
|
|
3528
|
+
|| (task.status === 'blocked' && task.review.status === 'unverified');
|
|
2461
3529
|
const handoffOverlay = handoffPosture.state === 'pending'
|
|
2462
3530
|
? {
|
|
2463
3531
|
action: 'task claim',
|
|
@@ -2466,6 +3534,15 @@ export class WorkflowService {
|
|
|
2466
3534
|
state: 'pending',
|
|
2467
3535
|
targetActor: handoffPosture.targetActor,
|
|
2468
3536
|
eventId: handoffPosture.latestEvent.eventId,
|
|
3537
|
+
credentialRecovery: {
|
|
3538
|
+
action: 'task handoff-replace',
|
|
3539
|
+
when: 'claim credential is unavailable',
|
|
3540
|
+
requiredActor: handoffPosture.latestEvent.actor,
|
|
3541
|
+
targetActor: handoffPosture.targetActor,
|
|
3542
|
+
grantId: handoffPosture.latestEvent.grantId,
|
|
3543
|
+
expectedRevision: task.revision,
|
|
3544
|
+
invalidatesPriorCredential: true,
|
|
3545
|
+
},
|
|
2469
3546
|
},
|
|
2470
3547
|
}
|
|
2471
3548
|
: handoffPosture.state === 'claimed'
|
|
@@ -2488,6 +3565,63 @@ export class WorkflowService {
|
|
|
2488
3565
|
},
|
|
2489
3566
|
}
|
|
2490
3567
|
: {};
|
|
3568
|
+
const correctiveReplanOverlay = (posture) => {
|
|
3569
|
+
const evaluation = evaluateTaskCorrectiveReplanAvailability(task, posture);
|
|
3570
|
+
const writerLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
3571
|
+
const command = handoffPosture.state === 'claimed'
|
|
3572
|
+
? 'task corrective-yield'
|
|
3573
|
+
: writerLease
|
|
3574
|
+
? 'doctor'
|
|
3575
|
+
: 'task corrective-replan-prepare';
|
|
3576
|
+
if (evaluation.allowed && handoffPosture.state === 'none' && writerLease) {
|
|
3577
|
+
const navigation = correctiveReplanNavigation(evaluation);
|
|
3578
|
+
return {
|
|
3579
|
+
action: command,
|
|
3580
|
+
blockedAction: 'task corrective-replan-prepare',
|
|
3581
|
+
stepId: posture.stepId,
|
|
3582
|
+
navigation: {
|
|
3583
|
+
kind: 'blocked',
|
|
3584
|
+
transitionId: navigation.transitionId,
|
|
3585
|
+
semanticFingerprint: navigation.semanticFingerprint,
|
|
3586
|
+
command: navigation.command,
|
|
3587
|
+
blockingGuardIds: ['task.corrective-replan.no-writer-lease'],
|
|
3588
|
+
pendingInputGuardIds: navigation.pendingInputGuardIds,
|
|
3589
|
+
reasonCode: 'ACTIVE_WRITER_LEASE',
|
|
3590
|
+
},
|
|
3591
|
+
correctiveDecisionGate: {
|
|
3592
|
+
attemptCount: posture.attemptCount,
|
|
3593
|
+
decision: posture.correctiveDecision?.decision ?? null,
|
|
3594
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3595
|
+
requiredAction: 'The active non-C1 writer lease must expire and be explicitly repaired before corrective replan preparation.',
|
|
3596
|
+
writerLease: {
|
|
3597
|
+
owner: writerLease.owner,
|
|
3598
|
+
expiresAt: writerLease.expiresAt,
|
|
3599
|
+
},
|
|
3600
|
+
},
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
return evaluation.allowed
|
|
3604
|
+
? {
|
|
3605
|
+
action: command,
|
|
3606
|
+
blockedAction: 'task plan-set',
|
|
3607
|
+
stepId: posture.stepId,
|
|
3608
|
+
navigation: { ...correctiveReplanNavigation(evaluation), command },
|
|
3609
|
+
correctiveDecisionGate: {
|
|
3610
|
+
attemptCount: posture.attemptCount,
|
|
3611
|
+
decision: posture.correctiveDecision?.decision ?? null,
|
|
3612
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3613
|
+
requiredAction: handoffPosture.state === 'claimed'
|
|
3614
|
+
? 'Yield the exact C1 claim and writer lease before preparing replacement authority.'
|
|
3615
|
+
: 'Prepare a state-bound candidate, confirm the Human gate, then execute its exact receipt.',
|
|
3616
|
+
},
|
|
3617
|
+
}
|
|
3618
|
+
: {
|
|
3619
|
+
action: 'doctor',
|
|
3620
|
+
blockedAction: 'task plan-set',
|
|
3621
|
+
stepId: posture.stepId,
|
|
3622
|
+
navigation: correctiveReplanBlockedNavigation(evaluation),
|
|
3623
|
+
};
|
|
3624
|
+
};
|
|
2491
3625
|
let guardedCorrectiveOverlay = {};
|
|
2492
3626
|
if (remediationModeRecovery) {
|
|
2493
3627
|
guardedCorrectiveOverlay = {
|
|
@@ -2506,7 +3640,12 @@ export class WorkflowService {
|
|
|
2506
3640
|
},
|
|
2507
3641
|
};
|
|
2508
3642
|
}
|
|
2509
|
-
if (!remediationModeRecovery &&
|
|
3643
|
+
if (!remediationModeRecovery && guardedCorrectivePosture === null && catalogCorrectiveReplanPosture) {
|
|
3644
|
+
guardedCorrectiveOverlay = correctiveReplanOverlay(catalogCorrectiveReplanPosture);
|
|
3645
|
+
}
|
|
3646
|
+
if (!remediationModeRecovery
|
|
3647
|
+
&& guardedCorrectivePosture
|
|
3648
|
+
&& (!strictStepReviewStep || guardedCorrectivePosture.posture === 'replan-required')) {
|
|
2510
3649
|
switch (guardedCorrectivePosture.posture) {
|
|
2511
3650
|
case 'decision-required':
|
|
2512
3651
|
if (guardedCorrectivePosture.correctiveDecision
|
|
@@ -2581,18 +3720,59 @@ export class WorkflowService {
|
|
|
2581
3720
|
};
|
|
2582
3721
|
break;
|
|
2583
3722
|
case 'replan-required':
|
|
2584
|
-
guardedCorrectiveOverlay =
|
|
2585
|
-
action: 'task plan-set',
|
|
2586
|
-
stepId: guardedCorrectivePosture.stepId,
|
|
2587
|
-
correctiveDecisionGate: {
|
|
2588
|
-
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2589
|
-
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2590
|
-
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2591
|
-
requiredAction: 'Record an actual new Plan posture before another remediation run.',
|
|
2592
|
-
},
|
|
2593
|
-
};
|
|
3723
|
+
guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
|
|
2594
3724
|
break;
|
|
2595
3725
|
case 'split-required':
|
|
3726
|
+
{
|
|
3727
|
+
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
3728
|
+
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
3729
|
+
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
3730
|
+
const replacementMilestone = task.milestoneId
|
|
3731
|
+
? this.store.readMilestone(projectId, task.milestoneId)
|
|
3732
|
+
: null;
|
|
3733
|
+
guardedCorrectiveOverlay = {
|
|
3734
|
+
action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
|
|
3735
|
+
...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
|
|
3736
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
3737
|
+
correctiveDecisionGate: {
|
|
3738
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
3739
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
3740
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
3741
|
+
humanApprovalRequired: false,
|
|
3742
|
+
},
|
|
3743
|
+
replacementGate: {
|
|
3744
|
+
milestoneId: task.milestoneId,
|
|
3745
|
+
oldTaskId: task.id,
|
|
3746
|
+
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
3747
|
+
oldMembershipDisposition: 'cancelled',
|
|
3748
|
+
replacementMembershipDisposition: 'required',
|
|
3749
|
+
transaction: 'journal-recoverable',
|
|
3750
|
+
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
3751
|
+
id: discovery.id,
|
|
3752
|
+
revision: discovery.revision,
|
|
3753
|
+
goal: discovery.goal,
|
|
3754
|
+
})),
|
|
3755
|
+
...(replacementDiscovery && replacementMilestone
|
|
3756
|
+
? {
|
|
3757
|
+
exactTransition: {
|
|
3758
|
+
command: 'task replacement-materialize',
|
|
3759
|
+
taskId: task.id,
|
|
3760
|
+
expectedTaskRevision: task.revision,
|
|
3761
|
+
discoveryId: replacementDiscovery.id,
|
|
3762
|
+
expectedDiscoveryRevision: replacementDiscovery.revision,
|
|
3763
|
+
milestoneId: replacementMilestone.id,
|
|
3764
|
+
expectedMilestoneRevision: replacementMilestone.revision,
|
|
3765
|
+
},
|
|
3766
|
+
}
|
|
3767
|
+
: {
|
|
3768
|
+
requiredAction: replacementDiscoveries.length === 0
|
|
3769
|
+
? 'Create and complete one thin replacement Discovery, then run next again.'
|
|
3770
|
+
: 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
|
|
3771
|
+
}),
|
|
3772
|
+
},
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3775
|
+
break;
|
|
2596
3776
|
case 'stop-escalate':
|
|
2597
3777
|
guardedCorrectiveOverlay = {
|
|
2598
3778
|
action: 'doctor',
|
|
@@ -2624,6 +3804,30 @@ export class WorkflowService {
|
|
|
2624
3804
|
strictStepReview: {
|
|
2625
3805
|
reviewRequired: true,
|
|
2626
3806
|
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
3807
|
+
infrastructureFailures: strictReviewInfrastructureFailures,
|
|
3808
|
+
maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
|
|
3809
|
+
recommendedMode: 'external-sealed',
|
|
3810
|
+
externalSealedReview: {
|
|
3811
|
+
packetCommand: 'task step-review-packet',
|
|
3812
|
+
recordCommand: 'task step-review-record',
|
|
3813
|
+
reviewerMustUseSeparateChat: true,
|
|
3814
|
+
repositorySealRequired: true,
|
|
3815
|
+
},
|
|
3816
|
+
},
|
|
3817
|
+
}
|
|
3818
|
+
: {}),
|
|
3819
|
+
...(strictTaskReviewRequired
|
|
3820
|
+
? {
|
|
3821
|
+
strictTaskReview: {
|
|
3822
|
+
reviewRequired: true,
|
|
3823
|
+
localInfrastructureFailureObserved: task.review.status === 'unverified',
|
|
3824
|
+
recommendedMode: 'external-sealed',
|
|
3825
|
+
externalSealedReview: {
|
|
3826
|
+
packetCommand: 'task review-packet',
|
|
3827
|
+
recordCommand: 'task review-sealed-record',
|
|
3828
|
+
reviewerMustUseSeparateChat: true,
|
|
3829
|
+
repositorySealRequired: true,
|
|
3830
|
+
},
|
|
2627
3831
|
},
|
|
2628
3832
|
}
|
|
2629
3833
|
: {}),
|
|
@@ -2653,13 +3857,65 @@ export class WorkflowService {
|
|
|
2653
3857
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2654
3858
|
};
|
|
2655
3859
|
if (handoffPosture.state === 'pending') {
|
|
2656
|
-
return {
|
|
3860
|
+
return this.withTaskNavigationContracts(task, {
|
|
2657
3861
|
...result,
|
|
2658
3862
|
action: 'task claim',
|
|
2659
3863
|
blockedAction: next.action,
|
|
2660
|
-
};
|
|
3864
|
+
});
|
|
2661
3865
|
}
|
|
2662
|
-
|
|
3866
|
+
const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
|
|
3867
|
+
.includes(String(result.action))
|
|
3868
|
+
? {
|
|
3869
|
+
...result,
|
|
3870
|
+
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
3871
|
+
}
|
|
3872
|
+
: result;
|
|
3873
|
+
return this.withTaskNavigationContracts(task, navigableResult);
|
|
3874
|
+
}
|
|
3875
|
+
taskPlanContract(projectId, task) {
|
|
3876
|
+
const map = this.store.readKnowledgeMap(projectId);
|
|
3877
|
+
return {
|
|
3878
|
+
requiredRequirementIds: [...task.requirements],
|
|
3879
|
+
requiredAcceptanceIds: [...task.acceptance],
|
|
3880
|
+
currentRisks: [...task.risks],
|
|
3881
|
+
exactInclusionRequired: true,
|
|
3882
|
+
briefHash: task.briefHash,
|
|
3883
|
+
knowledgeMap: knowledgeSummary(map),
|
|
3884
|
+
planRiskAuditContract: {
|
|
3885
|
+
required: true,
|
|
3886
|
+
option: '--risk-audit-file',
|
|
3887
|
+
atomicWithPlanSet: true,
|
|
3888
|
+
plannerAuditorSeparationRequired: true,
|
|
3889
|
+
},
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
assertTaskContextChangeAllowed(task) {
|
|
3893
|
+
const posture = this.taskTerminalCorrectivePosture(task);
|
|
3894
|
+
if (!posture)
|
|
3895
|
+
return;
|
|
3896
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
|
|
3897
|
+
taskId: task.id,
|
|
3898
|
+
stepId: posture.stepId,
|
|
3899
|
+
decision: posture.posture,
|
|
3900
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3901
|
+
requiredAction: posture.posture === 'split-required'
|
|
3902
|
+
? 'Materialize an atomic replacement Task through the exact replacement route.'
|
|
3903
|
+
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
taskHasTerminalCorrectiveDecision(task) {
|
|
3907
|
+
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
3908
|
+
}
|
|
3909
|
+
taskTerminalCorrectivePosture(task) {
|
|
3910
|
+
if (!task.planHash)
|
|
3911
|
+
return null;
|
|
3912
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
3913
|
+
if (!planRiskAudit)
|
|
3914
|
+
return null;
|
|
3915
|
+
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
3916
|
+
return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
|
|
3917
|
+
? posture
|
|
3918
|
+
: null;
|
|
2663
3919
|
}
|
|
2664
3920
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
2665
3921
|
return this.store.listDelegations(projectId)
|
|
@@ -2710,6 +3966,22 @@ export class WorkflowService {
|
|
|
2710
3966
|
policyHash: grant.policyHash,
|
|
2711
3967
|
}));
|
|
2712
3968
|
}
|
|
3969
|
+
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
3970
|
+
for (const task of tasks) {
|
|
3971
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById)
|
|
3972
|
+
|| !task.planHash
|
|
3973
|
+
|| !taskCanRebindKnowledge(task)
|
|
3974
|
+
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
3975
|
+
continue;
|
|
3976
|
+
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
3977
|
+
if (!assessment.eligible)
|
|
3978
|
+
continue;
|
|
3979
|
+
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
3980
|
+
if (options.length > 0)
|
|
3981
|
+
return { task, assessment, options };
|
|
3982
|
+
}
|
|
3983
|
+
return null;
|
|
3984
|
+
}
|
|
2713
3985
|
assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
|
|
2714
3986
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
2715
3987
|
if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
@@ -2779,6 +4051,130 @@ export class WorkflowService {
|
|
|
2779
4051
|
? milestone
|
|
2780
4052
|
: readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
2781
4053
|
}
|
|
4054
|
+
listMilestonesObserved(projectId, milestones, posture) {
|
|
4055
|
+
if (!posture)
|
|
4056
|
+
return milestones;
|
|
4057
|
+
const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
|
|
4058
|
+
return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
|
|
4059
|
+
? milestone
|
|
4060
|
+
: inspectMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
4061
|
+
}
|
|
4062
|
+
taskGitNavigationOverride(task) {
|
|
4063
|
+
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
|
|
4064
|
+
return null;
|
|
4065
|
+
}
|
|
4066
|
+
const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4067
|
+
if (currentBase === task.baseCommit)
|
|
4068
|
+
return null;
|
|
4069
|
+
return {
|
|
4070
|
+
scope: 'task',
|
|
4071
|
+
id: task.id,
|
|
4072
|
+
status: task.status,
|
|
4073
|
+
action: 'task sync-base',
|
|
4074
|
+
revision: task.revision,
|
|
4075
|
+
baseSyncRequired: {
|
|
4076
|
+
baseBranch: task.baseBranch,
|
|
4077
|
+
recordedBaseCommit: task.baseCommit,
|
|
4078
|
+
currentBaseCommit: currentBase,
|
|
4079
|
+
},
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
inspectDependencyProvenanceCandidate(snapshot, task) {
|
|
4083
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4084
|
+
const now = this.now().getTime();
|
|
4085
|
+
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
4086
|
+
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
4087
|
+
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
4088
|
+
pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
|
|
4089
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4090
|
+
+ snapshot.observation.pendingTaskTransactions.length,
|
|
4091
|
+
corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
|
|
4092
|
+
coreOperationCount: snapshot.observation.coreTaskOperations.length,
|
|
4093
|
+
verifiedHistoricalStepCommits: verifiedHistoricalStepReviewCommits(this.store, task),
|
|
4094
|
+
versions: {
|
|
4095
|
+
runtime: versions.runtime,
|
|
4096
|
+
declared: versions.declared,
|
|
4097
|
+
locked: versions.locked,
|
|
4098
|
+
installed: versions.installed,
|
|
4099
|
+
currentBranch: versions.currentBranch,
|
|
4100
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4101
|
+
},
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
withTaskNavigationContracts(task, next) {
|
|
4105
|
+
const action = typeof next.action === 'string' ? next.action : null;
|
|
4106
|
+
if (!action)
|
|
4107
|
+
return next;
|
|
4108
|
+
const policy = gitMutationPolicyForAction(task, action);
|
|
4109
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
4110
|
+
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
4111
|
+
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
|
|
4112
|
+
const activeLease = writerTokenContract
|
|
4113
|
+
? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
4114
|
+
: null;
|
|
4115
|
+
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
4116
|
+
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
4117
|
+
? {
|
|
4118
|
+
action: 'task writer-credential-replace',
|
|
4119
|
+
when: 'the active writer credential is unavailable',
|
|
4120
|
+
requiredActor: writerCredentialRecoveryActor,
|
|
4121
|
+
expectedRevision: task.revision,
|
|
4122
|
+
invalidatesPriorCredential: true,
|
|
4123
|
+
acquiresLeaseWhenAbsent: true,
|
|
4124
|
+
}
|
|
4125
|
+
: null;
|
|
4126
|
+
return {
|
|
4127
|
+
...next,
|
|
4128
|
+
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
4129
|
+
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
4130
|
+
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
|
|
4134
|
+
if (claimedActor)
|
|
4135
|
+
return claimedActor;
|
|
4136
|
+
if (activeLeaseOwner)
|
|
4137
|
+
return activeLeaseOwner;
|
|
4138
|
+
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
4139
|
+
? 'final_acceptance'
|
|
4140
|
+
: ['task step-complete', 'task submit'].includes(action)
|
|
4141
|
+
? 'execution'
|
|
4142
|
+
: null;
|
|
4143
|
+
if (!authorizationKind)
|
|
4144
|
+
return null;
|
|
4145
|
+
return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
|
|
4146
|
+
}
|
|
4147
|
+
describeMilestoneInitialAssembly(projectId, milestone, tasks) {
|
|
4148
|
+
const assessment = assessMilestoneInitialAssembly({
|
|
4149
|
+
store: this.store,
|
|
4150
|
+
projectId,
|
|
4151
|
+
milestone,
|
|
4152
|
+
listTasks: () => tasks,
|
|
4153
|
+
});
|
|
4154
|
+
if (!assessment.eligible)
|
|
4155
|
+
return null;
|
|
4156
|
+
const options = [
|
|
4157
|
+
{
|
|
4158
|
+
action: 'linked-task-assembly',
|
|
4159
|
+
commands: ['discovery start', 'discovery materialize'],
|
|
4160
|
+
description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
|
|
4161
|
+
},
|
|
4162
|
+
];
|
|
4163
|
+
if (assessment.orphanTaskIds.length > 0) {
|
|
4164
|
+
options.push({
|
|
4165
|
+
action: 'milestone plan-set',
|
|
4166
|
+
commands: ['milestone plan-set'],
|
|
4167
|
+
description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return {
|
|
4171
|
+
milestoneId: milestone.id,
|
|
4172
|
+
revision: milestone.revision,
|
|
4173
|
+
linkedTaskIds: assessment.orphanTaskIds,
|
|
4174
|
+
executionAvailable: false,
|
|
4175
|
+
options,
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
2782
4178
|
prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
|
|
2783
4179
|
validateMilestonePlan(plan);
|
|
2784
4180
|
return prepareMilestoneScopeChangeCandidate({
|
|
@@ -2788,6 +4184,12 @@ export class WorkflowService {
|
|
|
2788
4184
|
actor,
|
|
2789
4185
|
});
|
|
2790
4186
|
}
|
|
4187
|
+
publicWriterLeaseDiagnostic(lease) {
|
|
4188
|
+
return {
|
|
4189
|
+
...toPublicWriterLease(lease, this.now()),
|
|
4190
|
+
tokenFingerprint: createHash('sha256').update(lease.token).digest('hex').slice(0, 16),
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
2791
4193
|
proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
|
|
2792
4194
|
const currentLease = context.locks.inspect(taskId);
|
|
2793
4195
|
if (!currentLease) {
|
|
@@ -2877,9 +4279,12 @@ export class WorkflowService {
|
|
|
2877
4279
|
storedKnowledgeMapHash: storedHash,
|
|
2878
4280
|
});
|
|
2879
4281
|
}
|
|
2880
|
-
inspectAdoptionPreparation(identity, tasks, milestones) {
|
|
2881
|
-
const
|
|
2882
|
-
|
|
4282
|
+
inspectAdoptionPreparation(identity, tasks, milestones, observedLeases, observedPosture) {
|
|
4283
|
+
const leases = observedLeases
|
|
4284
|
+
?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
|
|
4285
|
+
const currentPosture = observedPosture === undefined
|
|
4286
|
+
? readCurrentAdoptionPosture(this.store, identity.projectId)
|
|
4287
|
+
: observedPosture;
|
|
2883
4288
|
if (currentPosture) {
|
|
2884
4289
|
assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
|
|
2885
4290
|
}
|
|
@@ -2892,7 +4297,7 @@ export class WorkflowService {
|
|
|
2892
4297
|
tasks,
|
|
2893
4298
|
milestones,
|
|
2894
4299
|
sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
|
|
2895
|
-
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks,
|
|
4300
|
+
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, leases),
|
|
2896
4301
|
now: this.now(),
|
|
2897
4302
|
}, currentPosture);
|
|
2898
4303
|
}
|
|
@@ -3434,13 +4839,13 @@ function nextForTask(task) {
|
|
|
3434
4839
|
awaiting_execution_authorization: 'task authorize',
|
|
3435
4840
|
ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
|
|
3436
4841
|
in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
|
|
3437
|
-
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-
|
|
4842
|
+
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
|
|
3438
4843
|
needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
|
|
3439
4844
|
awaiting_final_acceptance: 'task accept',
|
|
3440
4845
|
merging: 'task merge',
|
|
3441
4846
|
ready_to_merge: 'task merge-confirm',
|
|
3442
4847
|
merged: 'none',
|
|
3443
|
-
blocked: task.review.status === 'unverified' ? 'task review-
|
|
4848
|
+
blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
|
|
3444
4849
|
cancelled: 'none',
|
|
3445
4850
|
};
|
|
3446
4851
|
const selectedStep = activeStep ?? failedStep ?? runnableStep;
|
|
@@ -3462,6 +4867,95 @@ function nextForTask(task) {
|
|
|
3462
4867
|
: {}),
|
|
3463
4868
|
};
|
|
3464
4869
|
}
|
|
4870
|
+
function gitMutationPolicyForAction(task, action) {
|
|
4871
|
+
const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
|
|
4872
|
+
switch (action) {
|
|
4873
|
+
case 'task start':
|
|
4874
|
+
return {
|
|
4875
|
+
permission: 'git-metadata-write',
|
|
4876
|
+
requiredBeforeFirstCall: true,
|
|
4877
|
+
boundedToTransition: true,
|
|
4878
|
+
transition: 'task start',
|
|
4879
|
+
appliesWhen: { workspaceOwner: 'local' },
|
|
4880
|
+
metadataWrites,
|
|
4881
|
+
};
|
|
4882
|
+
case 'task step-complete':
|
|
4883
|
+
return {
|
|
4884
|
+
permission: 'git-metadata-write',
|
|
4885
|
+
requiredBeforeFirstCall: true,
|
|
4886
|
+
boundedToTransition: true,
|
|
4887
|
+
transition: 'task step-complete',
|
|
4888
|
+
metadataWrites,
|
|
4889
|
+
};
|
|
4890
|
+
case 'task merge':
|
|
4891
|
+
if (task.workspaceOwner !== 'local')
|
|
4892
|
+
return null;
|
|
4893
|
+
return {
|
|
4894
|
+
permission: 'git-metadata-write',
|
|
4895
|
+
requiredBeforeFirstCall: true,
|
|
4896
|
+
boundedToTransition: true,
|
|
4897
|
+
transition: 'task merge',
|
|
4898
|
+
metadataWrites,
|
|
4899
|
+
};
|
|
4900
|
+
case 'task sync-base':
|
|
4901
|
+
return {
|
|
4902
|
+
permission: 'git-metadata-write',
|
|
4903
|
+
requiredBeforeFirstCall: true,
|
|
4904
|
+
boundedToTransition: true,
|
|
4905
|
+
transition: 'task sync-base',
|
|
4906
|
+
metadataWrites,
|
|
4907
|
+
};
|
|
4908
|
+
default:
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
4913
|
+
const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
|
|
4914
|
+
const claimedC1LeaseBound = [
|
|
4915
|
+
'task run',
|
|
4916
|
+
'task step-review',
|
|
4917
|
+
'task step-review-record',
|
|
4918
|
+
'task review-record',
|
|
4919
|
+
'task review-launch',
|
|
4920
|
+
'task review-sealed-record',
|
|
4921
|
+
'task corrective-decision-recover',
|
|
4922
|
+
'task remediation-mode-recover',
|
|
4923
|
+
'task corrective-decision',
|
|
4924
|
+
'task corrective-yield',
|
|
4925
|
+
'task handoff',
|
|
4926
|
+
'task handoff-prepare',
|
|
4927
|
+
'task handback',
|
|
4928
|
+
'task handback-create',
|
|
4929
|
+
];
|
|
4930
|
+
if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
|
|
4931
|
+
return null;
|
|
4932
|
+
}
|
|
4933
|
+
return {
|
|
4934
|
+
required: true,
|
|
4935
|
+
option: '--writer-token',
|
|
4936
|
+
source: 'active-writer-lease',
|
|
4937
|
+
sensitive: true,
|
|
4938
|
+
renewalDisposition: 'reuse-presented-token',
|
|
4939
|
+
tokenReissuedOnRenewal: false,
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
function observationPreflightBlockers(observation) {
|
|
4943
|
+
const messages = {
|
|
4944
|
+
PROJECT_REGISTRATION_MISSING: 'Workflow project registration is missing.',
|
|
4945
|
+
PROJECT_REPOSITORY_ROOT_UNBOUND: 'The current repository root is not bound to the Workflow project.',
|
|
4946
|
+
PROJECT_RECOVERY_REQUIRED: 'A project transaction requires explicit recovery.',
|
|
4947
|
+
MILESTONE_RECOVERY_REQUIRED: 'A Milestone transaction requires explicit recovery.',
|
|
4948
|
+
TASK_RECOVERY_REQUIRED: 'A Task corrective-replan transaction requires explicit recovery.',
|
|
4949
|
+
TASK_TRANSACTION_INTEGRITY_FAILED: 'A Task transaction journal failed integrity validation.',
|
|
4950
|
+
TASK_OPERATION_IN_PROGRESS: 'A Core Task operation is in progress.',
|
|
4951
|
+
STALE_TASK_OPERATION: 'A stale Core Task operation requires bounded repair.',
|
|
4952
|
+
ADOPTION_MISSING: 'Workflow lifecycle adoption is missing.',
|
|
4953
|
+
};
|
|
4954
|
+
return observation.reasonCodes.flatMap((reasonCode) => {
|
|
4955
|
+
const message = messages[reasonCode];
|
|
4956
|
+
return message ? [message] : [];
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
3465
4959
|
function knowledgeSummary(map) {
|
|
3466
4960
|
return {
|
|
3467
4961
|
revision: map.revision,
|