codex-workflow-v2 2.0.0-beta.4 → 2.0.0-beta.6
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 +1 -1
- package/dist/src/alpha6/handoff.d.ts +2 -0
- package/dist/src/alpha6/handoff.js +124 -12
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +9 -7
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +1 -0
- package/dist/src/alpha6/review.js +9 -0
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +72 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/lifecycle/corrective-replan-credential-core.js +31 -2
- package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
- package/dist/src/reviewer.d.ts +39 -1
- package/dist/src/reviewer.js +179 -43
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.js +15 -5
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +53 -6
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +2 -0
- package/dist/src/state/corrective-replan-transaction.js +11 -4
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +29 -0
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +28 -3
- package/dist/src/workflow.js +443 -43
- package/dist/src/workflow.js.map +1 -1
- package/docs/release.md +3 -1
- package/docs/stable-release-defect-register.md +53 -2
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +36 -0
- package/schemas/task-handoff-event.schema.json +1 -1
package/dist/src/workflow.js
CHANGED
|
@@ -9,20 +9,21 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
|
9
9
|
import { WorkflowError } from './errors.js';
|
|
10
10
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
11
11
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
12
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
12
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
13
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
14
14
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
15
15
|
import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
16
|
import { assessMilestoneInitialAssembly, applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
17
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
18
|
-
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
18
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
19
19
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
20
20
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
21
21
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
22
22
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
23
|
+
import { ensureDirectory } from './fs-utils.js';
|
|
23
24
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
24
25
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
25
|
-
import { launchStrictStepReviewer } from './reviewer.js';
|
|
26
|
+
import { launchStrictStepReviewer, prepareExternalStrictStepReviewPacket, prepareExternalStrictTaskReviewPacket, strictReviewInputHash, validateExternalStrictStepReviewInput, validateExternalStrictTaskReviewInput, } from './reviewer.js';
|
|
26
27
|
import { listCoreTaskOperationMutexes, inspectWriterLease as inspectRawWriterLease, toPublicWriterLease, WriterLockManager, } from './state/lock.js';
|
|
27
28
|
import { FileStateStore } from './state/store.js';
|
|
28
29
|
import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
|
|
@@ -33,6 +34,7 @@ import { inspectWorkflowObservationBoundary, } from './observation.js';
|
|
|
33
34
|
import { createEntityId } from './ulid.js';
|
|
34
35
|
import { PACKAGE_VERSION } from './version.js';
|
|
35
36
|
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
37
|
+
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
36
38
|
export class WorkflowService {
|
|
37
39
|
store;
|
|
38
40
|
now;
|
|
@@ -71,6 +73,11 @@ export class WorkflowService {
|
|
|
71
73
|
const projectFile = path.join(projectRoot, 'project.json');
|
|
72
74
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
73
75
|
if (!projectAlreadyRegistered) {
|
|
76
|
+
// The registration posture is a sidecar under the project root. A completely
|
|
77
|
+
// fresh CODEX_WORKFLOW_STATE_HOME has no project directory yet, so establish
|
|
78
|
+
// only that directory before writing the posture; project.json remains the
|
|
79
|
+
// final registration record written below.
|
|
80
|
+
ensureDirectory(projectRoot);
|
|
74
81
|
initializeProjectRegistrationAdoption(this.store, {
|
|
75
82
|
projectId: identity.projectId,
|
|
76
83
|
repositoryIdentity: identity,
|
|
@@ -783,6 +790,7 @@ export class WorkflowService {
|
|
|
783
790
|
const { identity } = this.#mutationContext(repository);
|
|
784
791
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
785
792
|
assertExpectedRevision(milestone, expectedRevision);
|
|
793
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
786
794
|
return prepareMilestoneAutonomyContract({
|
|
787
795
|
projectId: identity.projectId,
|
|
788
796
|
milestone,
|
|
@@ -796,6 +804,7 @@ export class WorkflowService {
|
|
|
796
804
|
const { identity } = this.#mutationContext(repository);
|
|
797
805
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
798
806
|
assertExpectedRevision(milestone, expectedRevision);
|
|
807
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
799
808
|
return applyMilestoneAutonomyContract({
|
|
800
809
|
store: this.store,
|
|
801
810
|
projectId: identity.projectId,
|
|
@@ -844,6 +853,7 @@ export class WorkflowService {
|
|
|
844
853
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
845
854
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
846
855
|
}
|
|
856
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
847
857
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
848
858
|
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
849
859
|
if (planHash !== milestone.planHash) {
|
|
@@ -969,7 +979,7 @@ export class WorkflowService {
|
|
|
969
979
|
resultHash,
|
|
970
980
|
evidenceHash,
|
|
971
981
|
headCommit: currentHead,
|
|
972
|
-
reason:
|
|
982
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
973
983
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
974
984
|
...(delegation ? { delegation } : {}),
|
|
975
985
|
};
|
|
@@ -1416,7 +1426,7 @@ export class WorkflowService {
|
|
|
1416
1426
|
claimTokenAvailable: false,
|
|
1417
1427
|
};
|
|
1418
1428
|
}
|
|
1419
|
-
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
1429
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
1420
1430
|
const context = this.#mutationContext(repository);
|
|
1421
1431
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1422
1432
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1475,6 +1485,7 @@ export class WorkflowService {
|
|
|
1475
1485
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1476
1486
|
grantId,
|
|
1477
1487
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
1488
|
+
replacePending,
|
|
1478
1489
|
}, this.now());
|
|
1479
1490
|
releaseLease();
|
|
1480
1491
|
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
@@ -1542,6 +1553,43 @@ export class WorkflowService {
|
|
|
1542
1553
|
throw error;
|
|
1543
1554
|
}
|
|
1544
1555
|
}
|
|
1556
|
+
replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
|
|
1557
|
+
const context = this.#mutationContext(repository);
|
|
1558
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1559
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1560
|
+
const normalizedActor = actor.trim();
|
|
1561
|
+
if (!normalizedActor)
|
|
1562
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1563
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1564
|
+
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1565
|
+
const action = String(navigation.action);
|
|
1566
|
+
const currentLease = context.locks.inspect(taskId);
|
|
1567
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
1568
|
+
const requiredActor = writerTokenContract
|
|
1569
|
+
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1570
|
+
: null;
|
|
1571
|
+
if (!requiredActor || requiredActor !== normalizedActor) {
|
|
1572
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
|
|
1573
|
+
taskId,
|
|
1574
|
+
posture: posture.state,
|
|
1575
|
+
claimant: posture.state === 'claimed' ? posture.claimant : null,
|
|
1576
|
+
actor: normalizedActor,
|
|
1577
|
+
requiredActor,
|
|
1578
|
+
nextAction: action,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
const lease = currentLease
|
|
1582
|
+
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1583
|
+
: context.locks.acquire(taskId, normalizedActor);
|
|
1584
|
+
if (posture.state === 'claimed') {
|
|
1585
|
+
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
task,
|
|
1589
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1590
|
+
writerCredential: { disposition: 'issued', token: lease.token },
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1545
1593
|
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1546
1594
|
const context = this.#mutationContext(repository);
|
|
1547
1595
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1636,6 +1684,7 @@ export class WorkflowService {
|
|
|
1636
1684
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
1637
1685
|
}
|
|
1638
1686
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1687
|
+
let resumesCorrectiveReplan = false;
|
|
1639
1688
|
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1640
1689
|
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.`, {
|
|
1641
1690
|
taskId,
|
|
@@ -1650,20 +1699,21 @@ export class WorkflowService {
|
|
|
1650
1699
|
&& guardedPosture.correctiveDecision.planHash !== task.planHash
|
|
1651
1700
|
? guardedPosture.correctiveDecision
|
|
1652
1701
|
: null;
|
|
1653
|
-
const recovery = staleDecision
|
|
1702
|
+
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1654
1703
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1655
1704
|
: null;
|
|
1656
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1705
|
+
const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1706
|
+
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1657
1707
|
}
|
|
1658
1708
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1659
1709
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1660
|
-
if (step.status === 'planned' && dirty.length > 0) {
|
|
1710
|
+
if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
|
|
1661
1711
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1662
1712
|
changedFiles: dirty,
|
|
1663
1713
|
});
|
|
1664
1714
|
}
|
|
1665
1715
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
1666
|
-
if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
|
|
1716
|
+
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
1667
1717
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
1668
1718
|
changedFiles: dirtyOutsideStep,
|
|
1669
1719
|
});
|
|
@@ -1986,10 +2036,18 @@ export class WorkflowService {
|
|
|
1986
2036
|
resolution: currentCycle.resolution,
|
|
1987
2037
|
});
|
|
1988
2038
|
}
|
|
1989
|
-
|
|
2039
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2040
|
+
if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2041
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
|
|
2042
|
+
taskId: task.id,
|
|
2043
|
+
stepId,
|
|
2044
|
+
infrastructureFailures,
|
|
2045
|
+
requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
1990
2048
|
}
|
|
1991
2049
|
else if (currentCycle.resolution !== 'pending') {
|
|
1992
|
-
if (currentCycle.resolution === 'failed'
|
|
2050
|
+
if (currentCycle.resolution === 'failed') {
|
|
1993
2051
|
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1994
2052
|
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1995
2053
|
taskId: task.id,
|
|
@@ -1997,7 +2055,7 @@ export class WorkflowService {
|
|
|
1997
2055
|
resolution: currentCycle.resolution,
|
|
1998
2056
|
});
|
|
1999
2057
|
}
|
|
2000
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2058
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2001
2059
|
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
2002
2060
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2003
2061
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
@@ -2015,7 +2073,7 @@ export class WorkflowService {
|
|
|
2015
2073
|
reviewedCommit: launch.reviewedCommit,
|
|
2016
2074
|
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
2017
2075
|
}, this.now());
|
|
2018
|
-
if (recorded.resolution === 'failed'
|
|
2076
|
+
if (recorded.resolution === 'failed') {
|
|
2019
2077
|
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
2020
2078
|
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
2021
2079
|
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
@@ -2025,7 +2083,7 @@ export class WorkflowService {
|
|
|
2025
2083
|
resolution: recorded.resolution,
|
|
2026
2084
|
});
|
|
2027
2085
|
}
|
|
2028
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2086
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2029
2087
|
completionCommit: reviewRecordedEvent.completionCommit,
|
|
2030
2088
|
checksHash: reviewRecordedEvent.checksHash,
|
|
2031
2089
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
@@ -2034,6 +2092,130 @@ export class WorkflowService {
|
|
|
2034
2092
|
}
|
|
2035
2093
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2036
2094
|
}
|
|
2095
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2096
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2097
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2098
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2099
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2100
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2101
|
+
}
|
|
2102
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2103
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2104
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
|
|
2105
|
+
}
|
|
2106
|
+
const step = requireStep(task, stepId);
|
|
2107
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
2108
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
|
|
2109
|
+
}
|
|
2110
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2111
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
|
|
2112
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2113
|
+
}
|
|
2114
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2115
|
+
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
|
|
2116
|
+
}
|
|
2117
|
+
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2118
|
+
const context = this.#mutationContext(repository);
|
|
2119
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2120
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2121
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2122
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2123
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
|
|
2124
|
+
let review;
|
|
2125
|
+
try {
|
|
2126
|
+
review = validateExternalStrictStepReviewInput(input, packet);
|
|
2127
|
+
}
|
|
2128
|
+
catch (error) {
|
|
2129
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
|
|
2130
|
+
}
|
|
2131
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2132
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
|
|
2133
|
+
}
|
|
2134
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
2135
|
+
if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
|
|
2136
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
|
|
2137
|
+
taskId,
|
|
2138
|
+
stepId,
|
|
2139
|
+
resolution: currentCycle.resolution,
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2143
|
+
if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2144
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
|
|
2145
|
+
taskId,
|
|
2146
|
+
stepId,
|
|
2147
|
+
infrastructureFailures,
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2151
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2152
|
+
review,
|
|
2153
|
+
reviewerOutputHash: strictReviewInputHash(review),
|
|
2154
|
+
reviewedCommit: packet.reviewedCommit,
|
|
2155
|
+
isolationResult: 'verified',
|
|
2156
|
+
}, this.now());
|
|
2157
|
+
if (recorded.resolution === 'failed') {
|
|
2158
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2159
|
+
completionCommit: recorded.reviewRecordedEvent.completionCommit,
|
|
2160
|
+
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2161
|
+
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2162
|
+
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2163
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2164
|
+
}
|
|
2165
|
+
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2166
|
+
return {
|
|
2167
|
+
mode: 'external-sealed',
|
|
2168
|
+
packetHash: packet.packetHash,
|
|
2169
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2170
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2171
|
+
task: saved,
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision) {
|
|
2175
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2176
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2177
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2178
|
+
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
2179
|
+
if (task.status !== 'validating' && !retryingUnverified) {
|
|
2180
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2181
|
+
}
|
|
2182
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2183
|
+
assertTaskBranch(identity.repositoryRoot, task);
|
|
2184
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
2185
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
|
|
2186
|
+
}
|
|
2187
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2188
|
+
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2189
|
+
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2190
|
+
}
|
|
2191
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
2192
|
+
}
|
|
2193
|
+
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2194
|
+
const context = this.#mutationContext(repository);
|
|
2195
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2196
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2197
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2198
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2199
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
|
|
2200
|
+
let review;
|
|
2201
|
+
try {
|
|
2202
|
+
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
2203
|
+
}
|
|
2204
|
+
catch (error) {
|
|
2205
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
|
|
2206
|
+
}
|
|
2207
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2208
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
|
|
2209
|
+
}
|
|
2210
|
+
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2211
|
+
return {
|
|
2212
|
+
mode: 'external-sealed',
|
|
2213
|
+
packetHash: packet.packetHash,
|
|
2214
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2215
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2216
|
+
task: saved,
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2037
2219
|
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
2038
2220
|
const context = this.#mutationContext(repository);
|
|
2039
2221
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -2586,21 +2768,13 @@ export class WorkflowService {
|
|
|
2586
2768
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2587
2769
|
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2588
2770
|
const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
|
|
2597
|
-
if (assessment.eligible
|
|
2598
|
-
&& this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
|
|
2599
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2600
|
-
taskId: activeTask.id,
|
|
2601
|
-
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2602
|
-
});
|
|
2603
|
-
}
|
|
2771
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
2772
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
|
|
2773
|
+
if (refreshOwner) {
|
|
2774
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2775
|
+
taskId: refreshOwner.task.id,
|
|
2776
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2777
|
+
});
|
|
2604
2778
|
}
|
|
2605
2779
|
}
|
|
2606
2780
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
@@ -2629,7 +2803,7 @@ export class WorkflowService {
|
|
|
2629
2803
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2630
2804
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
2631
2805
|
}
|
|
2632
|
-
next(repository) {
|
|
2806
|
+
next(repository, taskId = null) {
|
|
2633
2807
|
const status = this.status(repository);
|
|
2634
2808
|
if (status.observation.kind === 'blocked') {
|
|
2635
2809
|
return {
|
|
@@ -2742,15 +2916,77 @@ export class WorkflowService {
|
|
|
2742
2916
|
observation: status.observation,
|
|
2743
2917
|
});
|
|
2744
2918
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
2919
|
+
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
2745
2920
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
2746
|
-
const
|
|
2921
|
+
const requestedTask = taskId === null
|
|
2922
|
+
? null
|
|
2923
|
+
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
2924
|
+
if (taskId !== null && !requestedTask) {
|
|
2925
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2926
|
+
}
|
|
2927
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
|
|
2928
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
2929
|
+
taskId: requestedTask.id,
|
|
2930
|
+
taskStatus: requestedTask.status,
|
|
2931
|
+
milestoneId: requestedTask.milestoneId,
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
const activeTask = requestedTask
|
|
2935
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2747
2936
|
if (activeTask) {
|
|
2748
|
-
const
|
|
2749
|
-
?
|
|
2937
|
+
const activeTaskMilestone = activeTask.milestoneId
|
|
2938
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
2750
2939
|
: null;
|
|
2940
|
+
const milestoneCancellationSafe = Boolean(activeTaskMilestone
|
|
2941
|
+
&& !activeTask.steps.some((step) => step.status === 'in_progress')
|
|
2942
|
+
&& status.observation.leases.length === 0
|
|
2943
|
+
&& status.observation.coreTaskOperations.length === 0
|
|
2944
|
+
&& status.observation.pendingProjectTransactions.length === 0
|
|
2945
|
+
&& status.observation.pendingMilestoneTransactions.length === 0
|
|
2946
|
+
&& status.observation.pendingTaskTransactions.length === 0
|
|
2947
|
+
&& status.observation.corruptTaskTransactions.length === 0);
|
|
2948
|
+
const withMilestoneCancellation = (value) => (milestoneCancellationSafe
|
|
2949
|
+
? {
|
|
2950
|
+
...value,
|
|
2951
|
+
milestoneCancellationOption: {
|
|
2952
|
+
command: 'milestone cancel',
|
|
2953
|
+
milestoneId: activeTaskMilestone.id,
|
|
2954
|
+
expectedRevision: activeTaskMilestone.revision,
|
|
2955
|
+
reasonRequired: true,
|
|
2956
|
+
taskDisposition: 'retained-historical',
|
|
2957
|
+
},
|
|
2958
|
+
}
|
|
2959
|
+
: value);
|
|
2960
|
+
let activePlanRiskAudit = null;
|
|
2961
|
+
let planRiskAuditRecovery = null;
|
|
2962
|
+
if (activeTask.planHash) {
|
|
2963
|
+
try {
|
|
2964
|
+
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
2965
|
+
}
|
|
2966
|
+
catch (error) {
|
|
2967
|
+
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
2968
|
+
throw error;
|
|
2969
|
+
planRiskAuditRecovery = {
|
|
2970
|
+
scope: 'task',
|
|
2971
|
+
id: activeTask.id,
|
|
2972
|
+
status: activeTask.status,
|
|
2973
|
+
action: 'task plan-risk-audit',
|
|
2974
|
+
revision: activeTask.revision,
|
|
2975
|
+
planRiskAuditRecovery: {
|
|
2976
|
+
state: 'required',
|
|
2977
|
+
reason: error.message,
|
|
2978
|
+
currentBriefHash: activeTask.briefHash,
|
|
2979
|
+
currentPlanHash: activeTask.planHash,
|
|
2980
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
2981
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
2982
|
+
},
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2751
2986
|
const taskNext = activeTask.status === 'planning'
|
|
2752
2987
|
? null
|
|
2753
|
-
:
|
|
2988
|
+
: planRiskAuditRecovery
|
|
2989
|
+
?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2754
2990
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
2755
2991
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2756
2992
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -2762,12 +2998,27 @@ export class WorkflowService {
|
|
|
2762
2998
|
|| hardStop
|
|
2763
2999
|
|| terminalCorrectiveDecision === 'split-required'
|
|
2764
3000
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2765
|
-
return withAdoption(taskNext);
|
|
3001
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
2766
3002
|
}
|
|
2767
3003
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
2768
|
-
return withAdoption(taskNext);
|
|
3004
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
3005
|
+
}
|
|
3006
|
+
let knowledgeMap;
|
|
3007
|
+
try {
|
|
3008
|
+
knowledgeMap = inspectCurrentKnowledgeMap();
|
|
3009
|
+
}
|
|
3010
|
+
catch (error) {
|
|
3011
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3012
|
+
throw error;
|
|
3013
|
+
return withAdoption(withMilestoneCancellation({
|
|
3014
|
+
scope: 'project-memory',
|
|
3015
|
+
status: 'missing',
|
|
3016
|
+
action: 'project-memory scan',
|
|
3017
|
+
stalenessOriginTaskId: activeTask.id,
|
|
3018
|
+
authorizesTask: false,
|
|
3019
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
3020
|
+
}));
|
|
2769
3021
|
}
|
|
2770
|
-
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
2771
3022
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
2772
3023
|
const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
|
|
2773
3024
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
@@ -2783,7 +3034,17 @@ export class WorkflowService {
|
|
|
2783
3034
|
const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
|
|
2784
3035
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2785
3036
|
: [];
|
|
2786
|
-
|
|
3037
|
+
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
3038
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
|
|
3039
|
+
if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
|
|
3040
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
|
|
3041
|
+
taskId: requestedTask.id,
|
|
3042
|
+
blockingTaskId: refreshOwner.task.id,
|
|
3043
|
+
requiredAction: 'Run repository next and its exact task context-refresh action first.',
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return withAdoption(withMilestoneCancellation({
|
|
2787
3048
|
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2788
3049
|
status: knowledgeMap.status,
|
|
2789
3050
|
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
@@ -2815,14 +3076,14 @@ export class WorkflowService {
|
|
|
2815
3076
|
},
|
|
2816
3077
|
}
|
|
2817
3078
|
: {}),
|
|
2818
|
-
});
|
|
3079
|
+
}));
|
|
2819
3080
|
}
|
|
2820
3081
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2821
3082
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
2822
3083
|
activeTask.planHash &&
|
|
2823
3084
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
2824
3085
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
2825
|
-
return withAdoption({
|
|
3086
|
+
return withAdoption(withMilestoneCancellation({
|
|
2826
3087
|
scope: 'task',
|
|
2827
3088
|
id: activeTask.id,
|
|
2828
3089
|
status: activeTask.status,
|
|
@@ -2830,9 +3091,41 @@ export class WorkflowService {
|
|
|
2830
3091
|
revision: activeTask.revision,
|
|
2831
3092
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
2832
3093
|
knowledgeMapHash,
|
|
2833
|
-
});
|
|
3094
|
+
}));
|
|
2834
3095
|
}
|
|
2835
|
-
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
3096
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
|
|
3097
|
+
}
|
|
3098
|
+
let repositoryKnowledgeMap;
|
|
3099
|
+
try {
|
|
3100
|
+
repositoryKnowledgeMap = inspectCurrentKnowledgeMap();
|
|
3101
|
+
}
|
|
3102
|
+
catch (error) {
|
|
3103
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3104
|
+
throw error;
|
|
3105
|
+
return withAdoption({
|
|
3106
|
+
scope: 'project-memory',
|
|
3107
|
+
status: 'missing',
|
|
3108
|
+
action: 'project-memory scan',
|
|
3109
|
+
authorizesMilestone: false,
|
|
3110
|
+
provenanceNotice: 'Project Knowledge must be current before Discovery or Milestone authorization.',
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
if (repositoryKnowledgeMap.status !== 'active' || !repositoryKnowledgeMap.approval) {
|
|
3114
|
+
const action = repositoryKnowledgeMap.status === 'stale'
|
|
3115
|
+
? 'project-memory reconcile'
|
|
3116
|
+
: 'project-memory approve';
|
|
3117
|
+
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
3118
|
+
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', repositoryKnowledgeMap)
|
|
3119
|
+
: [];
|
|
3120
|
+
return withAdoption({
|
|
3121
|
+
scope: 'project-memory',
|
|
3122
|
+
status: repositoryKnowledgeMap.status,
|
|
3123
|
+
action,
|
|
3124
|
+
revision: repositoryKnowledgeMap.revision,
|
|
3125
|
+
authorizesMilestone: false,
|
|
3126
|
+
provenanceNotice: 'Project Knowledge must be reconciled and approved before Discovery or Milestone authorization.',
|
|
3127
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3128
|
+
});
|
|
2836
3129
|
}
|
|
2837
3130
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2838
3131
|
if (activeMilestone) {
|
|
@@ -2911,6 +3204,19 @@ export class WorkflowService {
|
|
|
2911
3204
|
status: activeMilestone.status,
|
|
2912
3205
|
action: nextForMilestone(activeMilestone),
|
|
2913
3206
|
revision: activeMilestone.revision,
|
|
3207
|
+
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
3208
|
+
? {
|
|
3209
|
+
milestoneAuthorizationOptions: [
|
|
3210
|
+
{ action: 'milestone authorize', mode: 'direct-human' },
|
|
3211
|
+
{
|
|
3212
|
+
action: 'milestone autonomy-prepare',
|
|
3213
|
+
mode: 'delegated',
|
|
3214
|
+
requiresExactHumanConfirmation: true,
|
|
3215
|
+
maximumDurationHours: 72,
|
|
3216
|
+
},
|
|
3217
|
+
],
|
|
3218
|
+
}
|
|
3219
|
+
: {}),
|
|
2914
3220
|
...(activeMilestone.status === 'active'
|
|
2915
3221
|
? {
|
|
2916
3222
|
completionPolicy: {
|
|
@@ -3092,6 +3398,11 @@ export class WorkflowService {
|
|
|
3092
3398
|
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
3093
3399
|
}) ?? null
|
|
3094
3400
|
: null;
|
|
3401
|
+
const strictReviewInfrastructureFailures = strictStepReviewStep
|
|
3402
|
+
? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
|
|
3403
|
+
: 0;
|
|
3404
|
+
const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
|
|
3405
|
+
|| (task.status === 'blocked' && task.review.status === 'unverified');
|
|
3095
3406
|
const handoffOverlay = handoffPosture.state === 'pending'
|
|
3096
3407
|
? {
|
|
3097
3408
|
action: 'task claim',
|
|
@@ -3100,6 +3411,15 @@ export class WorkflowService {
|
|
|
3100
3411
|
state: 'pending',
|
|
3101
3412
|
targetActor: handoffPosture.targetActor,
|
|
3102
3413
|
eventId: handoffPosture.latestEvent.eventId,
|
|
3414
|
+
credentialRecovery: {
|
|
3415
|
+
action: 'task handoff-replace',
|
|
3416
|
+
when: 'claim credential is unavailable',
|
|
3417
|
+
requiredActor: handoffPosture.latestEvent.actor,
|
|
3418
|
+
targetActor: handoffPosture.targetActor,
|
|
3419
|
+
grantId: handoffPosture.latestEvent.grantId,
|
|
3420
|
+
expectedRevision: task.revision,
|
|
3421
|
+
invalidatesPriorCredential: true,
|
|
3422
|
+
},
|
|
3103
3423
|
},
|
|
3104
3424
|
}
|
|
3105
3425
|
: handoffPosture.state === 'claimed'
|
|
@@ -3361,6 +3681,30 @@ export class WorkflowService {
|
|
|
3361
3681
|
strictStepReview: {
|
|
3362
3682
|
reviewRequired: true,
|
|
3363
3683
|
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
3684
|
+
infrastructureFailures: strictReviewInfrastructureFailures,
|
|
3685
|
+
maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
|
|
3686
|
+
recommendedMode: 'external-sealed',
|
|
3687
|
+
externalSealedReview: {
|
|
3688
|
+
packetCommand: 'task step-review-packet',
|
|
3689
|
+
recordCommand: 'task step-review-record',
|
|
3690
|
+
reviewerMustUseSeparateChat: true,
|
|
3691
|
+
repositorySealRequired: true,
|
|
3692
|
+
},
|
|
3693
|
+
},
|
|
3694
|
+
}
|
|
3695
|
+
: {}),
|
|
3696
|
+
...(strictTaskReviewRequired
|
|
3697
|
+
? {
|
|
3698
|
+
strictTaskReview: {
|
|
3699
|
+
reviewRequired: true,
|
|
3700
|
+
localInfrastructureFailureObserved: task.review.status === 'unverified',
|
|
3701
|
+
recommendedMode: 'external-sealed',
|
|
3702
|
+
externalSealedReview: {
|
|
3703
|
+
packetCommand: 'task review-packet',
|
|
3704
|
+
recordCommand: 'task review-sealed-record',
|
|
3705
|
+
reviewerMustUseSeparateChat: true,
|
|
3706
|
+
repositorySealRequired: true,
|
|
3707
|
+
},
|
|
3364
3708
|
},
|
|
3365
3709
|
}
|
|
3366
3710
|
: {}),
|
|
@@ -3414,6 +3758,12 @@ export class WorkflowService {
|
|
|
3414
3758
|
exactInclusionRequired: true,
|
|
3415
3759
|
briefHash: task.briefHash,
|
|
3416
3760
|
knowledgeMap: knowledgeSummary(map),
|
|
3761
|
+
planRiskAuditContract: {
|
|
3762
|
+
required: true,
|
|
3763
|
+
option: '--risk-audit-file',
|
|
3764
|
+
atomicWithPlanSet: true,
|
|
3765
|
+
plannerAuditorSeparationRequired: true,
|
|
3766
|
+
},
|
|
3417
3767
|
};
|
|
3418
3768
|
}
|
|
3419
3769
|
assertTaskContextChangeAllowed(task) {
|
|
@@ -3493,6 +3843,22 @@ export class WorkflowService {
|
|
|
3493
3843
|
policyHash: grant.policyHash,
|
|
3494
3844
|
}));
|
|
3495
3845
|
}
|
|
3846
|
+
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
3847
|
+
for (const task of tasks) {
|
|
3848
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById)
|
|
3849
|
+
|| !task.planHash
|
|
3850
|
+
|| !taskCanRebindKnowledge(task)
|
|
3851
|
+
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
3852
|
+
continue;
|
|
3853
|
+
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
3854
|
+
if (!assessment.eligible)
|
|
3855
|
+
continue;
|
|
3856
|
+
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
3857
|
+
if (options.length > 0)
|
|
3858
|
+
return { task, assessment, options };
|
|
3859
|
+
}
|
|
3860
|
+
return null;
|
|
3861
|
+
}
|
|
3496
3862
|
assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
|
|
3497
3863
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
3498
3864
|
if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
@@ -3595,14 +3961,44 @@ export class WorkflowService {
|
|
|
3595
3961
|
if (!action)
|
|
3596
3962
|
return next;
|
|
3597
3963
|
const policy = gitMutationPolicyForAction(task, action);
|
|
3598
|
-
const
|
|
3964
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
3965
|
+
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
3599
3966
|
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
|
|
3967
|
+
const activeLease = writerTokenContract
|
|
3968
|
+
? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
3969
|
+
: null;
|
|
3970
|
+
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
3971
|
+
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
3972
|
+
? {
|
|
3973
|
+
action: 'task writer-credential-replace',
|
|
3974
|
+
when: 'the active writer credential is unavailable',
|
|
3975
|
+
requiredActor: writerCredentialRecoveryActor,
|
|
3976
|
+
expectedRevision: task.revision,
|
|
3977
|
+
invalidatesPriorCredential: true,
|
|
3978
|
+
acquiresLeaseWhenAbsent: true,
|
|
3979
|
+
}
|
|
3980
|
+
: null;
|
|
3600
3981
|
return {
|
|
3601
3982
|
...next,
|
|
3602
3983
|
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
3603
3984
|
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
3985
|
+
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
3604
3986
|
};
|
|
3605
3987
|
}
|
|
3988
|
+
writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
|
|
3989
|
+
if (claimedActor)
|
|
3990
|
+
return claimedActor;
|
|
3991
|
+
if (activeLeaseOwner)
|
|
3992
|
+
return activeLeaseOwner;
|
|
3993
|
+
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
3994
|
+
? 'final_acceptance'
|
|
3995
|
+
: ['task step-complete', 'task submit'].includes(action)
|
|
3996
|
+
? 'execution'
|
|
3997
|
+
: null;
|
|
3998
|
+
if (!authorizationKind)
|
|
3999
|
+
return null;
|
|
4000
|
+
return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
|
|
4001
|
+
}
|
|
3606
4002
|
describeMilestoneInitialAssembly(projectId, milestone, tasks) {
|
|
3607
4003
|
const assessment = assessMilestoneInitialAssembly({
|
|
3608
4004
|
store: this.store,
|
|
@@ -4373,8 +4769,10 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4373
4769
|
const claimedC1LeaseBound = [
|
|
4374
4770
|
'task run',
|
|
4375
4771
|
'task step-review',
|
|
4772
|
+
'task step-review-record',
|
|
4376
4773
|
'task review-record',
|
|
4377
4774
|
'task review-launch',
|
|
4775
|
+
'task review-sealed-record',
|
|
4378
4776
|
'task corrective-decision-recover',
|
|
4379
4777
|
'task remediation-mode-recover',
|
|
4380
4778
|
'task corrective-decision',
|
|
@@ -4392,6 +4790,8 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4392
4790
|
option: '--writer-token',
|
|
4393
4791
|
source: 'active-writer-lease',
|
|
4394
4792
|
sensitive: true,
|
|
4793
|
+
renewalDisposition: 'reuse-presented-token',
|
|
4794
|
+
tokenReissuedOnRenewal: false,
|
|
4395
4795
|
};
|
|
4396
4796
|
}
|
|
4397
4797
|
function observationPreflightBlockers(observation) {
|