codex-workflow-v2 2.0.0-beta.4 → 2.0.0-beta.5
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 +407 -43
- package/dist/src/workflow.js.map +1 -1
- package/docs/release.md +3 -1
- 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,
|
|
@@ -969,7 +976,7 @@ export class WorkflowService {
|
|
|
969
976
|
resultHash,
|
|
970
977
|
evidenceHash,
|
|
971
978
|
headCommit: currentHead,
|
|
972
|
-
reason:
|
|
979
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
973
980
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
974
981
|
...(delegation ? { delegation } : {}),
|
|
975
982
|
};
|
|
@@ -1416,7 +1423,7 @@ export class WorkflowService {
|
|
|
1416
1423
|
claimTokenAvailable: false,
|
|
1417
1424
|
};
|
|
1418
1425
|
}
|
|
1419
|
-
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
1426
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null, replacePending = false) {
|
|
1420
1427
|
const context = this.#mutationContext(repository);
|
|
1421
1428
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1422
1429
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -1475,6 +1482,7 @@ export class WorkflowService {
|
|
|
1475
1482
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1476
1483
|
grantId,
|
|
1477
1484
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
1485
|
+
replacePending,
|
|
1478
1486
|
}, this.now());
|
|
1479
1487
|
releaseLease();
|
|
1480
1488
|
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
@@ -1542,6 +1550,43 @@ export class WorkflowService {
|
|
|
1542
1550
|
throw error;
|
|
1543
1551
|
}
|
|
1544
1552
|
}
|
|
1553
|
+
replaceClaimedWriterCredential(repository, taskId, expectedRevision, actor) {
|
|
1554
|
+
const context = this.#mutationContext(repository);
|
|
1555
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1556
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1557
|
+
const normalizedActor = actor.trim();
|
|
1558
|
+
if (!normalizedActor)
|
|
1559
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1560
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1561
|
+
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1562
|
+
const action = String(navigation.action);
|
|
1563
|
+
const currentLease = context.locks.inspect(taskId);
|
|
1564
|
+
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed');
|
|
1565
|
+
const requiredActor = writerTokenContract
|
|
1566
|
+
? this.writerCredentialRecoveryActor(task, action, posture.state === 'claimed' ? posture.claimant : null, currentLease?.owner ?? null)
|
|
1567
|
+
: null;
|
|
1568
|
+
if (!requiredActor || requiredActor !== normalizedActor) {
|
|
1569
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} writer credential replacement requires the exact advertised recovery actor.`, {
|
|
1570
|
+
taskId,
|
|
1571
|
+
posture: posture.state,
|
|
1572
|
+
claimant: posture.state === 'claimed' ? posture.claimant : null,
|
|
1573
|
+
actor: normalizedActor,
|
|
1574
|
+
requiredActor,
|
|
1575
|
+
nextAction: action,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
const lease = currentLease
|
|
1579
|
+
? context.locks.replaceCredential(taskId, normalizedActor)
|
|
1580
|
+
: context.locks.acquire(taskId, normalizedActor);
|
|
1581
|
+
if (posture.state === 'claimed') {
|
|
1582
|
+
appendTaskWriterCredentialReplacement(this.store, task, normalizedActor, lease.token, this.now());
|
|
1583
|
+
}
|
|
1584
|
+
return {
|
|
1585
|
+
task,
|
|
1586
|
+
lease: toPublicWriterLease(lease, this.now()),
|
|
1587
|
+
writerCredential: { disposition: 'issued', token: lease.token },
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1545
1590
|
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1546
1591
|
const context = this.#mutationContext(repository);
|
|
1547
1592
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1636,6 +1681,7 @@ export class WorkflowService {
|
|
|
1636
1681
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
1637
1682
|
}
|
|
1638
1683
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1684
|
+
let resumesCorrectiveReplan = false;
|
|
1639
1685
|
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1640
1686
|
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
1687
|
taskId,
|
|
@@ -1650,20 +1696,21 @@ export class WorkflowService {
|
|
|
1650
1696
|
&& guardedPosture.correctiveDecision.planHash !== task.planHash
|
|
1651
1697
|
? guardedPosture.correctiveDecision
|
|
1652
1698
|
: null;
|
|
1653
|
-
const recovery = staleDecision
|
|
1699
|
+
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1654
1700
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1655
1701
|
: null;
|
|
1656
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1702
|
+
const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1703
|
+
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1657
1704
|
}
|
|
1658
1705
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1659
1706
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1660
|
-
if (step.status === 'planned' && dirty.length > 0) {
|
|
1707
|
+
if (step.status === 'planned' && dirty.length > 0 && !resumesCorrectiveReplan) {
|
|
1661
1708
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1662
1709
|
changedFiles: dirty,
|
|
1663
1710
|
});
|
|
1664
1711
|
}
|
|
1665
1712
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
1666
|
-
if (step.status === 'failed' && dirtyOutsideStep.length > 0) {
|
|
1713
|
+
if ((step.status === 'failed' || resumesCorrectiveReplan) && dirtyOutsideStep.length > 0) {
|
|
1667
1714
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} retry has changes outside allowedWrites.`, {
|
|
1668
1715
|
changedFiles: dirtyOutsideStep,
|
|
1669
1716
|
});
|
|
@@ -1986,10 +2033,18 @@ export class WorkflowService {
|
|
|
1986
2033
|
resolution: currentCycle.resolution,
|
|
1987
2034
|
});
|
|
1988
2035
|
}
|
|
1989
|
-
|
|
2036
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2037
|
+
if (infrastructureFailures >= MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2038
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review exhausted its bounded infrastructure retries.', {
|
|
2039
|
+
taskId: task.id,
|
|
2040
|
+
stepId,
|
|
2041
|
+
infrastructureFailures,
|
|
2042
|
+
requiredAction: 'Use an external sealed reviewer or run doctor; do not start another local reviewer process.',
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
1990
2045
|
}
|
|
1991
2046
|
else if (currentCycle.resolution !== 'pending') {
|
|
1992
|
-
if (currentCycle.resolution === 'failed'
|
|
2047
|
+
if (currentCycle.resolution === 'failed') {
|
|
1993
2048
|
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1994
2049
|
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1995
2050
|
taskId: task.id,
|
|
@@ -1997,7 +2052,7 @@ export class WorkflowService {
|
|
|
1997
2052
|
resolution: currentCycle.resolution,
|
|
1998
2053
|
});
|
|
1999
2054
|
}
|
|
2000
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2055
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2001
2056
|
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
2002
2057
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2003
2058
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
@@ -2015,7 +2070,7 @@ export class WorkflowService {
|
|
|
2015
2070
|
reviewedCommit: launch.reviewedCommit,
|
|
2016
2071
|
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
2017
2072
|
}, this.now());
|
|
2018
|
-
if (recorded.resolution === 'failed'
|
|
2073
|
+
if (recorded.resolution === 'failed') {
|
|
2019
2074
|
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
2020
2075
|
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
2021
2076
|
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
@@ -2025,7 +2080,7 @@ export class WorkflowService {
|
|
|
2025
2080
|
resolution: recorded.resolution,
|
|
2026
2081
|
});
|
|
2027
2082
|
}
|
|
2028
|
-
appendRemediationEvent(this.store, task, stepId,
|
|
2083
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2029
2084
|
completionCommit: reviewRecordedEvent.completionCommit,
|
|
2030
2085
|
checksHash: reviewRecordedEvent.checksHash,
|
|
2031
2086
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
@@ -2034,6 +2089,130 @@ export class WorkflowService {
|
|
|
2034
2089
|
}
|
|
2035
2090
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2036
2091
|
}
|
|
2092
|
+
prepareExternalStepReview(repository, taskId, stepId, expectedRevision) {
|
|
2093
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2094
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2095
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2096
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
2097
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2098
|
+
}
|
|
2099
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2100
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2101
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`);
|
|
2102
|
+
}
|
|
2103
|
+
const step = requireStep(task, stepId);
|
|
2104
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
2105
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
|
|
2106
|
+
}
|
|
2107
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2108
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== step.evidence.commitSha) {
|
|
2109
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2110
|
+
}
|
|
2111
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
2112
|
+
return prepareExternalStrictStepReviewPacket(identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence);
|
|
2113
|
+
}
|
|
2114
|
+
recordExternalStepReview(repository, taskId, stepId, expectedRevision, input, actor, writerToken = null) {
|
|
2115
|
+
const context = this.#mutationContext(repository);
|
|
2116
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2117
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2118
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2119
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2120
|
+
const packet = this.prepareExternalStepReview(repository, taskId, stepId, expectedRevision);
|
|
2121
|
+
let review;
|
|
2122
|
+
try {
|
|
2123
|
+
review = validateExternalStrictStepReviewInput(input, packet);
|
|
2124
|
+
}
|
|
2125
|
+
catch (error) {
|
|
2126
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict review.');
|
|
2127
|
+
}
|
|
2128
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2129
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Step reviewer must differ from the lifecycle mutation actor.');
|
|
2130
|
+
}
|
|
2131
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
2132
|
+
if (task.status === 'blocked' && currentCycle.resolution !== 'unverified') {
|
|
2133
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue requires an unverified prior review cycle.', {
|
|
2134
|
+
taskId,
|
|
2135
|
+
stepId,
|
|
2136
|
+
resolution: currentCycle.resolution,
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
const infrastructureFailures = countUnverifiedStrictStepReviews(this.store, task, stepId);
|
|
2140
|
+
if (infrastructureFailures > MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES) {
|
|
2141
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'External sealed review rescue exceeded its bounded infrastructure history.', {
|
|
2142
|
+
taskId,
|
|
2143
|
+
stepId,
|
|
2144
|
+
infrastructureFailures,
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
2148
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
2149
|
+
review,
|
|
2150
|
+
reviewerOutputHash: strictReviewInputHash(review),
|
|
2151
|
+
reviewedCommit: packet.reviewedCommit,
|
|
2152
|
+
isolationResult: 'verified',
|
|
2153
|
+
}, this.now());
|
|
2154
|
+
if (recorded.resolution === 'failed') {
|
|
2155
|
+
appendRemediationEvent(this.store, task, stepId, 'review-failed', {
|
|
2156
|
+
completionCommit: recorded.reviewRecordedEvent.completionCommit,
|
|
2157
|
+
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2158
|
+
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2159
|
+
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2160
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2161
|
+
}
|
|
2162
|
+
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2163
|
+
return {
|
|
2164
|
+
mode: 'external-sealed',
|
|
2165
|
+
packetHash: packet.packetHash,
|
|
2166
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2167
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2168
|
+
task: saved,
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
prepareExternalTaskReview(repository, taskId, expectedRevision) {
|
|
2172
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2173
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2174
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2175
|
+
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
2176
|
+
if (task.status !== 'validating' && !retryingUnverified) {
|
|
2177
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Task Review cannot run while task ${taskId} is ${task.status}.`);
|
|
2178
|
+
}
|
|
2179
|
+
validateTaskHistory(identity.repositoryRoot, task);
|
|
2180
|
+
assertTaskBranch(identity.repositoryRoot, task);
|
|
2181
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
2182
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Task Review requires a clean Task checkout.');
|
|
2183
|
+
}
|
|
2184
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2185
|
+
if (!task.evidenceHash || this.store.hashArtifact(taskRoot, 'evidence.json') !== task.evidenceHash) {
|
|
2186
|
+
throw new WorkflowError('STATE_CORRUPT', 'External Strict Task Review requires the exact submitted evidence artifact.');
|
|
2187
|
+
}
|
|
2188
|
+
return prepareExternalStrictTaskReviewPacket(identity.repositoryRoot, task, taskRoot, this.taskContext(repository, taskId, 'independent-reviewer'));
|
|
2189
|
+
}
|
|
2190
|
+
recordExternalTaskReview(repository, taskId, expectedRevision, input, actor, writerToken = null) {
|
|
2191
|
+
const context = this.#mutationContext(repository);
|
|
2192
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2193
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2194
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2195
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2196
|
+
const packet = this.prepareExternalTaskReview(repository, taskId, expectedRevision);
|
|
2197
|
+
let review;
|
|
2198
|
+
try {
|
|
2199
|
+
review = validateExternalStrictTaskReviewInput(input, packet);
|
|
2200
|
+
}
|
|
2201
|
+
catch (error) {
|
|
2202
|
+
throw new WorkflowError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'Invalid external strict Task review.');
|
|
2203
|
+
}
|
|
2204
|
+
if (review.reviewer.trim() === actor.trim()) {
|
|
2205
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The independent Task reviewer must differ from the lifecycle mutation actor.');
|
|
2206
|
+
}
|
|
2207
|
+
const saved = this.recordReview(repository, taskId, expectedRevision, review, actor, writerToken);
|
|
2208
|
+
return {
|
|
2209
|
+
mode: 'external-sealed',
|
|
2210
|
+
packetHash: packet.packetHash,
|
|
2211
|
+
repositorySealHash: packet.repositorySealHash,
|
|
2212
|
+
reviewerThreadId: input.reviewerThreadId,
|
|
2213
|
+
task: saved,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2037
2216
|
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
2038
2217
|
const context = this.#mutationContext(repository);
|
|
2039
2218
|
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
@@ -2586,21 +2765,13 @@ export class WorkflowService {
|
|
|
2586
2765
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2587
2766
|
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2588
2767
|
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
|
-
}
|
|
2768
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
2769
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(identity.projectId, this.store.listTasks(identity.projectId), milestonesById, current, inspected);
|
|
2770
|
+
if (refreshOwner) {
|
|
2771
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2772
|
+
taskId: refreshOwner.task.id,
|
|
2773
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2774
|
+
});
|
|
2604
2775
|
}
|
|
2605
2776
|
}
|
|
2606
2777
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
@@ -2629,7 +2800,7 @@ export class WorkflowService {
|
|
|
2629
2800
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
2630
2801
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
2631
2802
|
}
|
|
2632
|
-
next(repository) {
|
|
2803
|
+
next(repository, taskId = null) {
|
|
2633
2804
|
const status = this.status(repository);
|
|
2634
2805
|
if (status.observation.kind === 'blocked') {
|
|
2635
2806
|
return {
|
|
@@ -2743,14 +2914,75 @@ export class WorkflowService {
|
|
|
2743
2914
|
});
|
|
2744
2915
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
2745
2916
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
2746
|
-
const
|
|
2917
|
+
const requestedTask = taskId === null
|
|
2918
|
+
? null
|
|
2919
|
+
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
2920
|
+
if (taskId !== null && !requestedTask) {
|
|
2921
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2922
|
+
}
|
|
2923
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
|
|
2924
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
2925
|
+
taskId: requestedTask.id,
|
|
2926
|
+
taskStatus: requestedTask.status,
|
|
2927
|
+
milestoneId: requestedTask.milestoneId,
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
const activeTask = requestedTask
|
|
2931
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2747
2932
|
if (activeTask) {
|
|
2748
|
-
const
|
|
2749
|
-
?
|
|
2933
|
+
const activeTaskMilestone = activeTask.milestoneId
|
|
2934
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
2750
2935
|
: null;
|
|
2936
|
+
const milestoneCancellationSafe = Boolean(activeTaskMilestone
|
|
2937
|
+
&& !activeTask.steps.some((step) => step.status === 'in_progress')
|
|
2938
|
+
&& status.observation.leases.length === 0
|
|
2939
|
+
&& status.observation.coreTaskOperations.length === 0
|
|
2940
|
+
&& status.observation.pendingProjectTransactions.length === 0
|
|
2941
|
+
&& status.observation.pendingMilestoneTransactions.length === 0
|
|
2942
|
+
&& status.observation.pendingTaskTransactions.length === 0
|
|
2943
|
+
&& status.observation.corruptTaskTransactions.length === 0);
|
|
2944
|
+
const withMilestoneCancellation = (value) => (milestoneCancellationSafe
|
|
2945
|
+
? {
|
|
2946
|
+
...value,
|
|
2947
|
+
milestoneCancellationOption: {
|
|
2948
|
+
command: 'milestone cancel',
|
|
2949
|
+
milestoneId: activeTaskMilestone.id,
|
|
2950
|
+
expectedRevision: activeTaskMilestone.revision,
|
|
2951
|
+
reasonRequired: true,
|
|
2952
|
+
taskDisposition: 'retained-historical',
|
|
2953
|
+
},
|
|
2954
|
+
}
|
|
2955
|
+
: value);
|
|
2956
|
+
let activePlanRiskAudit = null;
|
|
2957
|
+
let planRiskAuditRecovery = null;
|
|
2958
|
+
if (activeTask.planHash) {
|
|
2959
|
+
try {
|
|
2960
|
+
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
2961
|
+
}
|
|
2962
|
+
catch (error) {
|
|
2963
|
+
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
2964
|
+
throw error;
|
|
2965
|
+
planRiskAuditRecovery = {
|
|
2966
|
+
scope: 'task',
|
|
2967
|
+
id: activeTask.id,
|
|
2968
|
+
status: activeTask.status,
|
|
2969
|
+
action: 'task plan-risk-audit',
|
|
2970
|
+
revision: activeTask.revision,
|
|
2971
|
+
planRiskAuditRecovery: {
|
|
2972
|
+
state: 'required',
|
|
2973
|
+
reason: error.message,
|
|
2974
|
+
currentBriefHash: activeTask.briefHash,
|
|
2975
|
+
currentPlanHash: activeTask.planHash,
|
|
2976
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
2977
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
2978
|
+
},
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2751
2982
|
const taskNext = activeTask.status === 'planning'
|
|
2752
2983
|
? null
|
|
2753
|
-
:
|
|
2984
|
+
: planRiskAuditRecovery
|
|
2985
|
+
?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2754
2986
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
2755
2987
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2756
2988
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -2762,12 +2994,27 @@ export class WorkflowService {
|
|
|
2762
2994
|
|| hardStop
|
|
2763
2995
|
|| terminalCorrectiveDecision === 'split-required'
|
|
2764
2996
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2765
|
-
return withAdoption(taskNext);
|
|
2997
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
2766
2998
|
}
|
|
2767
2999
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
2768
|
-
return withAdoption(taskNext);
|
|
3000
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
3001
|
+
}
|
|
3002
|
+
let knowledgeMap;
|
|
3003
|
+
try {
|
|
3004
|
+
knowledgeMap = this.knowledgeMapStatus(repository);
|
|
3005
|
+
}
|
|
3006
|
+
catch (error) {
|
|
3007
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
3008
|
+
throw error;
|
|
3009
|
+
return withAdoption(withMilestoneCancellation({
|
|
3010
|
+
scope: 'project-memory',
|
|
3011
|
+
status: 'missing',
|
|
3012
|
+
action: 'project-memory scan',
|
|
3013
|
+
stalenessOriginTaskId: activeTask.id,
|
|
3014
|
+
authorizesTask: false,
|
|
3015
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
3016
|
+
}));
|
|
2769
3017
|
}
|
|
2770
|
-
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
2771
3018
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
2772
3019
|
const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
|
|
2773
3020
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
@@ -2783,7 +3030,17 @@ export class WorkflowService {
|
|
|
2783
3030
|
const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
|
|
2784
3031
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2785
3032
|
: [];
|
|
2786
|
-
|
|
3033
|
+
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
3034
|
+
const refreshOwner = this.findDelegatedContextRefreshOwner(status.projectId, status.tasks, milestonesById, storedKnowledgeMap, knowledgeMap);
|
|
3035
|
+
if (refreshOwner && refreshOwner.task.id !== requestedTask.id) {
|
|
3036
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} cannot be selected while Task ${refreshOwner.task.id} requires an atomic context refresh.`, {
|
|
3037
|
+
taskId: requestedTask.id,
|
|
3038
|
+
blockingTaskId: refreshOwner.task.id,
|
|
3039
|
+
requiredAction: 'Run repository next and its exact task context-refresh action first.',
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return withAdoption(withMilestoneCancellation({
|
|
2787
3044
|
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2788
3045
|
status: knowledgeMap.status,
|
|
2789
3046
|
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
@@ -2815,14 +3072,14 @@ export class WorkflowService {
|
|
|
2815
3072
|
},
|
|
2816
3073
|
}
|
|
2817
3074
|
: {}),
|
|
2818
|
-
});
|
|
3075
|
+
}));
|
|
2819
3076
|
}
|
|
2820
3077
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
2821
3078
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
2822
3079
|
activeTask.planHash &&
|
|
2823
3080
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
2824
3081
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
2825
|
-
return withAdoption({
|
|
3082
|
+
return withAdoption(withMilestoneCancellation({
|
|
2826
3083
|
scope: 'task',
|
|
2827
3084
|
id: activeTask.id,
|
|
2828
3085
|
status: activeTask.status,
|
|
@@ -2830,9 +3087,9 @@ export class WorkflowService {
|
|
|
2830
3087
|
revision: activeTask.revision,
|
|
2831
3088
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
2832
3089
|
knowledgeMapHash,
|
|
2833
|
-
});
|
|
3090
|
+
}));
|
|
2834
3091
|
}
|
|
2835
|
-
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
3092
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
|
|
2836
3093
|
}
|
|
2837
3094
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2838
3095
|
if (activeMilestone) {
|
|
@@ -2911,6 +3168,19 @@ export class WorkflowService {
|
|
|
2911
3168
|
status: activeMilestone.status,
|
|
2912
3169
|
action: nextForMilestone(activeMilestone),
|
|
2913
3170
|
revision: activeMilestone.revision,
|
|
3171
|
+
...(activeMilestone.status === 'awaiting_execution_authorization' && activeMilestone.planHash
|
|
3172
|
+
? {
|
|
3173
|
+
milestoneAuthorizationOptions: [
|
|
3174
|
+
{ action: 'milestone authorize', mode: 'direct-human' },
|
|
3175
|
+
{
|
|
3176
|
+
action: 'milestone autonomy-prepare',
|
|
3177
|
+
mode: 'delegated',
|
|
3178
|
+
requiresExactHumanConfirmation: true,
|
|
3179
|
+
maximumDurationHours: 72,
|
|
3180
|
+
},
|
|
3181
|
+
],
|
|
3182
|
+
}
|
|
3183
|
+
: {}),
|
|
2914
3184
|
...(activeMilestone.status === 'active'
|
|
2915
3185
|
? {
|
|
2916
3186
|
completionPolicy: {
|
|
@@ -3092,6 +3362,11 @@ export class WorkflowService {
|
|
|
3092
3362
|
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
3093
3363
|
}) ?? null
|
|
3094
3364
|
: null;
|
|
3365
|
+
const strictReviewInfrastructureFailures = strictStepReviewStep
|
|
3366
|
+
? countUnverifiedStrictStepReviews(this.store, task, strictStepReviewStep.id)
|
|
3367
|
+
: 0;
|
|
3368
|
+
const strictTaskReviewRequired = (task.status === 'validating' && task.review.status === 'pending')
|
|
3369
|
+
|| (task.status === 'blocked' && task.review.status === 'unverified');
|
|
3095
3370
|
const handoffOverlay = handoffPosture.state === 'pending'
|
|
3096
3371
|
? {
|
|
3097
3372
|
action: 'task claim',
|
|
@@ -3100,6 +3375,15 @@ export class WorkflowService {
|
|
|
3100
3375
|
state: 'pending',
|
|
3101
3376
|
targetActor: handoffPosture.targetActor,
|
|
3102
3377
|
eventId: handoffPosture.latestEvent.eventId,
|
|
3378
|
+
credentialRecovery: {
|
|
3379
|
+
action: 'task handoff-replace',
|
|
3380
|
+
when: 'claim credential is unavailable',
|
|
3381
|
+
requiredActor: handoffPosture.latestEvent.actor,
|
|
3382
|
+
targetActor: handoffPosture.targetActor,
|
|
3383
|
+
grantId: handoffPosture.latestEvent.grantId,
|
|
3384
|
+
expectedRevision: task.revision,
|
|
3385
|
+
invalidatesPriorCredential: true,
|
|
3386
|
+
},
|
|
3103
3387
|
},
|
|
3104
3388
|
}
|
|
3105
3389
|
: handoffPosture.state === 'claimed'
|
|
@@ -3361,6 +3645,30 @@ export class WorkflowService {
|
|
|
3361
3645
|
strictStepReview: {
|
|
3362
3646
|
reviewRequired: true,
|
|
3363
3647
|
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
3648
|
+
infrastructureFailures: strictReviewInfrastructureFailures,
|
|
3649
|
+
maxLocalInfrastructureFailures: MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES,
|
|
3650
|
+
recommendedMode: 'external-sealed',
|
|
3651
|
+
externalSealedReview: {
|
|
3652
|
+
packetCommand: 'task step-review-packet',
|
|
3653
|
+
recordCommand: 'task step-review-record',
|
|
3654
|
+
reviewerMustUseSeparateChat: true,
|
|
3655
|
+
repositorySealRequired: true,
|
|
3656
|
+
},
|
|
3657
|
+
},
|
|
3658
|
+
}
|
|
3659
|
+
: {}),
|
|
3660
|
+
...(strictTaskReviewRequired
|
|
3661
|
+
? {
|
|
3662
|
+
strictTaskReview: {
|
|
3663
|
+
reviewRequired: true,
|
|
3664
|
+
localInfrastructureFailureObserved: task.review.status === 'unverified',
|
|
3665
|
+
recommendedMode: 'external-sealed',
|
|
3666
|
+
externalSealedReview: {
|
|
3667
|
+
packetCommand: 'task review-packet',
|
|
3668
|
+
recordCommand: 'task review-sealed-record',
|
|
3669
|
+
reviewerMustUseSeparateChat: true,
|
|
3670
|
+
repositorySealRequired: true,
|
|
3671
|
+
},
|
|
3364
3672
|
},
|
|
3365
3673
|
}
|
|
3366
3674
|
: {}),
|
|
@@ -3414,6 +3722,12 @@ export class WorkflowService {
|
|
|
3414
3722
|
exactInclusionRequired: true,
|
|
3415
3723
|
briefHash: task.briefHash,
|
|
3416
3724
|
knowledgeMap: knowledgeSummary(map),
|
|
3725
|
+
planRiskAuditContract: {
|
|
3726
|
+
required: true,
|
|
3727
|
+
option: '--risk-audit-file',
|
|
3728
|
+
atomicWithPlanSet: true,
|
|
3729
|
+
plannerAuditorSeparationRequired: true,
|
|
3730
|
+
},
|
|
3417
3731
|
};
|
|
3418
3732
|
}
|
|
3419
3733
|
assertTaskContextChangeAllowed(task) {
|
|
@@ -3493,6 +3807,22 @@ export class WorkflowService {
|
|
|
3493
3807
|
policyHash: grant.policyHash,
|
|
3494
3808
|
}));
|
|
3495
3809
|
}
|
|
3810
|
+
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
3811
|
+
for (const task of tasks) {
|
|
3812
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById)
|
|
3813
|
+
|| !task.planHash
|
|
3814
|
+
|| !taskCanRebindKnowledge(task)
|
|
3815
|
+
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
3816
|
+
continue;
|
|
3817
|
+
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
3818
|
+
if (!assessment.eligible)
|
|
3819
|
+
continue;
|
|
3820
|
+
const options = this.delegatedContextRefreshOptions(projectId, task, inspected, assessment.mode);
|
|
3821
|
+
if (options.length > 0)
|
|
3822
|
+
return { task, assessment, options };
|
|
3823
|
+
}
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3496
3826
|
assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
|
|
3497
3827
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
3498
3828
|
if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
@@ -3595,14 +3925,44 @@ export class WorkflowService {
|
|
|
3595
3925
|
if (!action)
|
|
3596
3926
|
return next;
|
|
3597
3927
|
const policy = gitMutationPolicyForAction(task, action);
|
|
3598
|
-
const
|
|
3928
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
3929
|
+
const claimedC1Posture = c1Posture.state === 'claimed';
|
|
3599
3930
|
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
|
|
3931
|
+
const activeLease = writerTokenContract
|
|
3932
|
+
? inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
3933
|
+
: null;
|
|
3934
|
+
const writerCredentialRecoveryActor = this.writerCredentialRecoveryActor(task, action, claimedC1Posture ? c1Posture.claimant : null, activeLease?.owner ?? null);
|
|
3935
|
+
const writerCredentialRecovery = writerTokenContract && writerCredentialRecoveryActor
|
|
3936
|
+
? {
|
|
3937
|
+
action: 'task writer-credential-replace',
|
|
3938
|
+
when: 'the active writer credential is unavailable',
|
|
3939
|
+
requiredActor: writerCredentialRecoveryActor,
|
|
3940
|
+
expectedRevision: task.revision,
|
|
3941
|
+
invalidatesPriorCredential: true,
|
|
3942
|
+
acquiresLeaseWhenAbsent: true,
|
|
3943
|
+
}
|
|
3944
|
+
: null;
|
|
3600
3945
|
return {
|
|
3601
3946
|
...next,
|
|
3602
3947
|
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
3603
3948
|
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
3949
|
+
...(writerCredentialRecovery ? { writerCredentialRecovery } : {}),
|
|
3604
3950
|
};
|
|
3605
3951
|
}
|
|
3952
|
+
writerCredentialRecoveryActor(task, action, claimedActor, activeLeaseOwner) {
|
|
3953
|
+
if (claimedActor)
|
|
3954
|
+
return claimedActor;
|
|
3955
|
+
if (activeLeaseOwner)
|
|
3956
|
+
return activeLeaseOwner;
|
|
3957
|
+
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
3958
|
+
? 'final_acceptance'
|
|
3959
|
+
: ['task step-complete', 'task submit'].includes(action)
|
|
3960
|
+
? 'execution'
|
|
3961
|
+
: null;
|
|
3962
|
+
if (!authorizationKind)
|
|
3963
|
+
return null;
|
|
3964
|
+
return [...task.authorizations].reverse().find((authorization) => authorization.kind === authorizationKind && authorization.decision === 'approved')?.actor ?? null;
|
|
3965
|
+
}
|
|
3606
3966
|
describeMilestoneInitialAssembly(projectId, milestone, tasks) {
|
|
3607
3967
|
const assessment = assessMilestoneInitialAssembly({
|
|
3608
3968
|
store: this.store,
|
|
@@ -4373,8 +4733,10 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4373
4733
|
const claimedC1LeaseBound = [
|
|
4374
4734
|
'task run',
|
|
4375
4735
|
'task step-review',
|
|
4736
|
+
'task step-review-record',
|
|
4376
4737
|
'task review-record',
|
|
4377
4738
|
'task review-launch',
|
|
4739
|
+
'task review-sealed-record',
|
|
4378
4740
|
'task corrective-decision-recover',
|
|
4379
4741
|
'task remediation-mode-recover',
|
|
4380
4742
|
'task corrective-decision',
|
|
@@ -4392,6 +4754,8 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4392
4754
|
option: '--writer-token',
|
|
4393
4755
|
source: 'active-writer-lease',
|
|
4394
4756
|
sensitive: true,
|
|
4757
|
+
renewalDisposition: 'reuse-presented-token',
|
|
4758
|
+
tokenReissuedOnRenewal: false,
|
|
4395
4759
|
};
|
|
4396
4760
|
}
|
|
4397
4761
|
function observationPreflightBlockers(observation) {
|