codex-workflow-v2 2.0.0-beta.13.1 → 2.0.0-beta.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/reviewer-runtime-build.json +43 -19
- package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +3 -0
- package/dist/src/alpha6/plan-risk.js +19 -9
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/preexecution-replan.d.ts +38 -0
- package/dist/src/alpha6/preexecution-replan.js +130 -0
- package/dist/src/alpha6/preexecution-replan.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +4 -2
- package/dist/src/alpha6/remediation.js +155 -20
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +18 -0
- package/dist/src/alpha6/root-cause-replan-carryover.js +396 -0
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -0
- package/dist/src/alpha7/autonomy.d.ts +3 -0
- package/dist/src/alpha7/autonomy.js +89 -42
- package/dist/src/alpha7/autonomy.js.map +1 -1
- package/dist/src/change-explanation.d.ts +64 -0
- package/dist/src/change-explanation.js +150 -0
- package/dist/src/change-explanation.js.map +1 -0
- package/dist/src/cli-actions.d.ts +3 -2
- package/dist/src/cli-actions.js +6 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +73 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +36 -0
- package/dist/src/dependency-provenance.d.ts +3 -2
- package/dist/src/dependency-provenance.js +33 -2
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/plan-semantics.d.ts +11 -0
- package/dist/src/domain/plan-semantics.js +49 -0
- package/dist/src/domain/plan-semantics.js.map +1 -0
- package/dist/src/gateway-handshake.js +2 -0
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/observed-routes.js +1 -0
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +49 -0
- package/dist/src/pending-review-update.js +132 -0
- package/dist/src/pending-review-update.js.map +1 -0
- package/dist/src/repository.js +16 -4
- package/dist/src/repository.js.map +1 -1
- package/dist/src/reviewer.js +4 -1
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +5 -4
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +12 -3
- package/dist/src/state/corrective-yield-executor.js.map +1 -1
- package/dist/src/state/store.d.ts +1 -1
- package/dist/src/state/store.js +17 -3
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +84 -1
- package/dist/src/workflow.js +1610 -355
- package/dist/src/workflow.js.map +1 -1
- package/docs/beta13.2-signal-review-recovery.md +38 -0
- package/docs/change-model.md +118 -0
- package/docs/delegated-approval.md +26 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +3 -5
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +11 -3
- package/docs/pending-review-update.md +15 -0
- package/docs/release.md +258 -2
- package/docs/updating-existing-project.md +10 -2
- package/package.json +2 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +204 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +11 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +298 -0
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +55 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +218 -21
- package/schemas/preexecution-replan-event.schema.json +45 -0
- package/schemas/task.schema.json +55 -0
package/dist/src/workflow.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
+
import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
|
|
2
|
+
import { explainChange } from './change-explanation.js';
|
|
3
|
+
import { pendingReviewRunnerFingerprint, PENDING_REVIEW_UPDATE_SIDECAR, preparePendingReviewUpdateEvent, PENDING_REVIEW_SOURCE_VERSION, pendingReviewSourceBinding, pendingReviewBindingHash, assertPendingReviewTransport, verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
1
4
|
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
5
|
import { createHash } from 'node:crypto';
|
|
3
6
|
import path from 'node:path';
|
|
4
7
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
|
-
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
8
|
+
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
|
|
6
9
|
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
7
10
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
8
11
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
9
12
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
13
|
+
import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from './domain/plan-semantics.js';
|
|
10
14
|
import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
|
|
11
15
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
12
16
|
import { WorkflowError } from './errors.js';
|
|
13
17
|
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
14
18
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
15
19
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
16
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
20
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
|
|
17
21
|
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
18
22
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
19
|
-
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
20
|
-
import {
|
|
23
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
24
|
+
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
25
|
+
import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
|
|
26
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
21
27
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
22
28
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
23
29
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
@@ -27,7 +33,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
|
|
|
27
33
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
28
34
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
29
35
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
30
|
-
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
36
|
+
import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
|
|
31
37
|
import { ensureDirectory } from './fs-utils.js';
|
|
32
38
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
33
39
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
@@ -43,12 +49,16 @@ import { createEntityId } from './ulid.js';
|
|
|
43
49
|
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
44
50
|
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
45
51
|
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
52
|
+
// Release-reviewed exact compatibility target; never infer support from PACKAGE_VERSION.
|
|
53
|
+
const CONSUMED_CARRYOVER_TARGET_VERSION = '2.0.0-beta.13.10';
|
|
46
54
|
export class WorkflowService {
|
|
47
55
|
store;
|
|
48
56
|
now;
|
|
49
|
-
|
|
57
|
+
transactionHooks;
|
|
58
|
+
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
|
|
50
59
|
this.store = store;
|
|
51
60
|
this.now = now;
|
|
61
|
+
this.transactionHooks = transactionHooks;
|
|
52
62
|
}
|
|
53
63
|
#mutationContext(repository) {
|
|
54
64
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -405,7 +415,7 @@ export class WorkflowService {
|
|
|
405
415
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
406
416
|
tasks,
|
|
407
417
|
milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
|
|
408
|
-
delegations: this.
|
|
418
|
+
delegations: this.listDelegations(identity.projectId),
|
|
409
419
|
adoption,
|
|
410
420
|
};
|
|
411
421
|
}
|
|
@@ -422,7 +432,7 @@ export class WorkflowService {
|
|
|
422
432
|
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
423
433
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation confirmation does not match the exact project and policy.', { projectId: identity.projectId, policyHash: gate.policyHash });
|
|
424
434
|
}
|
|
425
|
-
const duplicate = this.
|
|
435
|
+
const duplicate = this.listDelegations(identity.projectId)
|
|
426
436
|
.find((grant) => grant.policyHash === gate.policyHash);
|
|
427
437
|
if (duplicate) {
|
|
428
438
|
throw new WorkflowError('TRANSITION_BLOCKED', 'This exact delegation policy was already issued.', {
|
|
@@ -452,26 +462,35 @@ export class WorkflowService {
|
|
|
452
462
|
}, null);
|
|
453
463
|
}
|
|
454
464
|
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
455
|
-
const { identity } = this.#mutationContext(repository);
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
466
|
+
const before = this.store.readDelegation(identity.projectId, grantId);
|
|
467
|
+
const lease = before.scope.kind === 'milestone'
|
|
468
|
+
? locks.acquire(before.scope.id, 'workflow-core:delegation-revocation') : null;
|
|
469
|
+
try {
|
|
470
|
+
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
471
|
+
assertExpectedRevision(grant, expectedRevision);
|
|
472
|
+
if (grant.status !== 'active') {
|
|
473
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
|
|
474
|
+
}
|
|
475
|
+
if (actor.trim() !== grant.principal) {
|
|
476
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
|
|
477
|
+
principal: grant.principal,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (!reason.trim())
|
|
481
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
482
|
+
return this.store.writeDelegation({
|
|
483
|
+
...grant,
|
|
484
|
+
status: 'revoked',
|
|
485
|
+
revokedAt: this.now().toISOString(),
|
|
486
|
+
revokedBy: grant.principal,
|
|
487
|
+
revokeReason: reason.trim(),
|
|
488
|
+
}, expectedRevision);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
if (lease)
|
|
492
|
+
locks.release(lease.entityId, lease.token);
|
|
465
493
|
}
|
|
466
|
-
if (!reason.trim())
|
|
467
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
468
|
-
return this.store.writeDelegation({
|
|
469
|
-
...grant,
|
|
470
|
-
status: 'revoked',
|
|
471
|
-
revokedAt: this.now().toISOString(),
|
|
472
|
-
revokedBy: grant.principal,
|
|
473
|
-
revokeReason: reason.trim(),
|
|
474
|
-
}, expectedRevision);
|
|
475
494
|
}
|
|
476
495
|
startDiscovery(repository, input) {
|
|
477
496
|
if (!input.goal.trim())
|
|
@@ -1353,34 +1372,53 @@ export class WorkflowService {
|
|
|
1353
1372
|
};
|
|
1354
1373
|
}
|
|
1355
1374
|
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
1356
|
-
const { identity } = this.#mutationContext(repository);
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1375
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1376
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1377
|
+
try {
|
|
1378
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1379
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1380
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1381
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
1382
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
1383
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
|
|
1384
|
+
milestoneId,
|
|
1385
|
+
revision: milestone.revision,
|
|
1386
|
+
actor: prepared.actor,
|
|
1387
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1391
|
+
store: this.store,
|
|
1392
|
+
milestone,
|
|
1393
|
+
expectedRevision,
|
|
1394
|
+
prepared,
|
|
1395
|
+
now: this.now(),
|
|
1396
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1367
1397
|
});
|
|
1368
1398
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1399
|
+
finally {
|
|
1400
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
listDelegations(projectId) {
|
|
1404
|
+
// Valid append-only issuance events can precede publication of their grant.
|
|
1405
|
+
// Such missing state conveys no authority, even if mkdir already succeeded.
|
|
1406
|
+
const issuedIds = new Set(this.store.listMilestones(projectId).flatMap(milestone => readMilestoneAutonomyEvents(this.store, projectId, milestone.id).map(event => event.delegationGrantId)));
|
|
1407
|
+
return this.store.listDelegations(projectId, issuedIds);
|
|
1377
1408
|
}
|
|
1378
1409
|
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1410
|
+
const snapshot = this.observationSnapshot(repository);
|
|
1411
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
1412
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Autonomy preparation requires a coherent observation boundary.', {
|
|
1413
|
+
observation: snapshot.observation,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
1417
|
+
const milestone = inspectMilestoneWithIntegrity(this.store, identity.projectId, milestoneId, (taskId) => this.store.readTask(identity.projectId, taskId), () => this.store.listTasks(identity.projectId));
|
|
1381
1418
|
assertExpectedRevision(milestone, expectedRevision);
|
|
1382
1419
|
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1383
1420
|
return prepareMilestoneAutonomyContract({
|
|
1421
|
+
store: this.store,
|
|
1384
1422
|
projectId: identity.projectId,
|
|
1385
1423
|
milestone,
|
|
1386
1424
|
principal,
|
|
@@ -1390,208 +1428,244 @@ export class WorkflowService {
|
|
|
1390
1428
|
});
|
|
1391
1429
|
}
|
|
1392
1430
|
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
1393
|
-
const { identity } = this.#mutationContext(repository);
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1431
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1432
|
+
const lease = locks.acquire(milestoneId, 'workflow-core:autonomy-issuance');
|
|
1433
|
+
try {
|
|
1434
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1435
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1436
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1437
|
+
return applyMilestoneAutonomyContract({
|
|
1438
|
+
store: this.store,
|
|
1439
|
+
projectId: identity.projectId,
|
|
1440
|
+
milestone,
|
|
1441
|
+
principal,
|
|
1442
|
+
delegate,
|
|
1443
|
+
expiresAt,
|
|
1444
|
+
confirmationCode,
|
|
1445
|
+
now: this.now(),
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
finally {
|
|
1449
|
+
locks.release(milestoneId, lease.token);
|
|
1450
|
+
}
|
|
1407
1451
|
}
|
|
1408
1452
|
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
1409
|
-
const { identity } = this.#mutationContext(repository);
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
milestone,
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1453
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1454
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1455
|
+
try {
|
|
1456
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1457
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1458
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1459
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
1460
|
+
store: this.store,
|
|
1461
|
+
projectId: identity.projectId,
|
|
1462
|
+
milestone,
|
|
1463
|
+
actor,
|
|
1464
|
+
now: this.now(),
|
|
1465
|
+
});
|
|
1466
|
+
validateMilestonePlan(plan);
|
|
1467
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
1468
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
1469
|
+
milestone,
|
|
1470
|
+
plan,
|
|
1471
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1472
|
+
actor,
|
|
1473
|
+
autonomyContractEventHash: autonomy.contract.eventHash,
|
|
1474
|
+
});
|
|
1475
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1476
|
+
store: this.store,
|
|
1477
|
+
milestone,
|
|
1478
|
+
expectedRevision,
|
|
1479
|
+
prepared,
|
|
1480
|
+
now: this.now(),
|
|
1481
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
finally {
|
|
1485
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1486
|
+
}
|
|
1437
1487
|
}
|
|
1438
1488
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
1439
|
-
const { identity } = this.#mutationContext(repository);
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1489
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1490
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1491
|
+
try {
|
|
1492
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1493
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1494
|
+
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
1495
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
1496
|
+
}
|
|
1497
|
+
assertMilestoneDependenciesDeclared(milestone);
|
|
1498
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1499
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1500
|
+
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
1501
|
+
if (planHash !== milestone.planHash) {
|
|
1502
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
1503
|
+
}
|
|
1504
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
1505
|
+
const event = {
|
|
1506
|
+
kind: 'execution',
|
|
1507
|
+
decision: 'approved',
|
|
1508
|
+
actor: actor.trim() || 'user',
|
|
1509
|
+
recordedAt: this.now().toISOString(),
|
|
1510
|
+
briefHash: null,
|
|
1511
|
+
planHash,
|
|
1512
|
+
resultHash: null,
|
|
1513
|
+
evidenceHash: null,
|
|
1514
|
+
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
1515
|
+
reason,
|
|
1516
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1517
|
+
...(delegation ? { delegation } : {}),
|
|
1518
|
+
};
|
|
1519
|
+
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
1444
1520
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1448
|
-
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
1449
|
-
if (planHash !== milestone.planHash) {
|
|
1450
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
1521
|
+
finally {
|
|
1522
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1451
1523
|
}
|
|
1452
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
1453
|
-
const event = {
|
|
1454
|
-
kind: 'execution',
|
|
1455
|
-
decision: 'approved',
|
|
1456
|
-
actor: actor.trim() || 'user',
|
|
1457
|
-
recordedAt: this.now().toISOString(),
|
|
1458
|
-
briefHash: null,
|
|
1459
|
-
planHash,
|
|
1460
|
-
resultHash: null,
|
|
1461
|
-
evidenceHash: null,
|
|
1462
|
-
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
1463
|
-
reason,
|
|
1464
|
-
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1465
|
-
...(delegation ? { delegation } : {}),
|
|
1466
|
-
};
|
|
1467
|
-
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
1468
1524
|
}
|
|
1469
1525
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
1470
|
-
const { identity } = this.#mutationContext(repository);
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1491
|
-
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1526
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1527
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1528
|
+
try {
|
|
1529
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1530
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1531
|
+
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
1532
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
1533
|
+
}
|
|
1534
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1535
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1536
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1537
|
+
expectedBranch: milestone.baseBranch,
|
|
1538
|
+
actualBranch: currentBranch(identity.repositoryRoot),
|
|
1539
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
const incomplete = milestone.memberships.filter((membership) => {
|
|
1543
|
+
if (membership.disposition !== 'required')
|
|
1544
|
+
return false;
|
|
1545
|
+
return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
|
|
1492
1546
|
});
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1547
|
+
if (incomplete.length > 0) {
|
|
1548
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1549
|
+
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
const checks = runChecks(identity.repositoryRoot, milestone.checks);
|
|
1553
|
+
const baseHead = headCommit(identity.repositoryRoot);
|
|
1554
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1555
|
+
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
1556
|
+
milestoneId,
|
|
1557
|
+
baseBranch: milestone.baseBranch,
|
|
1558
|
+
headCommit: baseHead,
|
|
1559
|
+
generatedAt: this.now().toISOString(),
|
|
1560
|
+
membershipRevision: milestone.membershipRevision,
|
|
1561
|
+
memberships: milestone.memberships,
|
|
1562
|
+
checks,
|
|
1563
|
+
}, null, 2));
|
|
1564
|
+
if (checks.some((check) => !check.passed)) {
|
|
1565
|
+
const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
|
|
1566
|
+
this.store.publishArtifact(stagedEvidence);
|
|
1567
|
+
return saved;
|
|
1568
|
+
}
|
|
1569
|
+
const result = [
|
|
1570
|
+
`# Milestone result: ${milestone.title}`,
|
|
1571
|
+
'',
|
|
1572
|
+
`Outcome: ${milestone.outcome}`,
|
|
1573
|
+
'',
|
|
1574
|
+
`Validated base: ${milestone.baseBranch}@${baseHead}`,
|
|
1575
|
+
'',
|
|
1576
|
+
'All required memberships are merged and all Milestone checks passed.',
|
|
1577
|
+
'',
|
|
1578
|
+
].join('\n');
|
|
1579
|
+
const stagedResult = this.store.stageArtifact(root, 'result.md', result);
|
|
1580
|
+
const saved = this.store.writeMilestone({
|
|
1581
|
+
...milestone,
|
|
1582
|
+
status: 'awaiting_final_acceptance',
|
|
1583
|
+
evidenceHash: stagedEvidence.hash,
|
|
1584
|
+
resultHash: stagedResult.hash,
|
|
1585
|
+
}, expectedRevision);
|
|
1508
1586
|
this.store.publishArtifact(stagedEvidence);
|
|
1587
|
+
this.store.publishArtifact(stagedResult);
|
|
1509
1588
|
return saved;
|
|
1510
1589
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
`Outcome: ${milestone.outcome}`,
|
|
1515
|
-
'',
|
|
1516
|
-
`Validated base: ${milestone.baseBranch}@${baseHead}`,
|
|
1517
|
-
'',
|
|
1518
|
-
'All required memberships are merged and all Milestone checks passed.',
|
|
1519
|
-
'',
|
|
1520
|
-
].join('\n');
|
|
1521
|
-
const stagedResult = this.store.stageArtifact(root, 'result.md', result);
|
|
1522
|
-
const saved = this.store.writeMilestone({
|
|
1523
|
-
...milestone,
|
|
1524
|
-
status: 'awaiting_final_acceptance',
|
|
1525
|
-
evidenceHash: stagedEvidence.hash,
|
|
1526
|
-
resultHash: stagedResult.hash,
|
|
1527
|
-
}, expectedRevision);
|
|
1528
|
-
this.store.publishArtifact(stagedEvidence);
|
|
1529
|
-
this.store.publishArtifact(stagedResult);
|
|
1530
|
-
return saved;
|
|
1590
|
+
finally {
|
|
1591
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1592
|
+
}
|
|
1531
1593
|
}
|
|
1532
1594
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
1533
|
-
const { identity } = this.#mutationContext(repository);
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1595
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1596
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1597
|
+
try {
|
|
1598
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1599
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1600
|
+
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
1601
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
|
|
1602
|
+
}
|
|
1603
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1604
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
|
|
1605
|
+
}
|
|
1606
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1607
|
+
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
1608
|
+
const currentHead = headCommit(identity.repositoryRoot);
|
|
1609
|
+
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
1610
|
+
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
1611
|
+
if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
|
|
1612
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
|
|
1613
|
+
}
|
|
1614
|
+
const normalizedActor = actor.trim();
|
|
1615
|
+
if (!normalizedActor) {
|
|
1616
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
|
|
1617
|
+
}
|
|
1618
|
+
const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
|
|
1619
|
+
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
1620
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1621
|
+
milestoneId,
|
|
1622
|
+
revision: milestone.revision,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
1626
|
+
const event = {
|
|
1627
|
+
kind: 'final_acceptance',
|
|
1628
|
+
decision: 'approved',
|
|
1629
|
+
actor: normalizedActor,
|
|
1630
|
+
recordedAt: this.now().toISOString(),
|
|
1631
|
+
briefHash: null,
|
|
1632
|
+
planHash: milestone.planHash,
|
|
1633
|
+
resultHash,
|
|
1634
|
+
evidenceHash,
|
|
1635
|
+
headCommit: currentHead,
|
|
1636
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
1637
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1638
|
+
...(delegation ? { delegation } : {}),
|
|
1639
|
+
};
|
|
1640
|
+
return this.store.writeMilestone({
|
|
1641
|
+
...milestone,
|
|
1642
|
+
status: 'accepted',
|
|
1643
|
+
resultHash,
|
|
1644
|
+
evidenceHash,
|
|
1645
|
+
acceptedHead: currentHead,
|
|
1646
|
+
authorizations: [...milestone.authorizations, event],
|
|
1647
|
+
}, expectedRevision);
|
|
1553
1648
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1557
|
-
milestoneId,
|
|
1558
|
-
revision: milestone.revision,
|
|
1559
|
-
});
|
|
1649
|
+
finally {
|
|
1650
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1560
1651
|
}
|
|
1561
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
1562
|
-
const event = {
|
|
1563
|
-
kind: 'final_acceptance',
|
|
1564
|
-
decision: 'approved',
|
|
1565
|
-
actor: normalizedActor,
|
|
1566
|
-
recordedAt: this.now().toISOString(),
|
|
1567
|
-
briefHash: null,
|
|
1568
|
-
planHash: milestone.planHash,
|
|
1569
|
-
resultHash,
|
|
1570
|
-
evidenceHash,
|
|
1571
|
-
headCommit: currentHead,
|
|
1572
|
-
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
1573
|
-
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1574
|
-
...(delegation ? { delegation } : {}),
|
|
1575
|
-
};
|
|
1576
|
-
return this.store.writeMilestone({
|
|
1577
|
-
...milestone,
|
|
1578
|
-
status: 'accepted',
|
|
1579
|
-
resultHash,
|
|
1580
|
-
evidenceHash,
|
|
1581
|
-
acceptedHead: currentHead,
|
|
1582
|
-
authorizations: [...milestone.authorizations, event],
|
|
1583
|
-
}, expectedRevision);
|
|
1584
1652
|
}
|
|
1585
1653
|
cancelMilestone(repository, milestoneId, expectedRevision, reason) {
|
|
1586
1654
|
if (!reason.trim())
|
|
1587
1655
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
1588
|
-
const { identity } = this.#mutationContext(repository);
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1656
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1657
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1658
|
+
try {
|
|
1659
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1660
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1661
|
+
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
1662
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
1663
|
+
}
|
|
1664
|
+
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1665
|
+
}
|
|
1666
|
+
finally {
|
|
1667
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1593
1668
|
}
|
|
1594
|
-
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1595
1669
|
}
|
|
1596
1670
|
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
1597
1671
|
validatePlan(plan);
|
|
@@ -1600,6 +1674,10 @@ export class WorkflowService {
|
|
|
1600
1674
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
1601
1675
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1602
1676
|
assertExpectedRevision(task, expectedRevision);
|
|
1677
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
|
|
1678
|
+
const preExecutionReplanAudits = preExecutionReplan
|
|
1679
|
+
? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
|
|
1680
|
+
: null;
|
|
1603
1681
|
const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
|
|
1604
1682
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
1605
1683
|
? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
|
|
@@ -1746,6 +1824,9 @@ export class WorkflowService {
|
|
|
1746
1824
|
}
|
|
1747
1825
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1748
1826
|
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
1827
|
+
const changesPlanSemantics = !task.planHash
|
|
1828
|
+
|| normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
|
|
1829
|
+
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
|
|
1749
1830
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
1750
1831
|
? { ...authorization, decision: 'superseded' }
|
|
1751
1832
|
: authorization);
|
|
@@ -1763,6 +1844,83 @@ export class WorkflowService {
|
|
|
1763
1844
|
...(task.invalidatedStepCommits ?? []),
|
|
1764
1845
|
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1765
1846
|
])];
|
|
1847
|
+
if (preExecutionReplan && preExecutionReplanAudits) {
|
|
1848
|
+
const replanC1Posture = readTaskC1Posture(this.store, task);
|
|
1849
|
+
const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
1850
|
+
if (replanC1Posture.state !== 'none' || replanWriterLease) {
|
|
1851
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
|
|
1852
|
+
taskId: task.id,
|
|
1853
|
+
c1Posture: replanC1Posture.state,
|
|
1854
|
+
writerLeasePresent: replanWriterLease !== null,
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
if (!planRiskAudit) {
|
|
1858
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
|
|
1859
|
+
}
|
|
1860
|
+
if (task.planObjective !== plan.objective
|
|
1861
|
+
|| canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
|
|
1862
|
+
|| canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
|
|
1863
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
|
|
1864
|
+
taskId: task.id,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
if (!changesPlanSemantics) {
|
|
1868
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
|
|
1869
|
+
}
|
|
1870
|
+
const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
|
|
1871
|
+
const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
|
|
1872
|
+
if (missingStepIds.length > 0) {
|
|
1873
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
|
|
1874
|
+
taskId: task.id,
|
|
1875
|
+
missingStepIds,
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
|
|
1879
|
+
const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
|
|
1880
|
+
if (lostCompletedStepIds.length > 0) {
|
|
1881
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
|
|
1882
|
+
taskId: task.id,
|
|
1883
|
+
lostCompletedStepIds,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const weakenedForbiddenScope = task.steps.flatMap((step) => {
|
|
1887
|
+
const candidate = candidateSteps.get(step.id);
|
|
1888
|
+
const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
|
|
1889
|
+
return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
|
|
1890
|
+
});
|
|
1891
|
+
if (weakenedForbiddenScope.length > 0) {
|
|
1892
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
|
|
1893
|
+
taskId: task.id,
|
|
1894
|
+
weakenedForbiddenScope,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
|
|
1898
|
+
if (missingRisks.length > 0) {
|
|
1899
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
|
|
1900
|
+
taskId: task.id,
|
|
1901
|
+
missingRisks,
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
|
|
1905
|
+
const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
|
|
1906
|
+
const missingProductionEdges = (task.productionEdges ?? [])
|
|
1907
|
+
.filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
|
|
1908
|
+
if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
|
|
1909
|
+
|| missingProductionEdges.length > 0) {
|
|
1910
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
|
|
1911
|
+
taskId: task.id,
|
|
1912
|
+
missingProductionEdges,
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
|
|
1916
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
|
|
1917
|
+
taskId: task.id,
|
|
1918
|
+
expectedHead: preExecutionReplan.sourceHeadCommit,
|
|
1919
|
+
actualHead: headCommit(identity.repositoryRoot),
|
|
1920
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1766
1924
|
if (task.status === 'needs_fix' &&
|
|
1767
1925
|
task.review.status === 'failed' &&
|
|
1768
1926
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
@@ -1786,7 +1944,7 @@ export class WorkflowService {
|
|
|
1786
1944
|
: []),
|
|
1787
1945
|
...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
|
|
1788
1946
|
...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
|
|
1789
|
-
...(
|
|
1947
|
+
...(!changesPlanSemantics
|
|
1790
1948
|
? ['task.rolling-replan.semantic-plan-change']
|
|
1791
1949
|
: []),
|
|
1792
1950
|
...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
|
|
@@ -1859,6 +2017,61 @@ export class WorkflowService {
|
|
|
1859
2017
|
};
|
|
1860
2018
|
if (planRiskAudit)
|
|
1861
2019
|
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
2020
|
+
if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
|
|
2021
|
+
const now = this.now();
|
|
2022
|
+
const resultTask = {
|
|
2023
|
+
...candidate,
|
|
2024
|
+
revision: task.revision + 1,
|
|
2025
|
+
updatedAt: now.toISOString(),
|
|
2026
|
+
};
|
|
2027
|
+
const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
|
|
2028
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
2029
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2030
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2031
|
+
const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
|
|
2032
|
+
if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
|
|
2033
|
+
throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
|
|
2034
|
+
taskId: task.id,
|
|
2035
|
+
planHash: preparedPlan.planHash,
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
applyProjectTransaction({
|
|
2039
|
+
projectRoot,
|
|
2040
|
+
projectId: task.projectId,
|
|
2041
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2042
|
+
kind: 'preexecution-replacement-plan-set',
|
|
2043
|
+
now,
|
|
2044
|
+
targets: [
|
|
2045
|
+
{ path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
2046
|
+
{ path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
|
|
2047
|
+
{ path: relative(versionFile), content: preparedPlan.planBytes },
|
|
2048
|
+
{ path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
2049
|
+
],
|
|
2050
|
+
...(this.transactionHooks.afterPreExecutionReplanTargetWrite
|
|
2051
|
+
? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
|
|
2052
|
+
: {}),
|
|
2053
|
+
validate: () => {
|
|
2054
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2055
|
+
const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
|
|
2056
|
+
if (saved.revision !== resultTask.revision
|
|
2057
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
2058
|
+
|| saved.planHash !== preparedPlan.planHash
|
|
2059
|
+
|| this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
|
|
2060
|
+
|| sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
|
|
2061
|
+
|| currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
|
|
2062
|
+
|| hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
|
|
2063
|
+
|| readCurrentPreExecutionReplan(this.store, saved) !== null
|
|
2064
|
+
|| saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
|
|
2065
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
|
|
2066
|
+
taskId: task.id,
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
if (saved.baseCommit)
|
|
2070
|
+
validateTaskHistory(identity.repositoryRoot, saved);
|
|
2071
|
+
},
|
|
2072
|
+
});
|
|
2073
|
+
return this.store.readTask(task.projectId, task.id);
|
|
2074
|
+
}
|
|
1862
2075
|
const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
|
|
1863
2076
|
if (stagedPlan.hash !== preparedPlan.planHash) {
|
|
1864
2077
|
throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
|
|
@@ -1987,11 +2200,15 @@ export class WorkflowService {
|
|
|
1987
2200
|
}
|
|
1988
2201
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
1989
2202
|
const transitions = [];
|
|
1990
|
-
const
|
|
2203
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2204
|
+
const currentPlanRiskAudit = planningRecovery?.current ?? readCurrentPlanRiskAudit(this.store, task);
|
|
1991
2205
|
const currentPlanArtifactHash = task.planHash
|
|
1992
2206
|
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
|
|
1993
2207
|
: null;
|
|
1994
2208
|
const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
|
|
2209
|
+
if (planningRecovery && refreshAssessment.mode !== 'delegated-content-only') {
|
|
2210
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stopped planning context refresh permits content-only Knowledge drift.');
|
|
2211
|
+
}
|
|
1995
2212
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
1996
2213
|
// leave the composite operation half-applied.
|
|
1997
2214
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
@@ -2038,11 +2255,15 @@ export class WorkflowService {
|
|
|
2038
2255
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
2039
2256
|
}
|
|
2040
2257
|
transitions.push('task knowledge-rebind');
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2258
|
+
// A stopped Plan may regain its Knowledge binding, never execution authority.
|
|
2259
|
+
if (!planningRecovery)
|
|
2260
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
|
|
2261
|
+
? 'Delegated Plan-bounded supporting-source context refresh.'
|
|
2262
|
+
: 'Delegated content-only context refresh.', delegationGrantId);
|
|
2263
|
+
if (!planningRecovery)
|
|
2264
|
+
transitions.push('task execution authorize');
|
|
2045
2265
|
return {
|
|
2266
|
+
authority: planningRecovery ? 'planning-only' : 'execution',
|
|
2046
2267
|
mode: refreshAssessment.mode,
|
|
2047
2268
|
transitions,
|
|
2048
2269
|
addedSources: refreshAssessment.addedSources,
|
|
@@ -2052,8 +2273,16 @@ export class WorkflowService {
|
|
|
2052
2273
|
},
|
|
2053
2274
|
validate: (result) => {
|
|
2054
2275
|
const currentTask = this.store.readTask(identity.projectId, task.id);
|
|
2055
|
-
if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
|
|
2056
|
-
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit
|
|
2276
|
+
if (currentTask.revision !== result.task.revision || currentTask.status !== (planningRecovery ? 'awaiting_execution_authorization' : 'ready')) {
|
|
2277
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit the expected Task posture.');
|
|
2278
|
+
}
|
|
2279
|
+
if (planningRecovery) {
|
|
2280
|
+
const rebound = readFailedStepPlanningRecovery(this.store, currentTask);
|
|
2281
|
+
if (!rebound || rebound.baseline.eventHash !== planningRecovery.baseline.eventHash
|
|
2282
|
+
|| JSON.stringify(currentTask.steps) !== JSON.stringify(task.steps)
|
|
2283
|
+
|| JSON.stringify(currentTask.authorizations) !== JSON.stringify(task.authorizations)) {
|
|
2284
|
+
throw new WorkflowError('STATE_CORRUPT', 'Planning context refresh changed retained recovery evidence.');
|
|
2285
|
+
}
|
|
2057
2286
|
}
|
|
2058
2287
|
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
2059
2288
|
if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
|
|
@@ -2067,6 +2296,9 @@ export class WorkflowService {
|
|
|
2067
2296
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2068
2297
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2069
2298
|
assertExpectedRevision(task, expectedRevision);
|
|
2299
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2300
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
|
|
2301
|
+
}
|
|
2070
2302
|
if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
|
|
2071
2303
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
|
|
2072
2304
|
}
|
|
@@ -2124,9 +2356,143 @@ export class WorkflowService {
|
|
|
2124
2356
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2125
2357
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2126
2358
|
assertExpectedRevision(task, expectedRevision);
|
|
2359
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2360
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires task plan-set; the blocking audit cannot be replaced in place.', { taskId, requiredAction: 'task plan-set' });
|
|
2361
|
+
}
|
|
2127
2362
|
this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
2128
2363
|
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
2129
2364
|
}
|
|
2365
|
+
recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
|
|
2366
|
+
const context = this.#mutationContext(repository);
|
|
2367
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2368
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2369
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2370
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2371
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
|
|
2372
|
+
taskId,
|
|
2373
|
+
requiredAction: 'task plan-set',
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
|
|
2377
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
|
|
2378
|
+
taskId,
|
|
2379
|
+
status: task.status,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
2383
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
|
|
2384
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2388
|
+
const writerLease = context.locks.inspect(task.id);
|
|
2389
|
+
if (c1Posture.state !== 'none' || writerLease) {
|
|
2390
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
|
|
2391
|
+
taskId,
|
|
2392
|
+
c1Posture: c1Posture.state,
|
|
2393
|
+
writerLeasePresent: writerLease !== null,
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2397
|
+
if (task.baseCommit) {
|
|
2398
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
2399
|
+
}
|
|
2400
|
+
else if (task.workspaceOwner !== null || task.taskBranch !== null
|
|
2401
|
+
|| task.steps.some((step) => step.status === 'completed')) {
|
|
2402
|
+
throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
|
|
2403
|
+
taskId,
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2407
|
+
const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
|
|
2408
|
+
if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
|
|
2409
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
|
|
2410
|
+
}
|
|
2411
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2412
|
+
const actualBinding = {
|
|
2413
|
+
expectedBriefHash: task.briefHash,
|
|
2414
|
+
expectedPlanHash: task.planHash,
|
|
2415
|
+
expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
|
|
2416
|
+
expectedHeadCommit: currentHead,
|
|
2417
|
+
};
|
|
2418
|
+
if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
|
|
2419
|
+
throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
|
|
2420
|
+
expected: binding,
|
|
2421
|
+
actual: actualBinding,
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
if (input.decision !== 'stop-escalate') {
|
|
2425
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
|
|
2426
|
+
}
|
|
2427
|
+
if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
|
|
2428
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
|
|
2429
|
+
actor: actor.trim(),
|
|
2430
|
+
auditor: input.auditor.trim(),
|
|
2431
|
+
expectedPlanner: sourceAudit.audit.planner,
|
|
2432
|
+
actualPlanner: input.planner.trim(),
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
|
|
2436
|
+
const now = this.now();
|
|
2437
|
+
const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
|
|
2438
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
2439
|
+
const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
|
|
2440
|
+
sourceAudit: sourceAudit.audit,
|
|
2441
|
+
obstructionAudit: preparedAudit.event,
|
|
2442
|
+
authorization,
|
|
2443
|
+
sourceHeadCommit: currentHead,
|
|
2444
|
+
actor,
|
|
2445
|
+
reason,
|
|
2446
|
+
}, now);
|
|
2447
|
+
const resultTask = {
|
|
2448
|
+
...task,
|
|
2449
|
+
revision: task.revision + 1,
|
|
2450
|
+
updatedAt: now.toISOString(),
|
|
2451
|
+
status: 'awaiting_execution_authorization',
|
|
2452
|
+
authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
|
|
2453
|
+
? { ...candidate, decision: 'superseded' }
|
|
2454
|
+
: candidate),
|
|
2455
|
+
blockReason: reason.trim(),
|
|
2456
|
+
};
|
|
2457
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2458
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2459
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2460
|
+
applyProjectTransaction({
|
|
2461
|
+
projectRoot,
|
|
2462
|
+
projectId: task.projectId,
|
|
2463
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2464
|
+
kind: 'preexecution-plan-authority-withdrawal',
|
|
2465
|
+
now,
|
|
2466
|
+
targets: [
|
|
2467
|
+
{ path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
2468
|
+
{ path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
|
|
2469
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
2470
|
+
],
|
|
2471
|
+
validate: () => {
|
|
2472
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2473
|
+
const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
|
|
2474
|
+
const marker = readCurrentPreExecutionReplan(this.store, saved);
|
|
2475
|
+
if (saved.revision !== resultTask.revision
|
|
2476
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
2477
|
+
|| saved.planHash !== task.planHash
|
|
2478
|
+
|| latestAudit?.eventHash !== preparedAudit.event.eventHash
|
|
2479
|
+
|| marker?.eventHash !== preparedMarker.event.eventHash
|
|
2480
|
+
|| saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
|
|
2481
|
+
|| hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
|
|
2482
|
+
|| headCommit(context.identity.repositoryRoot) !== currentHead
|
|
2483
|
+
|| !isClean(context.identity.repositoryRoot)) {
|
|
2484
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
|
|
2485
|
+
}
|
|
2486
|
+
if (saved.baseCommit)
|
|
2487
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
2488
|
+
},
|
|
2489
|
+
});
|
|
2490
|
+
return {
|
|
2491
|
+
task: this.store.readTask(task.projectId, task.id),
|
|
2492
|
+
audit: preparedAudit.event,
|
|
2493
|
+
event: preparedMarker.event,
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2130
2496
|
showTaskHandoff(repository, taskId) {
|
|
2131
2497
|
const { identity } = this.#mutationContext(repository);
|
|
2132
2498
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
@@ -2140,6 +2506,12 @@ export class WorkflowService {
|
|
|
2140
2506
|
const context = this.#mutationContext(repository);
|
|
2141
2507
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2142
2508
|
assertExpectedRevision(task, expectedRevision);
|
|
2509
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2510
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
|
|
2511
|
+
taskId,
|
|
2512
|
+
requiredAction: 'task plan-set',
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2143
2515
|
const normalizedActor = actor.trim();
|
|
2144
2516
|
if (!normalizedActor) {
|
|
2145
2517
|
throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
|
|
@@ -2295,8 +2667,12 @@ export class WorkflowService {
|
|
|
2295
2667
|
});
|
|
2296
2668
|
}
|
|
2297
2669
|
}
|
|
2298
|
-
const
|
|
2299
|
-
const
|
|
2670
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2671
|
+
const navigation = planningRecovery
|
|
2672
|
+
? this.nextForTaskWithDelegation(task.projectId, context.identity.repositoryRoot, task, planningRecovery.current)
|
|
2673
|
+
: this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
2674
|
+
const action = String(navigation.action === 'task writer-credential-replace'
|
|
2675
|
+
? navigation.blockedAction : navigation.action);
|
|
2300
2676
|
const currentLease = context.locks.inspect(taskId);
|
|
2301
2677
|
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
2302
2678
|
const requiredActor = writerTokenContract
|
|
@@ -2469,11 +2845,15 @@ export class WorkflowService {
|
|
|
2469
2845
|
const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2470
2846
|
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2471
2847
|
: null;
|
|
2848
|
+
const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2849
|
+
? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2850
|
+
: null;
|
|
2472
2851
|
if (step.status === 'planned'
|
|
2473
2852
|
&& dirty.length > 0
|
|
2474
2853
|
&& !resumesCorrectiveReplan
|
|
2475
2854
|
&& !downstreamProofCarryover?.eligible
|
|
2476
|
-
&& !planIntegrityDirtyCarryover?.eligible
|
|
2855
|
+
&& !planIntegrityDirtyCarryover?.eligible
|
|
2856
|
+
&& !rootCauseReplanDirtyCarryover?.eligible) {
|
|
2477
2857
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2478
2858
|
changedFiles: dirty,
|
|
2479
2859
|
...(downstreamProofCarryover?.applicable
|
|
@@ -2482,6 +2862,9 @@ export class WorkflowService {
|
|
|
2482
2862
|
...(planIntegrityDirtyCarryover?.applicable
|
|
2483
2863
|
? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
|
|
2484
2864
|
: {}),
|
|
2865
|
+
...(rootCauseReplanDirtyCarryover?.applicable
|
|
2866
|
+
? { rootCauseReplanDirtyCarryoverBlockers: rootCauseReplanDirtyCarryover.blockers }
|
|
2867
|
+
: {}),
|
|
2485
2868
|
});
|
|
2486
2869
|
}
|
|
2487
2870
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
@@ -2747,7 +3130,7 @@ export class WorkflowService {
|
|
|
2747
3130
|
const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
|
|
2748
3131
|
? stepHistory
|
|
2749
3132
|
: [...stepHistory, preparedRemediation.event];
|
|
2750
|
-
const rollingCauseDecision =
|
|
3133
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
2751
3134
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2752
3135
|
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
|
|
2753
3136
|
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
@@ -2970,7 +3353,15 @@ export class WorkflowService {
|
|
|
2970
3353
|
throw new WorkflowError('TRANSITION_BLOCKED', `External Strict Step Review requires ${stepId} in progress with canonical evidence.`);
|
|
2971
3354
|
}
|
|
2972
3355
|
validateTaskHistory(identity.repositoryRoot, task);
|
|
2973
|
-
|
|
3356
|
+
const transport = verifiedPendingReviewTransport(identity.repositoryRoot, task, stepId, this.store.taskRoot(task.projectId, task.id));
|
|
3357
|
+
if (transport) {
|
|
3358
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
3359
|
+
if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== transport.source.pendingEventHash
|
|
3360
|
+
|| cycle.binding.evidenceHash !== transport.source.evidenceHash) {
|
|
3361
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The transported strict review must retain its exact pending chain.');
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (!isClean(identity.repositoryRoot) || (headCommit(identity.repositoryRoot) !== step.evidence.commitSha && !transport)) {
|
|
2974
3365
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'External Strict Step Review requires a clean checkout at the completion commit.');
|
|
2975
3366
|
}
|
|
2976
3367
|
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
@@ -3053,7 +3444,7 @@ export class WorkflowService {
|
|
|
3053
3444
|
const resultingHistory = preparedRemediation.postimage === null
|
|
3054
3445
|
? stepHistory
|
|
3055
3446
|
: [...stepHistory, preparedRemediation.event];
|
|
3056
|
-
const rollingCauseDecision =
|
|
3447
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
3057
3448
|
const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
|
|
3058
3449
|
const paused = {
|
|
3059
3450
|
...resolved,
|
|
@@ -3077,7 +3468,7 @@ export class WorkflowService {
|
|
|
3077
3468
|
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
3078
3469
|
actor: c1Posture.claimant,
|
|
3079
3470
|
writerToken: reviewLease.token,
|
|
3080
|
-
reason: `Yield execution authority after
|
|
3471
|
+
reason: `Yield execution authority after authoritative replan cause ${preparedRemediation.event.eventId}.`,
|
|
3081
3472
|
eventId: `HOF-${sha256Hex([
|
|
3082
3473
|
'strict-review-root-cause-replan-pause-v1',
|
|
3083
3474
|
task.id,
|
|
@@ -3346,10 +3737,14 @@ export class WorkflowService {
|
|
|
3346
3737
|
assertExpectedRevision(task, expectedRevision);
|
|
3347
3738
|
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
3348
3739
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
3349
|
-
|
|
3740
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
3741
|
+
if (!['ready', 'needs_fix', 'blocked'].includes(task.status) && !planningRecovery) {
|
|
3350
3742
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
3351
3743
|
}
|
|
3352
|
-
|
|
3744
|
+
if (planningRecovery && correctiveAudit.decision === 'continue-fix') {
|
|
3745
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A stopped rebound Plan requires replan, split, or escalation; continue-fix cannot restore execution authority.', { taskId, stepId });
|
|
3746
|
+
}
|
|
3747
|
+
const planRiskAudit = readCorrectivePlanningAudit(this.store, task);
|
|
3353
3748
|
if (!planRiskAudit) {
|
|
3354
3749
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
|
|
3355
3750
|
taskId,
|
|
@@ -3640,22 +4035,153 @@ export class WorkflowService {
|
|
|
3640
4035
|
const { identity } = this.#mutationContext(repository);
|
|
3641
4036
|
return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
|
|
3642
4037
|
}
|
|
3643
|
-
applyAdoption(repository, actor, confirmationCode) {
|
|
4038
|
+
applyAdoption(repository, actor, confirmationCode) {
|
|
4039
|
+
const context = this.#mutationContext(repository);
|
|
4040
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
4041
|
+
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
4042
|
+
return applyAdoptionPosture(this.store, {
|
|
4043
|
+
projectId: context.identity.projectId,
|
|
4044
|
+
repositoryIdentity: context.identity,
|
|
4045
|
+
packageVersion: PACKAGE_VERSION,
|
|
4046
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
4047
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
4048
|
+
tasks,
|
|
4049
|
+
milestones,
|
|
4050
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
4051
|
+
blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
|
|
4052
|
+
now: this.now(),
|
|
4053
|
+
}, actor, confirmationCode);
|
|
4054
|
+
}
|
|
4055
|
+
pendingReviewUpdatePreflight(repository, taskId) {
|
|
4056
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4057
|
+
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4058
|
+
try {
|
|
4059
|
+
pendingReviewRunnerFingerprint();
|
|
4060
|
+
}
|
|
4061
|
+
catch (error) {
|
|
4062
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4063
|
+
}
|
|
4064
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4065
|
+
let binding = null;
|
|
4066
|
+
if (PACKAGE_VERSION === PENDING_REVIEW_SOURCE_VERSION)
|
|
4067
|
+
blockers.push('The exact target runner must be newer than the supported source.');
|
|
4068
|
+
if (!task)
|
|
4069
|
+
blockers.push('Task not found.');
|
|
4070
|
+
if (task) {
|
|
4071
|
+
try {
|
|
4072
|
+
binding = pendingReviewSourceBinding(snapshot.identity.repositoryRoot, this.store, task);
|
|
4073
|
+
}
|
|
4074
|
+
catch (error) {
|
|
4075
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4076
|
+
}
|
|
4077
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4078
|
+
if ([versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4079
|
+
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PENDING_REVIEW_SOURCE_VERSION)) {
|
|
4080
|
+
blockers.push('All source version surfaces must equal the exact supported beta.13.1.');
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
const otherRunningSteps = snapshot.tasks.flatMap((candidate) => candidate.steps.filter((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== binding?.stepId)));
|
|
4084
|
+
if (otherRunningSteps.length)
|
|
4085
|
+
blockers.push('No other Step may be running.');
|
|
4086
|
+
const stale = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= this.now().getTime());
|
|
4087
|
+
if (snapshot.leases.some((lease) => !stale.includes(lease)))
|
|
4088
|
+
blockers.push('No live writer lease may exist.');
|
|
4089
|
+
if (stale.some((lease) => lease.entityId !== taskId))
|
|
4090
|
+
blockers.push('No unrelated stale writer lease may exist.');
|
|
4091
|
+
const staleLeaseRepair = blockers.length === 0 && binding
|
|
4092
|
+
? stale.map((lease) => ({ action: 'locks repair', entityId: lease.entityId })) : [];
|
|
4093
|
+
if (stale.length)
|
|
4094
|
+
blockers.push('Repair the exact stale Task lease with the installed source locks repair, then rerun source preflight.');
|
|
4095
|
+
return { readOnly: true, eligible: blockers.length === 0 && binding !== null, binding,
|
|
4096
|
+
bindingHash: binding ? pendingReviewBindingHash(binding) : null, blockers, staleLeaseRepair };
|
|
4097
|
+
}
|
|
4098
|
+
pendingReviewDependencyPreflight(repository, taskId, source) {
|
|
4099
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4100
|
+
const blockers = observationPreflightBlockers(snapshot.observation);
|
|
4101
|
+
try {
|
|
4102
|
+
pendingReviewRunnerFingerprint();
|
|
4103
|
+
}
|
|
4104
|
+
catch (error) {
|
|
4105
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4106
|
+
}
|
|
4107
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4108
|
+
const taskHead = headCommit(snapshot.identity.repositoryRoot);
|
|
4109
|
+
let baseTransportCommit = null;
|
|
4110
|
+
if (snapshot.leases.length)
|
|
4111
|
+
blockers.push('No live or stale writer lease may exist.');
|
|
4112
|
+
if (!task)
|
|
4113
|
+
blockers.push('Task not found.');
|
|
4114
|
+
if (task && existsSync(path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)))
|
|
4115
|
+
blockers.push('A pending review update is already recorded.');
|
|
4116
|
+
if (task) {
|
|
4117
|
+
try {
|
|
4118
|
+
// Receipt fields are claims, not authority. Reconstruct every source binding from
|
|
4119
|
+
// canonical Task/sidecars and the exact parents of the candidate Git commits.
|
|
4120
|
+
const parent = runGit(repository, ['rev-parse', 'HEAD^']);
|
|
4121
|
+
baseTransportCommit = runGit(repository, ['rev-parse', task.baseBranch]);
|
|
4122
|
+
const baseParent = runGit(repository, ['rev-parse', `${baseTransportCommit}^`]);
|
|
4123
|
+
const reconstructed = pendingReviewSourceBinding(repository, this.store, task, parent, baseParent);
|
|
4124
|
+
if (canonicalJsonStringify(reconstructed) !== canonicalJsonStringify(source)) {
|
|
4125
|
+
throw new Error('Source receipt does not match reconstructed Task/revision/Plan/HEAD/pending bindings.');
|
|
4126
|
+
}
|
|
4127
|
+
assertPendingReviewTransport(repository, reconstructed, taskHead, baseTransportCommit);
|
|
4128
|
+
if (snapshot.tasks.some((candidate) => candidate.steps.some((step) => step.status === 'in_progress' && (candidate.id !== taskId || step.id !== source.stepId)))) {
|
|
4129
|
+
throw new Error('No other Step may be running.');
|
|
4130
|
+
}
|
|
4131
|
+
const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
|
|
4132
|
+
if ([versions.runtime, versions.declared, versions.locked, versions.installed, versions.currentBranch,
|
|
4133
|
+
...versions.milestoneBases.map((base) => base.version)].some((version) => version !== PACKAGE_VERSION)) {
|
|
4134
|
+
throw new Error('All target version surfaces must equal the exact target runner.');
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
catch (error) {
|
|
4138
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return { readOnly: true, eligible: blockers.length === 0, blockers, taskHead, baseTransportCommit };
|
|
4142
|
+
}
|
|
4143
|
+
recoverPendingReviewDependency(repository, taskId, expectedRevision, source, actor, reason) {
|
|
4144
|
+
if (!actor.trim() || !reason.trim())
|
|
4145
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Actor and reason are required.');
|
|
4146
|
+
const initial = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4147
|
+
if (!initial.eligible)
|
|
4148
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight failed.', initial);
|
|
3644
4149
|
const context = this.#mutationContext(repository);
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
4150
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
4151
|
+
assertExpectedRevision(task, expectedRevision);
|
|
4152
|
+
const preflight = this.pendingReviewDependencyPreflight(repository, taskId, source);
|
|
4153
|
+
if (!preflight.eligible || !preflight.baseTransportCommit) {
|
|
4154
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending review dependency preflight changed.', preflight);
|
|
4155
|
+
}
|
|
4156
|
+
const now = this.now();
|
|
4157
|
+
const nextTask = { ...task, revision: task.revision + 1, updatedAt: now.toISOString(),
|
|
4158
|
+
systemCommits: [...task.systemCommits, preflight.taskHead],
|
|
4159
|
+
dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
|
|
4160
|
+
commitSha: preflight.taskHead, parentCommitSha: source.completionCommit,
|
|
4161
|
+
packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, files: ['package-lock.json', 'package.json'],
|
|
4162
|
+
actor: actor.trim(), reason: reason.trim(), recordedAt: now.toISOString(),
|
|
4163
|
+
}] };
|
|
4164
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
4165
|
+
const event = preparePendingReviewUpdateEvent({ source, baseTransportCommit: preflight.baseTransportCommit }, preflight.taskHead, now.toISOString());
|
|
4166
|
+
applyProjectTransaction({ projectRoot, projectId: task.projectId,
|
|
4167
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
4168
|
+
kind: 'pending-review-dependency-recovery', now,
|
|
4169
|
+
targets: [{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), 'state.json')).split(path.sep).join('/'),
|
|
4170
|
+
content: `${JSON.stringify(nextTask, null, 2)}\n` },
|
|
4171
|
+
{ path: path.relative(projectRoot, path.join(this.store.taskRoot(task.projectId, task.id), PENDING_REVIEW_UPDATE_SIDECAR)).split(path.sep).join('/'),
|
|
4172
|
+
content: `${canonicalJsonStringify(event)}\n` }],
|
|
4173
|
+
validate: () => {
|
|
4174
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
4175
|
+
if (saved.revision !== nextTask.revision || !verifiedPendingReviewTransport(repository, saved, source.stepId, this.store.taskRoot(task.projectId, task.id))) {
|
|
4176
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pending review dependency recovery readback failed.');
|
|
4177
|
+
}
|
|
4178
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, saved, source.stepId);
|
|
4179
|
+
if (cycle.resolution !== 'pending' || cycle.pendingEvent?.eventHash !== source.pendingEventHash) {
|
|
4180
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pending review changed during dependency recovery.');
|
|
4181
|
+
}
|
|
4182
|
+
},
|
|
4183
|
+
});
|
|
4184
|
+
return this.store.readTask(task.projectId, task.id);
|
|
3659
4185
|
}
|
|
3660
4186
|
updatePreflight(repository) {
|
|
3661
4187
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -3669,6 +4195,9 @@ export class WorkflowService {
|
|
|
3669
4195
|
const now = this.now().getTime();
|
|
3670
4196
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
3671
4197
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4198
|
+
const pendingPreExecutionReplanTaskIds = tasks
|
|
4199
|
+
.filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
|
|
4200
|
+
.map((task) => task.id);
|
|
3672
4201
|
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
3673
4202
|
const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
3674
4203
|
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
@@ -3696,6 +4225,9 @@ export class WorkflowService {
|
|
|
3696
4225
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
3697
4226
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
3698
4227
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
4228
|
+
...(pendingPreExecutionReplanTaskIds.length > 0
|
|
4229
|
+
? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
|
|
4230
|
+
: []),
|
|
3699
4231
|
...versionBlockers,
|
|
3700
4232
|
...observationPreflightBlockers(snapshot.observation),
|
|
3701
4233
|
];
|
|
@@ -3721,10 +4253,12 @@ export class WorkflowService {
|
|
|
3721
4253
|
milestoneBases,
|
|
3722
4254
|
},
|
|
3723
4255
|
dependencyTransport: {
|
|
3724
|
-
safe: versionBlockers.length === 0,
|
|
3725
|
-
requiredAction:
|
|
3726
|
-
?
|
|
3727
|
-
:
|
|
4256
|
+
safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
|
|
4257
|
+
requiredAction: pendingPreExecutionReplanTaskIds.length > 0
|
|
4258
|
+
? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
|
|
4259
|
+
: versionBlockers.length === 0
|
|
4260
|
+
? null
|
|
4261
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
3728
4262
|
},
|
|
3729
4263
|
adoption: snapshot.adoption,
|
|
3730
4264
|
observation: snapshot.observation,
|
|
@@ -3740,7 +4274,299 @@ export class WorkflowService {
|
|
|
3740
4274
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3741
4275
|
if (!task)
|
|
3742
4276
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3743
|
-
|
|
4277
|
+
const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
|
|
4278
|
+
const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
|
|
4279
|
+
const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
|
|
4280
|
+
if (!preflight.rootCauseReplan)
|
|
4281
|
+
return preflight;
|
|
4282
|
+
let baseTip = null;
|
|
4283
|
+
let baseDependency = null;
|
|
4284
|
+
try {
|
|
4285
|
+
baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4286
|
+
baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
|
|
4287
|
+
}
|
|
4288
|
+
catch {
|
|
4289
|
+
// The blocker below keeps a missing or unreadable base reference fail closed.
|
|
4290
|
+
}
|
|
4291
|
+
const bridgeBlockers = [
|
|
4292
|
+
...(preflight.dependencyCommits.length !== 1
|
|
4293
|
+
? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
|
|
4294
|
+
...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
|
|
4295
|
+
? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
|
|
4296
|
+
...(preflight.parentCommitSha
|
|
4297
|
+
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
|
|
4298
|
+
? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
|
|
4299
|
+
...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
|
|
4300
|
+
? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
|
|
4301
|
+
...(!baseDependency
|
|
4302
|
+
|| baseDependency.commitSha !== baseTip
|
|
4303
|
+
|| baseDependency.packageVersion !== PACKAGE_VERSION
|
|
4304
|
+
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
|
|
4305
|
+
!== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
|
|
4306
|
+
? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
|
|
4307
|
+
];
|
|
4308
|
+
return bridgeBlockers.length === 0
|
|
4309
|
+
? preflight
|
|
4310
|
+
: {
|
|
4311
|
+
...preflight,
|
|
4312
|
+
eligible: false,
|
|
4313
|
+
action: null,
|
|
4314
|
+
commitSha: null,
|
|
4315
|
+
parentCommitSha: null,
|
|
4316
|
+
blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4319
|
+
correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
|
|
4320
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4321
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4322
|
+
if (!task)
|
|
4323
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4324
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4325
|
+
const sourceVersion = '2.0.0-beta.13.6';
|
|
4326
|
+
const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
|
|
4327
|
+
const sourceSurfaces = {
|
|
4328
|
+
declared: versions.declared,
|
|
4329
|
+
locked: versions.locked,
|
|
4330
|
+
installed: versions.installed,
|
|
4331
|
+
currentBranch: versions.currentBranch,
|
|
4332
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4333
|
+
};
|
|
4334
|
+
const c1 = readTaskC1Posture(this.store, task);
|
|
4335
|
+
if (c1.state !== 'claimed') {
|
|
4336
|
+
return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
|
|
4337
|
+
}
|
|
4338
|
+
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4339
|
+
const now = this.now().getTime();
|
|
4340
|
+
const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
4341
|
+
const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4342
|
+
const transactionCount = snapshot.observation.pendingProjectTransactions.length
|
|
4343
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4344
|
+
+ snapshot.observation.pendingTaskTransactions.length;
|
|
4345
|
+
const authorizationBlockers = [];
|
|
4346
|
+
if (assessment.eligible) {
|
|
4347
|
+
try {
|
|
4348
|
+
this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
|
|
4349
|
+
}
|
|
4350
|
+
catch (error) {
|
|
4351
|
+
authorizationBlockers.push(error instanceof Error ? error.message : String(error));
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
const baseBlockers = [
|
|
4355
|
+
...assessment.blockers,
|
|
4356
|
+
...authorizationBlockers,
|
|
4357
|
+
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4358
|
+
...Object.entries(sourceSurfaces)
|
|
4359
|
+
.filter(([, version]) => version !== sourceVersion)
|
|
4360
|
+
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4361
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4362
|
+
...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
|
|
4363
|
+
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4364
|
+
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4365
|
+
...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
|
|
4366
|
+
? ['The exact original Worker C1 claim must remain current.'] : []),
|
|
4367
|
+
...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
|
|
4368
|
+
...(staleLeases.some((lease) => lease.entityId !== task.id
|
|
4369
|
+
|| c1.state !== 'claimed' || lease.owner !== c1.claimant
|
|
4370
|
+
|| createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
|
|
4371
|
+
? ['No unrelated or differently owned stale writer lease may exist.'] : []),
|
|
4372
|
+
...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
|
|
4373
|
+
];
|
|
4374
|
+
const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
|
|
4375
|
+
? staleLeases[0]
|
|
4376
|
+
: null;
|
|
4377
|
+
const blockers = [
|
|
4378
|
+
...baseBlockers,
|
|
4379
|
+
...(repairableStale
|
|
4380
|
+
? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
|
|
4381
|
+
: []),
|
|
4382
|
+
];
|
|
4383
|
+
const compatibility = assessment.compatibility;
|
|
4384
|
+
return {
|
|
4385
|
+
readOnly: true,
|
|
4386
|
+
eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
|
|
4387
|
+
action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
|
|
4388
|
+
? 'update corrective-carryover-transport'
|
|
4389
|
+
: null,
|
|
4390
|
+
projectId: task.projectId,
|
|
4391
|
+
taskId: task.id,
|
|
4392
|
+
taskRevision: task.revision,
|
|
4393
|
+
stepId: compatibility?.stepId ?? null,
|
|
4394
|
+
sourceVersion,
|
|
4395
|
+
targetVersion,
|
|
4396
|
+
sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
|
|
4397
|
+
taskBranch: task.taskBranch,
|
|
4398
|
+
baseBranch: task.baseBranch,
|
|
4399
|
+
dirtyFiles: compatibility?.dirtyFiles ?? [],
|
|
4400
|
+
dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
|
|
4401
|
+
compatibilityHash: compatibility?.compatibilityHash ?? null,
|
|
4402
|
+
c1Posture: c1.state === 'claimed' ? {
|
|
4403
|
+
state: 'claimed',
|
|
4404
|
+
claimant: c1.claimant,
|
|
4405
|
+
claimEventId: compatibility?.c1ClaimEventId ?? null,
|
|
4406
|
+
claimEventHash: compatibility?.c1ClaimEventHash ?? null,
|
|
4407
|
+
} : null,
|
|
4408
|
+
staleLeaseRepair: repairableStale ? [{
|
|
4409
|
+
action: 'locks repair',
|
|
4410
|
+
entityId: task.id,
|
|
4411
|
+
owner: repairableStale.owner,
|
|
4412
|
+
expiresAt: repairableStale.expiresAt,
|
|
4413
|
+
}] : [],
|
|
4414
|
+
requiredTransport: [
|
|
4415
|
+
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4416
|
+
'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
|
|
4417
|
+
'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
|
|
4418
|
+
],
|
|
4419
|
+
blockers,
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
|
|
4423
|
+
const repositoryRoot = snapshot.identity.repositoryRoot;
|
|
4424
|
+
const sourceVersion = '2.0.0-beta.13.6';
|
|
4425
|
+
const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
|
|
4426
|
+
const c1 = readTaskC1Posture(this.store, task);
|
|
4427
|
+
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
|
|
4428
|
+
const currentHead = headCommit(repositoryRoot);
|
|
4429
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
4430
|
+
const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
4431
|
+
const navigation = this.next(repositoryRoot, task.id);
|
|
4432
|
+
const action = String(navigation.action);
|
|
4433
|
+
const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
|
|
4434
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
4435
|
+
const terminal = readRemediationEvents(this.store, task)
|
|
4436
|
+
.filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
|
|
4437
|
+
const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
|
|
4438
|
+
const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
|
|
4439
|
+
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
|
|
4440
|
+
const missingClaimOnly = assessment.blockers.length === 1
|
|
4441
|
+
&& assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
|
|
4442
|
+
const rollingWave = navigation.rollingWave;
|
|
4443
|
+
const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
|
|
4444
|
+
&& rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
|
|
4445
|
+
|| (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
|
|
4446
|
+
|| (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
|
|
4447
|
+
&& c1.state === 'pending' && c1.targetActor === yielded?.actor);
|
|
4448
|
+
const blockers = [
|
|
4449
|
+
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4450
|
+
...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
|
|
4451
|
+
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4452
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4453
|
+
...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
|
|
4454
|
+
...(snapshot.observation.pendingProjectTransactions.length
|
|
4455
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4456
|
+
+ snapshot.observation.pendingTaskTransactions.length
|
|
4457
|
+
+ snapshot.observation.corruptTaskTransactions.length
|
|
4458
|
+
+ snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
|
|
4459
|
+
...(!recorded || recorded.stepId === stepId
|
|
4460
|
+
|| task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
|
|
4461
|
+
? ['A different, verified completed Step must own the historical carryover binding.'] : []),
|
|
4462
|
+
...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
|
|
4463
|
+
...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
|
|
4464
|
+
|| task.workspaceOwner !== 'local'
|
|
4465
|
+
|| currentBranch(repositoryRoot) !== task.taskBranch
|
|
4466
|
+
|| task.steps.some((candidate) => candidate.status === 'in_progress')
|
|
4467
|
+
? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
|
|
4468
|
+
...(!step || dirtyFiles.length === 0
|
|
4469
|
+
|| dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
|
|
4470
|
+
? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
|
|
4471
|
+
...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
|
|
4472
|
+
|| terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
|
|
4473
|
+
? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
|
|
4474
|
+
...(!yielded || !terminal || yielded.planHash === null
|
|
4475
|
+
|| !Number.isFinite(Date.parse(yielded.recordedAt))
|
|
4476
|
+
|| Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
|
|
4477
|
+
? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
|
|
4478
|
+
];
|
|
4479
|
+
let candidateBinding = null;
|
|
4480
|
+
if (action === 'task plan-set') {
|
|
4481
|
+
if (!candidate) {
|
|
4482
|
+
blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
|
|
4483
|
+
}
|
|
4484
|
+
else {
|
|
4485
|
+
try {
|
|
4486
|
+
validatePlan(candidate.plan);
|
|
4487
|
+
const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
|
|
4488
|
+
if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
|
|
4489
|
+
throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
|
|
4490
|
+
}
|
|
4491
|
+
const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
|
|
4492
|
+
if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
|
|
4493
|
+
throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
|
|
4494
|
+
}
|
|
4495
|
+
if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
|
|
4496
|
+
throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
|
|
4497
|
+
}
|
|
4498
|
+
if (candidate.plan.objective !== task.planObjective
|
|
4499
|
+
|| canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
|
|
4500
|
+
|| canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
|
|
4501
|
+
throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
|
|
4502
|
+
}
|
|
4503
|
+
const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
|
|
4504
|
+
this.validateKnowledgeTargets(map, candidate.plan);
|
|
4505
|
+
const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
|
|
4506
|
+
if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
|
|
4507
|
+
=== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
|
|
4508
|
+
throw new Error('Candidate must change the failed Plan semantics before source recovery.');
|
|
4509
|
+
}
|
|
4510
|
+
if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
|
|
4511
|
+
throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
|
|
4512
|
+
}
|
|
4513
|
+
const auditTask = {
|
|
4514
|
+
...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
|
|
4515
|
+
knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
|
|
4516
|
+
knowledgeMapHash: candidate.plan.knowledgeMapHash,
|
|
4517
|
+
steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
|
|
4518
|
+
...candidateStep, status: 'planned', evidence: null, failurePause: null,
|
|
4519
|
+
}),
|
|
4520
|
+
};
|
|
4521
|
+
validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
|
|
4522
|
+
candidateBinding = {
|
|
4523
|
+
candidatePlanHash: prepared.planHash,
|
|
4524
|
+
candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
|
|
4525
|
+
riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
|
|
4526
|
+
};
|
|
4527
|
+
}
|
|
4528
|
+
catch (error) {
|
|
4529
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
try {
|
|
4534
|
+
validateTaskHistory(repositoryRoot, task);
|
|
4535
|
+
this.assertTaskKnowledgeBinding(repositoryRoot, task);
|
|
4536
|
+
const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
|
|
4537
|
+
if (!sourceAudit || !yielded?.planHash || !task.planHash
|
|
4538
|
+
|| normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
|
|
4539
|
+
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
|
|
4540
|
+
throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
catch (error) {
|
|
4544
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4545
|
+
}
|
|
4546
|
+
const binding = {
|
|
4547
|
+
taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
|
|
4548
|
+
planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
|
|
4549
|
+
sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
|
|
4550
|
+
completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
|
|
4551
|
+
terminalRemediationEventHash: terminal?.eventHash ?? null,
|
|
4552
|
+
correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
|
|
4553
|
+
c1Posture: c1.state,
|
|
4554
|
+
candidateBinding,
|
|
4555
|
+
};
|
|
4556
|
+
return {
|
|
4557
|
+
readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
|
|
4558
|
+
action: null, transportAllowed: false,
|
|
4559
|
+
...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
|
|
4560
|
+
compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
|
|
4561
|
+
sourceRecovery: blockers.length === 0 ? {
|
|
4562
|
+
eligible: true, action,
|
|
4563
|
+
...(action === 'task plan-set' ? { ...candidateBinding } : {}),
|
|
4564
|
+
...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
|
|
4565
|
+
...(action === 'task claim' ? { actor: yielded.actor } : {}),
|
|
4566
|
+
instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
|
|
4567
|
+
} : null,
|
|
4568
|
+
blockers,
|
|
4569
|
+
};
|
|
3744
4570
|
}
|
|
3745
4571
|
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
3746
4572
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -4211,7 +5037,9 @@ export class WorkflowService {
|
|
|
4211
5037
|
? Boolean(record.historicalStepProvenance)
|
|
4212
5038
|
: recoveryMode === 'plan-integrity'
|
|
4213
5039
|
? Boolean(record.planIntegrity)
|
|
4214
|
-
:
|
|
5040
|
+
: recoveryMode === false && record.rootCauseReplan
|
|
5041
|
+
? true
|
|
5042
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
4215
5043
|
if (prior && current.systemCommits.includes(currentHead))
|
|
4216
5044
|
return current;
|
|
4217
5045
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -4227,13 +5055,26 @@ export class WorkflowService {
|
|
|
4227
5055
|
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
4228
5056
|
: recoveryMode
|
|
4229
5057
|
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4230
|
-
: this.
|
|
5058
|
+
: this.dependencyProvenanceRecoveryPreflight(repository, taskId);
|
|
4231
5059
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
4232
5060
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
4233
5061
|
taskId,
|
|
4234
5062
|
blockers: preflight.blockers,
|
|
4235
5063
|
});
|
|
4236
5064
|
}
|
|
5065
|
+
if (preflight.rootCauseReplan) {
|
|
5066
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
5067
|
+
if (posture.state !== 'claimed'
|
|
5068
|
+
|| posture.claimant !== preflight.rootCauseReplan.claimant
|
|
5069
|
+
|| normalizedActor !== posture.claimant) {
|
|
5070
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
|
|
5071
|
+
taskId,
|
|
5072
|
+
actor: normalizedActor,
|
|
5073
|
+
requiredActor: preflight.rootCauseReplan.claimant,
|
|
5074
|
+
posture: posture.state,
|
|
5075
|
+
});
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
4237
5078
|
const now = this.now();
|
|
4238
5079
|
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
4239
5080
|
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
@@ -4259,6 +5100,9 @@ export class WorkflowService {
|
|
|
4259
5100
|
...(isTerminalCommit && preflight.planIntegrity
|
|
4260
5101
|
? { planIntegrity: preflight.planIntegrity }
|
|
4261
5102
|
: {}),
|
|
5103
|
+
...(isTerminalCommit && preflight.rootCauseReplan
|
|
5104
|
+
? { rootCauseReplan: preflight.rootCauseReplan }
|
|
5105
|
+
: {}),
|
|
4262
5106
|
};
|
|
4263
5107
|
});
|
|
4264
5108
|
const nextTask = {
|
|
@@ -4585,6 +5429,18 @@ export class WorkflowService {
|
|
|
4585
5429
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
4586
5430
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
4587
5431
|
}
|
|
5432
|
+
explainChange(repository, taskId, input) {
|
|
5433
|
+
const status = this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
5434
|
+
const task = status.tasks.find(candidate => candidate.id === taskId);
|
|
5435
|
+
if (!task && status.observation.kind === 'assessment') {
|
|
5436
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'The requested Task is not present in the current observation.');
|
|
5437
|
+
}
|
|
5438
|
+
const navigation = this.nextFromStatus(repository, taskId, status);
|
|
5439
|
+
const step = task?.steps.find(candidate => candidate.id === navigation.stepId)
|
|
5440
|
+
?? task?.steps.find(candidate => candidate.status === 'in_progress' || candidate.status === 'failed');
|
|
5441
|
+
const effectiveTask = task && step ? taskWithEffectiveStepAllowedWrites(this.store, task, step.id) : task;
|
|
5442
|
+
return explainChange(input, repository, effectiveTask, navigation);
|
|
5443
|
+
}
|
|
4588
5444
|
next(repository, taskId = null) {
|
|
4589
5445
|
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
4590
5446
|
}
|
|
@@ -4696,6 +5552,37 @@ export class WorkflowService {
|
|
|
4696
5552
|
}
|
|
4697
5553
|
const withAdoption = (value) => ({
|
|
4698
5554
|
...value,
|
|
5555
|
+
...(status.milestones.some((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5556
|
+
? { activeMilestoneAutonomyOptions: status.milestones
|
|
5557
|
+
.filter((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5558
|
+
.filter((milestone) => {
|
|
5559
|
+
const event = readMilestoneAutonomyEvents(this.store, status.projectId, milestone.id).at(-1);
|
|
5560
|
+
if (!event)
|
|
5561
|
+
return true;
|
|
5562
|
+
try {
|
|
5563
|
+
requireActiveMilestoneAutonomy({
|
|
5564
|
+
store: this.store, projectId: status.projectId, milestone,
|
|
5565
|
+
actor: event.delegate, now: this.now(),
|
|
5566
|
+
});
|
|
5567
|
+
return false;
|
|
5568
|
+
}
|
|
5569
|
+
catch (error) {
|
|
5570
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
5571
|
+
return true;
|
|
5572
|
+
if (error instanceof WorkflowError && error.code === 'NOT_FOUND')
|
|
5573
|
+
return true;
|
|
5574
|
+
throw error;
|
|
5575
|
+
}
|
|
5576
|
+
})
|
|
5577
|
+
.map((milestone) => ({
|
|
5578
|
+
action: 'milestone autonomy-prepare',
|
|
5579
|
+
milestoneId: milestone.id,
|
|
5580
|
+
expectedRevision: milestone.revision,
|
|
5581
|
+
requiresExactHumanConfirmation: true,
|
|
5582
|
+
maximumDurationHours: 72,
|
|
5583
|
+
preparationRequired: true,
|
|
5584
|
+
meaning: 'Optional human-requested preparation; existing valid contracts cannot be replaced. Knowledge must be active.',
|
|
5585
|
+
})) } : {}),
|
|
4699
5586
|
adoption: adoptionInfo,
|
|
4700
5587
|
observation: status.observation,
|
|
4701
5588
|
});
|
|
@@ -4748,6 +5635,19 @@ export class WorkflowService {
|
|
|
4748
5635
|
?? prioritizedRemediationTask
|
|
4749
5636
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
4750
5637
|
if (activeTask) {
|
|
5638
|
+
let pendingReviewDependencyRecovery = null;
|
|
5639
|
+
try {
|
|
5640
|
+
const reconstructed = pendingReviewSourceBinding(repositoryRoot, this.store, activeTask, runGit(repositoryRoot, ['rev-parse', 'HEAD^']), runGit(repositoryRoot, ['rev-parse', `${activeTask.baseBranch}^`]));
|
|
5641
|
+
pendingReviewDependencyRecovery = this.pendingReviewDependencyPreflight(repositoryRoot, activeTask.id, reconstructed);
|
|
5642
|
+
}
|
|
5643
|
+
catch { /* A nonmatching shape follows ordinary navigation without a compatibility bypass. */ }
|
|
5644
|
+
if (pendingReviewDependencyRecovery?.eligible) {
|
|
5645
|
+
return withAdoption({ scope: 'task', id: activeTask.id, status: activeTask.status,
|
|
5646
|
+
action: 'update pending-review-dependency-recover', revision: activeTask.revision,
|
|
5647
|
+
pendingReviewDependencyRecovery,
|
|
5648
|
+
sourceReceiptRequired: true,
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
4751
5651
|
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4752
5652
|
if (historicalStepProvenanceRecovery.eligible) {
|
|
4753
5653
|
return withAdoption({
|
|
@@ -4911,13 +5811,18 @@ export class WorkflowService {
|
|
|
4911
5811
|
&& !activeTask.steps.some((step) => step.status === 'failed');
|
|
4912
5812
|
if (activeTask.planHash && !requiresReplacementPlan) {
|
|
4913
5813
|
try {
|
|
4914
|
-
activePlanRiskAudit =
|
|
5814
|
+
activePlanRiskAudit = readCorrectivePlanningAudit(this.store, activeTask);
|
|
4915
5815
|
}
|
|
4916
5816
|
catch (error) {
|
|
4917
5817
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
4918
5818
|
throw error;
|
|
4919
5819
|
const blockingDecision = error.details.decision;
|
|
4920
5820
|
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
5821
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
|
|
5822
|
+
const verifiedPreExecutionReplan = preExecutionReplan
|
|
5823
|
+
&& blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
|
|
5824
|
+
? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
|
|
5825
|
+
: null;
|
|
4921
5826
|
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
4922
5827
|
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
4923
5828
|
.reverse()
|
|
@@ -4936,6 +5841,9 @@ export class WorkflowService {
|
|
|
4936
5841
|
: new Map();
|
|
4937
5842
|
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
4938
5843
|
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
5844
|
+
&& activeTask.planHash !== null
|
|
5845
|
+
&& activeTask.workspaceOwner === null
|
|
5846
|
+
&& activeTask.steps.every(step => step.status === 'planned')
|
|
4939
5847
|
&& remediationMilestone?.status === 'active'
|
|
4940
5848
|
&& remediationMembership?.disposition === 'required';
|
|
4941
5849
|
const remediationAncestorIds = new Set();
|
|
@@ -4951,50 +5859,34 @@ export class WorkflowService {
|
|
|
4951
5859
|
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
4952
5860
|
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
4953
5861
|
.sort();
|
|
4954
|
-
planRiskAuditRecovery =
|
|
5862
|
+
planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
|
|
4955
5863
|
? {
|
|
4956
5864
|
scope: 'task',
|
|
4957
5865
|
id: activeTask.id,
|
|
4958
5866
|
status: activeTask.status,
|
|
4959
|
-
action:
|
|
4960
|
-
? 'task plan-set'
|
|
4961
|
-
: existingRemediationTask
|
|
4962
|
-
? 'doctor'
|
|
4963
|
-
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4964
|
-
? 'milestone remediation-materialize'
|
|
4965
|
-
: remediationDiscovery
|
|
4966
|
-
? 'discovery update'
|
|
4967
|
-
: 'discovery start',
|
|
5867
|
+
action: 'task plan-set',
|
|
4968
5868
|
blockedAction: 'task authorize',
|
|
4969
5869
|
revision: activeTask.revision,
|
|
4970
|
-
|
|
4971
|
-
state: 'recorded
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5870
|
+
preExecutionReplan: {
|
|
5871
|
+
state: 'recorded',
|
|
5872
|
+
eventId: preExecutionReplan.eventId,
|
|
5873
|
+
eventHash: preExecutionReplan.eventHash,
|
|
5874
|
+
sourcePlanHash: preExecutionReplan.sourcePlanHash,
|
|
5875
|
+
sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
|
|
5876
|
+
obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
|
|
5877
|
+
obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
|
|
5878
|
+
preservesTaskScope: true,
|
|
5879
|
+
preservesCompletedStepAuthority: true,
|
|
5880
|
+
requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
|
|
4975
5881
|
},
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
humanApprovalRequired: false,
|
|
4985
|
-
blockedTaskId: activeTask.id,
|
|
4986
|
-
blockedTaskRevision: activeTask.revision,
|
|
4987
|
-
milestoneId: remediationMilestone.id,
|
|
4988
|
-
milestoneRevision: remediationMilestone.revision,
|
|
4989
|
-
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
4990
|
-
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
4991
|
-
discoveryId: remediationDiscovery?.id ?? null,
|
|
4992
|
-
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
4993
|
-
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
4994
|
-
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
4995
|
-
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
4996
|
-
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
4997
|
-
command: existingRemediationTask?.status === 'merged'
|
|
5882
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5883
|
+
}
|
|
5884
|
+
: blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
5885
|
+
? {
|
|
5886
|
+
scope: 'task',
|
|
5887
|
+
id: activeTask.id,
|
|
5888
|
+
status: activeTask.status,
|
|
5889
|
+
action: existingRemediationTask?.status === 'merged'
|
|
4998
5890
|
? 'task plan-set'
|
|
4999
5891
|
: existingRemediationTask
|
|
5000
5892
|
? 'doctor'
|
|
@@ -5003,46 +5895,84 @@ export class WorkflowService {
|
|
|
5003
5895
|
: remediationDiscovery
|
|
5004
5896
|
? 'discovery update'
|
|
5005
5897
|
: 'discovery start',
|
|
5006
|
-
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5007
|
-
preservesMilestoneSemantics: true,
|
|
5008
|
-
},
|
|
5009
|
-
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5010
|
-
}
|
|
5011
|
-
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5012
|
-
? {
|
|
5013
|
-
scope: 'task',
|
|
5014
|
-
id: activeTask.id,
|
|
5015
|
-
status: activeTask.status,
|
|
5016
|
-
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5017
5898
|
blockedAction: 'task authorize',
|
|
5018
5899
|
revision: activeTask.revision,
|
|
5019
5900
|
planRiskAuditDecision: {
|
|
5020
5901
|
state: 'recorded-blocking',
|
|
5021
5902
|
decision: blockingDecision,
|
|
5022
5903
|
reason: error.message,
|
|
5023
|
-
requiredAction:
|
|
5024
|
-
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5025
|
-
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5904
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
5026
5905
|
},
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5906
|
+
auditedRemediation: {
|
|
5907
|
+
state: existingRemediationTask?.status === 'merged'
|
|
5908
|
+
? 'merged-replan-required'
|
|
5909
|
+
: existingRemediationTask
|
|
5910
|
+
? 'materialized-not-merged'
|
|
5911
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5912
|
+
? 'ready-to-materialize'
|
|
5913
|
+
: 'discovery-required',
|
|
5914
|
+
humanApprovalRequired: false,
|
|
5915
|
+
blockedTaskId: activeTask.id,
|
|
5916
|
+
blockedTaskRevision: activeTask.revision,
|
|
5917
|
+
milestoneId: remediationMilestone.id,
|
|
5918
|
+
milestoneRevision: remediationMilestone.revision,
|
|
5919
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
5920
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
5921
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
5922
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
5923
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
5924
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
5925
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
5926
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
5927
|
+
command: existingRemediationTask?.status === 'merged'
|
|
5928
|
+
? 'task plan-set'
|
|
5929
|
+
: existingRemediationTask
|
|
5930
|
+
? 'doctor'
|
|
5931
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5932
|
+
? 'milestone remediation-materialize'
|
|
5933
|
+
: remediationDiscovery
|
|
5934
|
+
? 'discovery update'
|
|
5935
|
+
: 'discovery start',
|
|
5936
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5937
|
+
preservesMilestoneSemantics: true,
|
|
5044
5938
|
},
|
|
5045
|
-
|
|
5939
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5940
|
+
}
|
|
5941
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5942
|
+
? {
|
|
5943
|
+
scope: 'task',
|
|
5944
|
+
id: activeTask.id,
|
|
5945
|
+
status: activeTask.status,
|
|
5946
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5947
|
+
blockedAction: 'task authorize',
|
|
5948
|
+
revision: activeTask.revision,
|
|
5949
|
+
planRiskAuditDecision: {
|
|
5950
|
+
state: 'recorded-blocking',
|
|
5951
|
+
decision: blockingDecision,
|
|
5952
|
+
reason: error.message,
|
|
5953
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
5954
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5955
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5956
|
+
},
|
|
5957
|
+
...(blockingDecision === 'stop-escalate'
|
|
5958
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
5959
|
+
: {}),
|
|
5960
|
+
}
|
|
5961
|
+
: {
|
|
5962
|
+
scope: 'task',
|
|
5963
|
+
id: activeTask.id,
|
|
5964
|
+
status: activeTask.status,
|
|
5965
|
+
action: 'task plan-risk-audit',
|
|
5966
|
+
revision: activeTask.revision,
|
|
5967
|
+
planRiskAuditRecovery: {
|
|
5968
|
+
state: 'required',
|
|
5969
|
+
reason: error.message,
|
|
5970
|
+
currentBriefHash: activeTask.briefHash,
|
|
5971
|
+
currentPlanHash: activeTask.planHash,
|
|
5972
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
5973
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
5974
|
+
},
|
|
5975
|
+
};
|
|
5046
5976
|
}
|
|
5047
5977
|
}
|
|
5048
5978
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -5092,7 +6022,7 @@ export class WorkflowService {
|
|
|
5092
6022
|
|| taskNext?.action === 'task downstream-proof-recover'
|
|
5093
6023
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
5094
6024
|
|| (taskNext?.action === 'task writer-credential-replace'
|
|
5095
|
-
&&
|
|
6025
|
+
&& ['task step-complete', 'task step-review', 'task remediation-mode-recover'].includes(String(taskNext?.blockedAction)))
|
|
5096
6026
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
5097
6027
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
5098
6028
|
|| taskNext?.action === 'task corrective-yield'
|
|
@@ -5168,6 +6098,7 @@ export class WorkflowService {
|
|
|
5168
6098
|
contextRefresh: {
|
|
5169
6099
|
action: 'task context-refresh',
|
|
5170
6100
|
mode: refreshAssessment.mode,
|
|
6101
|
+
authority: readFailedStepPlanningRecovery(this.store, activeTask) ? 'planning-only' : 'execution',
|
|
5171
6102
|
addedSources: refreshAssessment.addedSources,
|
|
5172
6103
|
options: contextRefreshOptions,
|
|
5173
6104
|
},
|
|
@@ -5524,7 +6455,9 @@ export class WorkflowService {
|
|
|
5524
6455
|
return { milestone, authorization, scopeChange };
|
|
5525
6456
|
}
|
|
5526
6457
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
5527
|
-
const
|
|
6458
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
6459
|
+
// A stopped rebound Plan can expose planning recovery, never an approval route.
|
|
6460
|
+
const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
|
|
5528
6461
|
? 'task.execution_authorize'
|
|
5529
6462
|
: task.status === 'awaiting_final_acceptance'
|
|
5530
6463
|
? 'task.final_accept'
|
|
@@ -5565,6 +6498,9 @@ export class WorkflowService {
|
|
|
5565
6498
|
}
|
|
5566
6499
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
5567
6500
|
const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
6501
|
+
const rootCauseStepId = nextStepId
|
|
6502
|
+
?? task.steps.find((candidate) => candidate.status === 'planned')?.id
|
|
6503
|
+
?? null;
|
|
5568
6504
|
const downstreamProofCarryover = next.action === 'task run'
|
|
5569
6505
|
&& nextStepId
|
|
5570
6506
|
&& changedFiles(repositoryRoot).length > 0
|
|
@@ -5575,6 +6511,24 @@ export class WorkflowService {
|
|
|
5575
6511
|
&& changedFiles(repositoryRoot).length > 0
|
|
5576
6512
|
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
5577
6513
|
: null;
|
|
6514
|
+
let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
|
|
6515
|
+
&& rootCauseStepId
|
|
6516
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
6517
|
+
? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
|
|
6518
|
+
: null;
|
|
6519
|
+
if (rootCauseReplanDirtyCarryover?.eligible) {
|
|
6520
|
+
try {
|
|
6521
|
+
this.assertExecutionAuthorizationFresh(task, repositoryRoot);
|
|
6522
|
+
}
|
|
6523
|
+
catch (error) {
|
|
6524
|
+
rootCauseReplanDirtyCarryover = {
|
|
6525
|
+
applicable: true,
|
|
6526
|
+
eligible: false,
|
|
6527
|
+
compatibility: null,
|
|
6528
|
+
blockers: [error instanceof Error ? error.message : String(error)],
|
|
6529
|
+
};
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
5578
6532
|
let mechanicalFeasibilityOverlay = {};
|
|
5579
6533
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
5580
6534
|
try {
|
|
@@ -5918,8 +6872,8 @@ export class WorkflowService {
|
|
|
5918
6872
|
...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
|
|
5919
6873
|
? [`${guardedCorrectivePosture.stepId} must remain failed.`]
|
|
5920
6874
|
: []),
|
|
5921
|
-
...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
5922
|
-
...(rollingC1Posture.state !== 'none'
|
|
6875
|
+
...(writerLease && rollingC1Posture.state !== 'claimed' ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
6876
|
+
...(rollingC1Posture.state !== 'none' && rollingC1Posture.state !== 'claimed'
|
|
5923
6877
|
? ['No C1 claim may remain after the root-cause failure pause.']
|
|
5924
6878
|
: []),
|
|
5925
6879
|
];
|
|
@@ -5936,7 +6890,7 @@ export class WorkflowService {
|
|
|
5936
6890
|
},
|
|
5937
6891
|
}
|
|
5938
6892
|
: {
|
|
5939
|
-
action: 'task plan-set',
|
|
6893
|
+
action: rollingC1Posture.state === 'claimed' ? 'task corrective-yield' : 'task plan-set',
|
|
5940
6894
|
blockedAction: 'task run',
|
|
5941
6895
|
stepId: guardedCorrectivePosture.stepId,
|
|
5942
6896
|
rollingWave: {
|
|
@@ -6083,9 +7037,42 @@ export class WorkflowService {
|
|
|
6083
7037
|
},
|
|
6084
7038
|
}
|
|
6085
7039
|
: {};
|
|
7040
|
+
const preExecutionReplanBlockers = task.status === 'ready'
|
|
7041
|
+
? [
|
|
7042
|
+
...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
|
|
7043
|
+
...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
|
|
7044
|
+
...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
|
|
7045
|
+
...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
7046
|
+
? ['No active or stale writer lease may exist.']
|
|
7047
|
+
: []),
|
|
7048
|
+
...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
|
|
7049
|
+
...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
|
|
7050
|
+
? ['A current positive Plan Risk Audit is required.']
|
|
7051
|
+
: []),
|
|
7052
|
+
]
|
|
7053
|
+
: [];
|
|
7054
|
+
const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
|
|
7055
|
+
? {
|
|
7056
|
+
command: 'task preexecution-replan',
|
|
7057
|
+
state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
|
|
7058
|
+
expectedRevision: task.revision,
|
|
7059
|
+
expectedBriefHash: task.briefHash,
|
|
7060
|
+
expectedPlanHash: task.planHash,
|
|
7061
|
+
expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
|
|
7062
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
7063
|
+
auditDecision: 'stop-escalate',
|
|
7064
|
+
actorMustEqualAuditAuditor: true,
|
|
7065
|
+
humanApprovalRequired: false,
|
|
7066
|
+
boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
|
|
7067
|
+
preservesTaskScope: true,
|
|
7068
|
+
preservesCompletedStepAuthority: true,
|
|
7069
|
+
blockers: preExecutionReplanBlockers,
|
|
7070
|
+
}
|
|
7071
|
+
: null;
|
|
6086
7072
|
const result = {
|
|
6087
7073
|
...next,
|
|
6088
7074
|
...handoffOverlay,
|
|
7075
|
+
...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
|
|
6089
7076
|
...(strictStepReviewStep
|
|
6090
7077
|
? {
|
|
6091
7078
|
action: 'task step-review',
|
|
@@ -6171,7 +7158,7 @@ export class WorkflowService {
|
|
|
6171
7158
|
downstreamProofCarryover: {
|
|
6172
7159
|
state: 'validated',
|
|
6173
7160
|
invalidationEventId: downstreamProofCarryover.invalidationEventId,
|
|
6174
|
-
stepId:
|
|
7161
|
+
stepId: rootCauseStepId,
|
|
6175
7162
|
dirtyFiles: downstreamProofCarryover.dirtyFiles,
|
|
6176
7163
|
dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
|
|
6177
7164
|
meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
|
|
@@ -6211,6 +7198,52 @@ export class WorkflowService {
|
|
|
6211
7198
|
},
|
|
6212
7199
|
}
|
|
6213
7200
|
: {}),
|
|
7201
|
+
...(rootCauseReplanDirtyCarryover?.applicable
|
|
7202
|
+
? rootCauseReplanDirtyCarryover.eligible && rootCauseReplanDirtyCarryover.compatibility
|
|
7203
|
+
? {
|
|
7204
|
+
action: 'task run',
|
|
7205
|
+
rootCauseReplanDirtyCarryover: {
|
|
7206
|
+
state: 'validated',
|
|
7207
|
+
stepId: nextStepId,
|
|
7208
|
+
compatibilityHash: rootCauseReplanDirtyCarryover.compatibility.compatibilityHash,
|
|
7209
|
+
dirtyFiles: rootCauseReplanDirtyCarryover.compatibility.dirtyFiles,
|
|
7210
|
+
dirtyWorktreeHash: rootCauseReplanDirtyCarryover.compatibility.dirtyWorktreeHash,
|
|
7211
|
+
meaning: 'The replacement Step may adopt the exact retained root-cause failure bytes.',
|
|
7212
|
+
},
|
|
7213
|
+
}
|
|
7214
|
+
: rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
7215
|
+
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1')
|
|
7216
|
+
? handoffPosture.state === 'none'
|
|
7217
|
+
? {
|
|
7218
|
+
action: 'task handoff-prepare',
|
|
7219
|
+
blockedAction: 'task run',
|
|
7220
|
+
rootCauseReplanDirtyCarryover: {
|
|
7221
|
+
state: 'awaiting-c1',
|
|
7222
|
+
stepId: rootCauseStepId,
|
|
7223
|
+
requiredActor: defaultTaskWorkerActor(task.id),
|
|
7224
|
+
},
|
|
7225
|
+
}
|
|
7226
|
+
: handoffPosture.state === 'pending'
|
|
7227
|
+
? {}
|
|
7228
|
+
: {
|
|
7229
|
+
action: 'doctor',
|
|
7230
|
+
blockedAction: 'task run',
|
|
7231
|
+
rootCauseReplanDirtyCarryover: {
|
|
7232
|
+
state: 'blocked',
|
|
7233
|
+
stepId: rootCauseStepId,
|
|
7234
|
+
blockers: rootCauseReplanDirtyCarryover.blockers,
|
|
7235
|
+
},
|
|
7236
|
+
}
|
|
7237
|
+
: {
|
|
7238
|
+
action: 'doctor',
|
|
7239
|
+
blockedAction: 'task run',
|
|
7240
|
+
rootCauseReplanDirtyCarryover: {
|
|
7241
|
+
state: 'blocked',
|
|
7242
|
+
stepId: rootCauseStepId,
|
|
7243
|
+
blockers: rootCauseReplanDirtyCarryover.blockers,
|
|
7244
|
+
},
|
|
7245
|
+
}
|
|
7246
|
+
: {}),
|
|
6214
7247
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6215
7248
|
...(correctiveDerivedAuthority && correctiveDerivedActor
|
|
6216
7249
|
? {
|
|
@@ -6227,9 +7260,56 @@ export class WorkflowService {
|
|
|
6227
7260
|
}
|
|
6228
7261
|
: {}),
|
|
6229
7262
|
};
|
|
6230
|
-
|
|
7263
|
+
// A later independent audit may discover an obstruction absent from the sealed
|
|
7264
|
+
// fix review. Advertise the existing append-only decision route without turning
|
|
7265
|
+
// every ordinary repair into a mandatory replan or rewriting that review.
|
|
7266
|
+
const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
|
|
7267
|
+
const optionalCorrectiveDecision = (result.action === 'task run' || planningRecovery !== null)
|
|
7268
|
+
&& (['needs_fix', 'ready'].includes(task.status) || planningRecovery !== null)
|
|
7269
|
+
&& failedStep && task.planHash && planRiskAudit
|
|
7270
|
+
&& planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
|
|
7271
|
+
&& !task.steps.some(step => step.status === 'in_progress')
|
|
7272
|
+
&& failedStepRemediations.length >= 2
|
|
7273
|
+
&& handoffPosture.state !== 'pending'
|
|
7274
|
+
&& !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
|
|
7275
|
+
&& currentBranch(repositoryRoot) === task.taskBranch && isClean(repositoryRoot)
|
|
7276
|
+
? {
|
|
7277
|
+
command: 'task corrective-decision',
|
|
7278
|
+
state: 'eligible',
|
|
7279
|
+
optional: true,
|
|
7280
|
+
taskId: task.id,
|
|
7281
|
+
stepId: failedStep.id,
|
|
7282
|
+
expectedRevision: task.revision,
|
|
7283
|
+
expectedBriefHash: task.briefHash,
|
|
7284
|
+
expectedPlanHash: task.planHash,
|
|
7285
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
7286
|
+
triggeringAttemptCount: correctiveDecisionOrdinal,
|
|
7287
|
+
coveredRemediationEvents: failedStepRemediations.map(event => ({ eventId: event.eventId, eventHash: event.eventHash })),
|
|
7288
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
7289
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
7290
|
+
requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
|
|
7291
|
+
decisions: planningRecovery ? ['replan-required', 'split-required', 'stop-escalate']
|
|
7292
|
+
: ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
|
|
7293
|
+
authority: 'none',
|
|
7294
|
+
preservesOriginalReview: true,
|
|
7295
|
+
humanApprovalRequiredForDecision: false,
|
|
7296
|
+
replanRequiresExistingHumanGate: true,
|
|
7297
|
+
requiredAction: 'Only for newly assessed evidence: obtain an independent corrective audit, refresh next and existing writer authority, then record the decision. This option does not authorize Plan replacement or extra writes.',
|
|
7298
|
+
}
|
|
7299
|
+
: null;
|
|
7300
|
+
const resultWithCorrectiveOption = optionalCorrectiveDecision
|
|
7301
|
+
? { ...result, correctiveDecisionOption: optionalCorrectiveDecision,
|
|
7302
|
+
...(planningRecovery ? { action: 'task corrective-decision', blockedAction: 'task authorize',
|
|
7303
|
+
planningRecovery: { authority: 'planning-only', obstructionAuditHash: planningRecovery.current.audit.eventHash,
|
|
7304
|
+
baselineAuditHash: planningRecovery.baseline.eventHash } } : {}) }
|
|
7305
|
+
: planningRecovery && result.action === 'task authorize'
|
|
7306
|
+
? { ...result, action: 'doctor', blockedAction: 'task authorize' } : result;
|
|
7307
|
+
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
7308
|
+
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
7309
|
+
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
7310
|
+
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
|
|
6231
7311
|
return this.withTaskNavigationContracts(task, {
|
|
6232
|
-
...
|
|
7312
|
+
...resultWithCorrectiveOption,
|
|
6233
7313
|
action: 'task claim',
|
|
6234
7314
|
blockedAction: next.action,
|
|
6235
7315
|
});
|
|
@@ -6237,10 +7317,10 @@ export class WorkflowService {
|
|
|
6237
7317
|
const navigableResult = ['task plan-set']
|
|
6238
7318
|
.includes(String(result.action))
|
|
6239
7319
|
? {
|
|
6240
|
-
...
|
|
7320
|
+
...resultWithCorrectiveOption,
|
|
6241
7321
|
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
6242
7322
|
}
|
|
6243
|
-
:
|
|
7323
|
+
: resultWithCorrectiveOption;
|
|
6244
7324
|
return this.withTaskNavigationContracts(task, navigableResult);
|
|
6245
7325
|
}
|
|
6246
7326
|
taskPlanContract(projectId, task) {
|
|
@@ -6261,6 +7341,9 @@ export class WorkflowService {
|
|
|
6261
7341
|
};
|
|
6262
7342
|
}
|
|
6263
7343
|
assertTaskContextChangeAllowed(task) {
|
|
7344
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
7345
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has a pending pre-execution replan; replace the implementation Plan before changing context.`, { taskId: task.id, requiredAction: 'task plan-set' });
|
|
7346
|
+
}
|
|
6264
7347
|
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
6265
7348
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
6266
7349
|
taskId: task.id,
|
|
@@ -6302,7 +7385,7 @@ export class WorkflowService {
|
|
|
6302
7385
|
taskTerminalCorrectivePosture(task) {
|
|
6303
7386
|
if (!task.planHash)
|
|
6304
7387
|
return null;
|
|
6305
|
-
const planRiskAudit =
|
|
7388
|
+
const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
|
|
6306
7389
|
if (!planRiskAudit)
|
|
6307
7390
|
return null;
|
|
6308
7391
|
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
@@ -6347,7 +7430,7 @@ export class WorkflowService {
|
|
|
6347
7430
|
}
|
|
6348
7431
|
}
|
|
6349
7432
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
6350
|
-
return this.
|
|
7433
|
+
return this.listDelegations(projectId)
|
|
6351
7434
|
.filter((grant) => {
|
|
6352
7435
|
try {
|
|
6353
7436
|
return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
|
|
@@ -6369,7 +7452,20 @@ export class WorkflowService {
|
|
|
6369
7452
|
}));
|
|
6370
7453
|
}
|
|
6371
7454
|
delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
|
|
6372
|
-
|
|
7455
|
+
let planningRecovery;
|
|
7456
|
+
try {
|
|
7457
|
+
planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
7458
|
+
if (!planningRecovery)
|
|
7459
|
+
readCurrentPlanRiskAudit(this.store, task);
|
|
7460
|
+
}
|
|
7461
|
+
catch (error) {
|
|
7462
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
7463
|
+
return [];
|
|
7464
|
+
throw error;
|
|
7465
|
+
}
|
|
7466
|
+
if (planningRecovery && mode !== 'delegated-content-only')
|
|
7467
|
+
return [];
|
|
7468
|
+
return this.listDelegations(projectId)
|
|
6373
7469
|
.filter((grant) => {
|
|
6374
7470
|
try {
|
|
6375
7471
|
if (mode === 'delegated-plan-bounded-supporting-source') {
|
|
@@ -6388,8 +7484,8 @@ export class WorkflowService {
|
|
|
6388
7484
|
grantId: grant.id,
|
|
6389
7485
|
principal: grant.principal,
|
|
6390
7486
|
delegate: grant.delegate,
|
|
6391
|
-
transition: 'task.execution_authorize',
|
|
6392
|
-
transitions: ['project_memory.approve', 'task.execution_authorize'],
|
|
7487
|
+
transition: planningRecovery ? 'project_memory.approve' : 'task.execution_authorize',
|
|
7488
|
+
transitions: planningRecovery ? ['project_memory.approve'] : ['project_memory.approve', 'task.execution_authorize'],
|
|
6393
7489
|
scope: grant.scope,
|
|
6394
7490
|
expiresAt: grant.expiresAt,
|
|
6395
7491
|
policyHash: grant.policyHash,
|
|
@@ -6746,7 +7842,7 @@ export class WorkflowService {
|
|
|
6746
7842
|
blockers,
|
|
6747
7843
|
};
|
|
6748
7844
|
}
|
|
6749
|
-
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
|
|
7845
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
|
|
6750
7846
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
6751
7847
|
const now = this.now().getTime();
|
|
6752
7848
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -6766,7 +7862,89 @@ export class WorkflowService {
|
|
|
6766
7862
|
currentBranch: versions.currentBranch,
|
|
6767
7863
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
6768
7864
|
},
|
|
6769
|
-
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
|
|
7865
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
|
|
7866
|
+
}
|
|
7867
|
+
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
|
|
7868
|
+
const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
|
|
7869
|
+
&& task.systemCommits.includes(record.commitSha)) ?? null;
|
|
7870
|
+
const recorded = recordedRecovery?.rootCauseReplan ?? null;
|
|
7871
|
+
if (recorded) {
|
|
7872
|
+
const recordedStep = task.steps.find((candidate) => candidate.id === recorded.stepId) ?? null;
|
|
7873
|
+
if (!recordedStep) {
|
|
7874
|
+
return {
|
|
7875
|
+
applicable: true,
|
|
7876
|
+
eligible: false,
|
|
7877
|
+
compatibility: null,
|
|
7878
|
+
blockers: [`Recorded corrective carryover Step ${recorded.stepId} is missing from the current Plan.`],
|
|
7879
|
+
};
|
|
7880
|
+
}
|
|
7881
|
+
if (recordedStep.status === 'completed') {
|
|
7882
|
+
const blockers = [];
|
|
7883
|
+
if (!recordedStep.evidence) {
|
|
7884
|
+
blockers.push(`Completed corrective carryover Step ${recorded.stepId} has no execution evidence.`);
|
|
7885
|
+
}
|
|
7886
|
+
try {
|
|
7887
|
+
validateTaskHistory(repositoryRoot, task, historyHead);
|
|
7888
|
+
const sourceAudit = readPlanRiskAuditEvents(this.store, task).find((event) => event.eventId === recorded.replacementPlanRiskAuditEventId
|
|
7889
|
+
&& event.eventHash === recorded.replacementPlanRiskAuditEventHash) ?? null;
|
|
7890
|
+
const classification = sourceAudit?.stepClassifications.find((candidate) => candidate.stepId === recorded.stepId) ?? null;
|
|
7891
|
+
if (!sourceAudit || !classification) {
|
|
7892
|
+
blockers.push('Completed corrective carryover Step lost its exact replacement Plan Risk Audit binding.');
|
|
7893
|
+
}
|
|
7894
|
+
else if (classification.reviewRequired
|
|
7895
|
+
&& recordedStep.evidence
|
|
7896
|
+
&& !verifiedHistoricalStepReviewBindings(this.store, task).some((binding) => binding.stepId === recorded.stepId
|
|
7897
|
+
&& binding.commitSha === recordedStep.evidence.commitSha)) {
|
|
7898
|
+
blockers.push(`Completed corrective carryover Step ${recorded.stepId} lacks verified strict-review proof.`);
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
catch (error) {
|
|
7902
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
7903
|
+
}
|
|
7904
|
+
if (blockers.length > 0) {
|
|
7905
|
+
return {
|
|
7906
|
+
applicable: true,
|
|
7907
|
+
eligible: false,
|
|
7908
|
+
compatibility: null,
|
|
7909
|
+
blockers: [...new Set(blockers)],
|
|
7910
|
+
};
|
|
7911
|
+
}
|
|
7912
|
+
// A reviewed, history-valid completed Step consumed this carryover. Keep its
|
|
7913
|
+
// immutable recovery record, but let later Steps use their own lifecycle route.
|
|
7914
|
+
}
|
|
7915
|
+
else {
|
|
7916
|
+
if (stepId && stepId !== recorded.stepId) {
|
|
7917
|
+
return {
|
|
7918
|
+
applicable: true,
|
|
7919
|
+
eligible: false,
|
|
7920
|
+
compatibility: null,
|
|
7921
|
+
blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7924
|
+
return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
|
|
7925
|
+
binding: recorded,
|
|
7926
|
+
});
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
const candidates = task.steps
|
|
7930
|
+
.filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
|
|
7931
|
+
.flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
|
|
7932
|
+
.filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
|
|
7933
|
+
.map((event) => event.failureEvidence.completionCommit))]
|
|
7934
|
+
.map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
|
|
7935
|
+
.filter((assessment) => assessment.applicable);
|
|
7936
|
+
const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
|
|
7937
|
+
if (eligible.length === 1)
|
|
7938
|
+
return eligible[0];
|
|
7939
|
+
return {
|
|
7940
|
+
applicable: candidates.length > 0,
|
|
7941
|
+
eligible: false,
|
|
7942
|
+
compatibility: null,
|
|
7943
|
+
blockers: [...new Set([
|
|
7944
|
+
...candidates.flatMap((assessment) => assessment.blockers),
|
|
7945
|
+
...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
|
|
7946
|
+
])],
|
|
7947
|
+
};
|
|
6770
7948
|
}
|
|
6771
7949
|
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
6772
7950
|
const now = this.now().getTime();
|
|
@@ -7470,6 +8648,83 @@ function taskHasStarted(task) {
|
|
|
7470
8648
|
'merged',
|
|
7471
8649
|
].includes(task.status);
|
|
7472
8650
|
}
|
|
8651
|
+
function resolvePreExecutionReplanAudits(store, task, marker) {
|
|
8652
|
+
const audits = readPlanRiskAuditEvents(store, task);
|
|
8653
|
+
const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
|
|
8654
|
+
&& audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
|
|
8655
|
+
const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
|
|
8656
|
+
&& audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
|
|
8657
|
+
const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
|
|
8658
|
+
&& authorization.decision === 'superseded'
|
|
8659
|
+
&& hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
|
|
8660
|
+
if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
|
|
8661
|
+
|| !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
|
|
8662
|
+
|| obstructionAudit.decision !== 'stop-escalate'
|
|
8663
|
+
|| sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
|
|
8664
|
+
|| !sourceAuthorization) {
|
|
8665
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
|
|
8666
|
+
taskId: task.id,
|
|
8667
|
+
eventId: marker.eventId,
|
|
8668
|
+
});
|
|
8669
|
+
}
|
|
8670
|
+
return { sourceAudit, obstructionAudit };
|
|
8671
|
+
}
|
|
8672
|
+
function assertPlanRiskSafetyPreserved(source, candidate, label) {
|
|
8673
|
+
const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
|
|
8674
|
+
const missing = [];
|
|
8675
|
+
for (const sourceClassification of source.stepClassifications) {
|
|
8676
|
+
const candidateClassification = candidateByStep.get(sourceClassification.stepId);
|
|
8677
|
+
if (!candidateClassification) {
|
|
8678
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
|
|
8679
|
+
continue;
|
|
8680
|
+
}
|
|
8681
|
+
const preserveStrings = (field, sourceValues, candidateValues) => {
|
|
8682
|
+
const absent = sourceValues.filter((value) => !candidateValues.includes(value));
|
|
8683
|
+
if (absent.length > 0)
|
|
8684
|
+
missing.push({ stepId: sourceClassification.stepId, field, values: absent });
|
|
8685
|
+
};
|
|
8686
|
+
preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
|
|
8687
|
+
preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
|
|
8688
|
+
preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
|
|
8689
|
+
if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
|
|
8690
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
|
|
8691
|
+
}
|
|
8692
|
+
const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
|
|
8693
|
+
const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
|
|
8694
|
+
const absentProofs = (sourceClassification.proofObligations ?? [])
|
|
8695
|
+
.filter((proof) => !candidateProofs.has(proofBinding(proof)));
|
|
8696
|
+
if (absentProofs.length > 0) {
|
|
8697
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
if (missing.length > 0) {
|
|
8701
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
|
|
8702
|
+
missing,
|
|
8703
|
+
});
|
|
8704
|
+
}
|
|
8705
|
+
const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
|
|
8706
|
+
canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
|
|
8707
|
+
canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
|
|
8708
|
+
])));
|
|
8709
|
+
const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
|
|
8710
|
+
const binding = canonicalJsonStringify({
|
|
8711
|
+
stepId: classification.stepId,
|
|
8712
|
+
failureMode: proof.failureMode,
|
|
8713
|
+
evidence: proof.evidence,
|
|
8714
|
+
});
|
|
8715
|
+
const previous = sourceProofText.get(binding);
|
|
8716
|
+
return previous !== undefined
|
|
8717
|
+
&& previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
|
|
8718
|
+
}));
|
|
8719
|
+
if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
|
|
8720
|
+
|| candidate.summary.trim() === source.summary.trim())) {
|
|
8721
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
|
|
8722
|
+
sourceSummary: source.summary,
|
|
8723
|
+
candidateSummary: candidate.summary,
|
|
8724
|
+
candidateDecision: candidate.decision,
|
|
8725
|
+
});
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
7473
8728
|
function taskCanRebindKnowledge(task) {
|
|
7474
8729
|
return [
|
|
7475
8730
|
'awaiting_execution_authorization',
|