codex-workflow-v2 2.0.0-beta.12 → 2.0.0-beta.12.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 +65 -5
- package/dist/src/alpha6/downstream-proof.d.ts +68 -0
- package/dist/src/alpha6/downstream-proof.js +604 -0
- package/dist/src/alpha6/downstream-proof.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +6 -2
- package/dist/src/alpha6/milestone.js +164 -10
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +2 -2
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +8 -3
- package/dist/src/alpha6/remediation.js +374 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +6 -0
- package/dist/src/alpha6/review.js +12 -0
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/alpha7/corrective-recovery.d.ts +8 -1
- package/dist/src/alpha7/corrective-recovery.js +406 -26
- package/dist/src/alpha7/corrective-recovery.js.map +1 -1
- package/dist/src/cli.js +135 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +154 -3
- package/dist/src/dependency-provenance.d.ts +9 -2
- package/dist/src/dependency-provenance.js +70 -8
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +6 -2
- package/dist/src/git.js.map +1 -1
- package/dist/src/historical-step-provenance.d.ts +20 -0
- package/dist/src/historical-step-provenance.js +111 -0
- package/dist/src/historical-step-provenance.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.js +5 -0
- package/dist/src/state/corrective-replan-executor.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 +40 -2
- package/dist/src/workflow.js +1440 -65
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +91 -1
- package/docs/decisions.md +14 -0
- package/docs/delegated-approval.md +6 -1
- package/docs/development-flow.md +60 -5
- 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 +97 -7
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +68 -9
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +150 -33
- package/docs/release.md +51 -8
- package/docs/updating-existing-project.md +155 -2
- package/docs/validation-report.md +101 -94
- package/package.json +7 -2
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +69 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +156 -1
- package/schemas/authorization-event.schema.json +8 -1
- package/schemas/downstream-proof-invalidation-event.schema.json +71 -0
- package/schemas/downstream-proof-replan-recovery-event.schema.json +61 -0
- package/schemas/milestone-scope-change-event.schema.json +25 -1
- package/schemas/stop-escalate-override-event.schema.json +45 -0
- package/schemas/task.schema.json +104 -0
- package/scripts/generate-pdf-docs.py +13 -2
package/dist/src/workflow.js
CHANGED
|
@@ -2,7 +2,8 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
|
-
import { inspectDependencyProvenanceRecovery } from './dependency-provenance.js';
|
|
5
|
+
import { inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
6
|
+
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
6
7
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
7
8
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
8
9
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
@@ -10,17 +11,18 @@ import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
|
10
11
|
import { WorkflowError } from './errors.js';
|
|
11
12
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
12
13
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
13
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
14
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
15
|
+
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
14
16
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
15
|
-
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
16
|
-
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
17
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
18
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
17
19
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
18
20
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
19
|
-
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
21
|
+
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneScopeChangeEvents, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
20
22
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
21
|
-
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
23
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, verifiedHistoricalStepReviewBindings, } from './alpha6/review.js';
|
|
22
24
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
23
|
-
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
25
|
+
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
24
26
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
25
27
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
26
28
|
import { ensureDirectory } from './fs-utils.js';
|
|
@@ -110,6 +112,31 @@ export class WorkflowService {
|
|
|
110
112
|
repaired: true,
|
|
111
113
|
};
|
|
112
114
|
}
|
|
115
|
+
prepareTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason) {
|
|
116
|
+
const snapshot = this.observationSnapshot(repository);
|
|
117
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
118
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
119
|
+
observation: snapshot.observation,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId) ?? null;
|
|
123
|
+
if (!task)
|
|
124
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
125
|
+
this.assertStopOverrideRepositoryBoundary(snapshot.identity, task);
|
|
126
|
+
return prepareStopEscalateOverride(this.store, task, stepId, expectedRevision, actor, reason, headCommit(snapshot.identity.repositoryRoot));
|
|
127
|
+
}
|
|
128
|
+
applyTaskStopOverride(repository, taskId, stepId, expectedRevision, actor, reason, confirmationCode) {
|
|
129
|
+
const snapshot = this.observationSnapshot(repository);
|
|
130
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
131
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override requires a coherent observation boundary.', {
|
|
132
|
+
observation: snapshot.observation,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const context = this.#mutationContext(repository);
|
|
136
|
+
const task = context.store.readTask(context.identity.projectId, taskId);
|
|
137
|
+
this.assertStopOverrideRepositoryBoundary(context.identity, task, context.locks.list());
|
|
138
|
+
return appendStopEscalateOverride(context.store, task, stepId, expectedRevision, actor, reason, headCommit(context.identity.repositoryRoot), confirmationCode, this.now());
|
|
139
|
+
}
|
|
113
140
|
prepareCorrectiveReplan(repository, taskId, candidate) {
|
|
114
141
|
return this.correctiveReplanController(repository).prepare({ taskId, candidate });
|
|
115
142
|
}
|
|
@@ -145,6 +172,202 @@ export class WorkflowService {
|
|
|
145
172
|
now: this.now,
|
|
146
173
|
}).recover({ taskId, transactionId });
|
|
147
174
|
}
|
|
175
|
+
recoverDownstreamProof(repository, taskId, expectedTaskRevision, actor, writerToken, reason) {
|
|
176
|
+
const context = this.#mutationContext(repository);
|
|
177
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
178
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
179
|
+
assertExpectedRevision(task, expectedTaskRevision);
|
|
180
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, lease.token);
|
|
181
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
182
|
+
if (posture.state === 'none' && lease.owner !== actor.trim()) {
|
|
183
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream proof recovery actor must own the active writer lease.', {
|
|
184
|
+
taskId,
|
|
185
|
+
requiredActor: lease.owner,
|
|
186
|
+
actualActor: actor.trim(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const assessment = assessDownstreamProofRecovery(this.store, context.identity.repositoryRoot, task);
|
|
190
|
+
if (!assessment.applicable || !assessment.eligible || !assessment.activeStepId) {
|
|
191
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task is not eligible for bounded downstream proof recovery.', {
|
|
192
|
+
taskId,
|
|
193
|
+
assessment,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const invalidatedIds = new Set(assessment.invalidatedSteps.map((step) => step.stepId));
|
|
197
|
+
const retainedHistoricalCommits = assessment.invalidatedSteps.map((step) => step.evidenceCommit);
|
|
198
|
+
const now = this.now();
|
|
199
|
+
const resultTask = {
|
|
200
|
+
...task,
|
|
201
|
+
revision: task.revision + 1,
|
|
202
|
+
updatedAt: now.toISOString(),
|
|
203
|
+
status: 'needs_fix',
|
|
204
|
+
steps: task.steps.map((step) => {
|
|
205
|
+
if (invalidatedIds.has(step.id))
|
|
206
|
+
return { ...step, status: 'planned', evidence: null };
|
|
207
|
+
if (step.id === assessment.activeStepId)
|
|
208
|
+
return { ...step, status: 'blocked', evidence: null };
|
|
209
|
+
return step;
|
|
210
|
+
}),
|
|
211
|
+
authorizations: task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
212
|
+
? { ...authorization, decision: 'superseded' }
|
|
213
|
+
: authorization),
|
|
214
|
+
review: emptyReview(),
|
|
215
|
+
resultHash: null,
|
|
216
|
+
evidenceHash: null,
|
|
217
|
+
blockReason: `${assessment.activeStepId} exposed changes required in completed predecessor authority; replace the Step Plan before resuming execution.`,
|
|
218
|
+
invalidatedStepCommits: [...new Set([
|
|
219
|
+
...(task.invalidatedStepCommits ?? []),
|
|
220
|
+
...retainedHistoricalCommits,
|
|
221
|
+
])],
|
|
222
|
+
};
|
|
223
|
+
const invalidation = prepareDownstreamProofInvalidation(this.store, resultTask, assessment, {
|
|
224
|
+
previousTaskRevision: task.revision,
|
|
225
|
+
actor,
|
|
226
|
+
reason,
|
|
227
|
+
}, now);
|
|
228
|
+
const preparedYield = posture.state === 'claimed'
|
|
229
|
+
? prepareTaskCorrectiveYield(this.store, resultTask, {
|
|
230
|
+
actor,
|
|
231
|
+
writerToken,
|
|
232
|
+
reason: `Yield after downstream proof invalidated predecessor completion authority: ${reason}`,
|
|
233
|
+
eventId: `HOF-${sha256Hex([
|
|
234
|
+
'downstream-proof-recovery-v1',
|
|
235
|
+
task.id,
|
|
236
|
+
String(task.revision),
|
|
237
|
+
invalidation.event.eventHash,
|
|
238
|
+
actor.trim(),
|
|
239
|
+
].join('\0'))}`,
|
|
240
|
+
}, now)
|
|
241
|
+
: null;
|
|
242
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
243
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
244
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
245
|
+
applyProjectTransaction({
|
|
246
|
+
projectRoot,
|
|
247
|
+
projectId: task.projectId,
|
|
248
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
249
|
+
kind: 'downstream-proof-predecessor-recovery',
|
|
250
|
+
now,
|
|
251
|
+
targets: [
|
|
252
|
+
{
|
|
253
|
+
path: relative(path.join(taskRoot, downstreamProofInvalidationSidecar())),
|
|
254
|
+
content: invalidation.postimage,
|
|
255
|
+
},
|
|
256
|
+
...(preparedYield
|
|
257
|
+
? [{ path: relative(path.join(taskRoot, 'handoffs.jsonl')), content: preparedYield.postimage.toString('utf8') }]
|
|
258
|
+
: []),
|
|
259
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
260
|
+
{ path: relative(path.join(this.store.lockRoot(task.projectId), `${task.id}.json`)), content: null },
|
|
261
|
+
],
|
|
262
|
+
validate: () => {
|
|
263
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
264
|
+
const events = readDownstreamProofInvalidations(this.store, saved);
|
|
265
|
+
const readbackDirtyFiles = changedFiles(context.identity.repositoryRoot).sort();
|
|
266
|
+
if (saved.revision !== resultTask.revision
|
|
267
|
+
|| saved.status !== 'needs_fix'
|
|
268
|
+
|| events.at(-1)?.eventHash !== invalidation.event.eventHash
|
|
269
|
+
|| headCommit(context.identity.repositoryRoot) !== assessment.preservedHeadCommit
|
|
270
|
+
|| JSON.stringify(readbackDirtyFiles) !== JSON.stringify(assessment.dirtyFiles)
|
|
271
|
+
|| context.locks.inspect(task.id) !== null
|
|
272
|
+
|| (preparedYield && readTaskC1Posture(this.store, saved).state !== 'none')) {
|
|
273
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream proof recovery transaction readback failed.', {
|
|
274
|
+
taskId,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
task: this.store.readTask(task.projectId, task.id),
|
|
282
|
+
invalidation: invalidation.event,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
recoverDownstreamProofReplan(repository, taskId, expectedTaskRevision, actor, reason) {
|
|
286
|
+
const context = this.#mutationContext(repository);
|
|
287
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
288
|
+
assertExpectedRevision(task, expectedTaskRevision);
|
|
289
|
+
// This repair intentionally runs before Project Knowledge refresh: the exact
|
|
290
|
+
// dependency-only transport can make the current map stale, while the
|
|
291
|
+
// recovery assessment still binds the stranded Task to its recorded rebind.
|
|
292
|
+
const observation = this.observationSnapshot(repository).observation;
|
|
293
|
+
if (observation.kind !== 'assessment') {
|
|
294
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan recovery requires a coherent observation boundary.', {
|
|
295
|
+
observation,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
299
|
+
const assessment = assessDownstreamProofReplanRecovery(this.store, context.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
300
|
+
const environmentBlockers = [
|
|
301
|
+
...(!assessment.dependencyRecoveryCommit
|
|
302
|
+
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
303
|
+
: []),
|
|
304
|
+
...(observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
305
|
+
...(observation.pendingProjectTransactions.length > 0
|
|
306
|
+
|| observation.pendingMilestoneTransactions.length > 0
|
|
307
|
+
|| observation.pendingTaskTransactions.length > 0
|
|
308
|
+
? ['No pending Workflow transaction may exist.'] : []),
|
|
309
|
+
...(observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
310
|
+
...(observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
311
|
+
];
|
|
312
|
+
if (!assessment.applicable || !assessment.eligible || environmentBlockers.length > 0) {
|
|
313
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task is not eligible for bounded downstream-proof replan recovery.', {
|
|
314
|
+
taskId,
|
|
315
|
+
blockers: [...assessment.blockers, ...environmentBlockers],
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const now = this.now();
|
|
319
|
+
const resultTask = {
|
|
320
|
+
...task,
|
|
321
|
+
revision: task.revision + 1,
|
|
322
|
+
updatedAt: now.toISOString(),
|
|
323
|
+
status: 'needs_fix',
|
|
324
|
+
authorizations: task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
325
|
+
? { ...authorization, decision: 'superseded' }
|
|
326
|
+
: authorization),
|
|
327
|
+
review: emptyReview(),
|
|
328
|
+
resultHash: null,
|
|
329
|
+
evidenceHash: null,
|
|
330
|
+
blockReason: `${assessment.invalidation.activeStepId} downstream-proof recovery requires replacement of the full unfinished Step Plan before execution can resume.`,
|
|
331
|
+
};
|
|
332
|
+
const recovery = prepareDownstreamProofReplanRecovery(this.store, resultTask, assessment, { previousTaskRevision: task.revision, actor, reason }, now);
|
|
333
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
334
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
335
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
336
|
+
applyProjectTransaction({
|
|
337
|
+
projectRoot,
|
|
338
|
+
projectId: task.projectId,
|
|
339
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
340
|
+
kind: 'downstream-proof-replan-recovery',
|
|
341
|
+
now,
|
|
342
|
+
targets: [
|
|
343
|
+
{
|
|
344
|
+
path: relative(path.join(taskRoot, downstreamProofReplanRecoverySidecar())),
|
|
345
|
+
content: recovery.postimage,
|
|
346
|
+
},
|
|
347
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
348
|
+
],
|
|
349
|
+
validate: () => {
|
|
350
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
351
|
+
const events = readDownstreamProofReplanRecoveries(this.store, saved);
|
|
352
|
+
if (saved.revision !== resultTask.revision
|
|
353
|
+
|| saved.status !== 'needs_fix'
|
|
354
|
+
|| saved.planHash !== task.planHash
|
|
355
|
+
|| events.at(-1)?.eventHash !== recovery.event.eventHash
|
|
356
|
+
|| headCommit(context.identity.repositoryRoot) !== recovery.event.preservedHeadCommit
|
|
357
|
+
|| JSON.stringify(changedFiles(context.identity.repositoryRoot).sort())
|
|
358
|
+
!== JSON.stringify(recovery.event.dirtyFiles)) {
|
|
359
|
+
throw new WorkflowError('STATE_CORRUPT', 'Downstream-proof replan recovery transaction readback failed.', {
|
|
360
|
+
taskId,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
task: this.store.readTask(task.projectId, task.id),
|
|
368
|
+
recovery: recovery.event,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
148
371
|
correctiveReplanController(repository) {
|
|
149
372
|
const identity = resolveRepositoryIdentity(repository);
|
|
150
373
|
return createCorrectiveReplanPublicController({
|
|
@@ -378,6 +601,316 @@ export class WorkflowService {
|
|
|
378
601
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
379
602
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
380
603
|
}
|
|
604
|
+
materializeAuditedRemediationTask(repository, milestoneId, expectedMilestoneRevision, blockedTaskId, expectedBlockedTaskRevision, input) {
|
|
605
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
606
|
+
const observation = this.observationSnapshot(repository).observation;
|
|
607
|
+
if (observation.kind !== 'assessment'
|
|
608
|
+
|| observation.leases.length > 0
|
|
609
|
+
|| observation.coreTaskOperations.length > 0
|
|
610
|
+
|| observation.pendingProjectTransactions.length > 0
|
|
611
|
+
|| observation.pendingMilestoneTransactions.length > 0
|
|
612
|
+
|| observation.pendingTaskTransactions.length > 0
|
|
613
|
+
|| observation.corruptTaskTransactions.length > 0) {
|
|
614
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires a coherent, idle observation boundary.', { observation });
|
|
615
|
+
}
|
|
616
|
+
if (!isClean(identity.repositoryRoot)) {
|
|
617
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Audited remediation materialization requires a clean Milestone base checkout.', { changedFiles: changedFiles(identity.repositoryRoot) });
|
|
618
|
+
}
|
|
619
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
620
|
+
assertExpectedRevision(milestone, expectedMilestoneRevision);
|
|
621
|
+
if (milestone.status !== 'active' || currentBranch(identity.repositoryRoot) !== milestone.baseBranch) {
|
|
622
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation materialization requires the active Milestone base branch.', {
|
|
623
|
+
milestoneStatus: milestone.status,
|
|
624
|
+
expectedBranch: milestone.baseBranch,
|
|
625
|
+
actualBranch: currentBranch(identity.repositoryRoot),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
629
|
+
const blockedTask = this.store.readTask(identity.projectId, blockedTaskId);
|
|
630
|
+
assertExpectedRevision(blockedTask, expectedBlockedTaskRevision);
|
|
631
|
+
if (blockedTask.milestoneId !== milestone.id
|
|
632
|
+
|| blockedTask.status !== 'awaiting_execution_authorization'
|
|
633
|
+
|| !blockedTask.planHash
|
|
634
|
+
|| blockedTask.workspaceOwner !== null
|
|
635
|
+
|| blockedTask.steps.some((step) => step.status !== 'planned')) {
|
|
636
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation is available only for an unstarted Milestone Task with a rejected current Plan.', {
|
|
637
|
+
taskId: blockedTask.id,
|
|
638
|
+
taskStatus: blockedTask.status,
|
|
639
|
+
milestoneId: blockedTask.milestoneId,
|
|
640
|
+
workspaceOwner: blockedTask.workspaceOwner,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const blockedMembership = milestone.memberships.find((membership) => membership.taskId === blockedTask.id);
|
|
644
|
+
if (blockedMembership?.disposition !== 'required') {
|
|
645
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The blocked Task must remain a required Milestone member.', { taskId: blockedTask.id, disposition: blockedMembership?.disposition ?? null });
|
|
646
|
+
}
|
|
647
|
+
const audit = readPlanRiskAuditEvents(this.store, blockedTask).at(-1);
|
|
648
|
+
if (!audit
|
|
649
|
+
|| audit.decision !== 'stop-escalate'
|
|
650
|
+
|| audit.taskRevision !== blockedTask.revision
|
|
651
|
+
|| audit.briefHash !== blockedTask.briefHash
|
|
652
|
+
|| audit.planHash !== blockedTask.planHash) {
|
|
653
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation requires the current stop-escalate Plan Risk Audit.', {
|
|
654
|
+
taskId: blockedTask.id,
|
|
655
|
+
currentTaskRevision: blockedTask.revision,
|
|
656
|
+
currentBriefHash: blockedTask.briefHash,
|
|
657
|
+
currentPlanHash: blockedTask.planHash,
|
|
658
|
+
latestAudit: audit ?? null,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const duplicate = readMilestoneScopeChangeEvents(this.store, identity.projectId, milestone.id)
|
|
662
|
+
.find((event) => event.authorizationMode === 'audited-remediation'
|
|
663
|
+
&& event.remediationEvidence?.planRiskAuditEventHash === audit.eventHash);
|
|
664
|
+
if (duplicate) {
|
|
665
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'This blocking Plan Risk Audit already has a materialized remediation Task.', {
|
|
666
|
+
planRiskAuditEventId: audit.eventId,
|
|
667
|
+
remediationTaskId: duplicate.remediationEvidence?.remediationTaskId ?? null,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const discovery = this.store.readDiscovery(identity.projectId, input.discoveryId);
|
|
671
|
+
assertExpectedRevision(discovery, input.expectedDiscoveryRevision);
|
|
672
|
+
const readiness = assessDiscovery(discovery);
|
|
673
|
+
if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
|
|
674
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation Discovery is not ready to materialize.', {
|
|
675
|
+
discoveryId: discovery.id,
|
|
676
|
+
missing: readiness.missing,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const title = input.title.trim();
|
|
680
|
+
if (!title)
|
|
681
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Remediation Task title is required.');
|
|
682
|
+
const predecessorTaskIds = [...new Set(input.predecessorTaskIds.map((taskId) => taskId.trim()))].sort();
|
|
683
|
+
if (predecessorTaskIds.length === 0 || predecessorTaskIds.some((taskId) => !taskId)) {
|
|
684
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Audited remediation requires at least one explicit merged predecessor Task.');
|
|
685
|
+
}
|
|
686
|
+
const requiredPredecessorTaskIds = [...(blockedMembership.dependsOnTaskIds ?? [])].sort();
|
|
687
|
+
if (canonicalJsonStringify(predecessorTaskIds) !== canonicalJsonStringify(requiredPredecessorTaskIds)) {
|
|
688
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation must inherit the blocked Task exact direct predecessor frontier.', {
|
|
689
|
+
blockedTaskId: blockedTask.id,
|
|
690
|
+
requiredPredecessorTaskIds,
|
|
691
|
+
actualPredecessorTaskIds: predecessorTaskIds,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const membershipsByTaskId = new Map(milestone.memberships.map((membership) => [membership.taskId, membership]));
|
|
695
|
+
const ancestorTaskIds = new Set();
|
|
696
|
+
const visitAncestors = (taskId) => {
|
|
697
|
+
const membership = membershipsByTaskId.get(taskId);
|
|
698
|
+
for (const dependencyId of membership?.dependsOnTaskIds ?? []) {
|
|
699
|
+
if (ancestorTaskIds.has(dependencyId))
|
|
700
|
+
continue;
|
|
701
|
+
ancestorTaskIds.add(dependencyId);
|
|
702
|
+
visitAncestors(dependencyId);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
visitAncestors(blockedTask.id);
|
|
706
|
+
for (const predecessorTaskId of predecessorTaskIds) {
|
|
707
|
+
const predecessorMembership = membershipsByTaskId.get(predecessorTaskId);
|
|
708
|
+
const predecessor = this.store.readTask(identity.projectId, predecessorTaskId);
|
|
709
|
+
if (!ancestorTaskIds.has(predecessorTaskId)
|
|
710
|
+
|| predecessorMembership?.disposition !== 'required'
|
|
711
|
+
|| predecessor.status !== 'merged') {
|
|
712
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Every remediation predecessor must be a merged required ancestor of the blocked Task.', {
|
|
713
|
+
predecessorTaskId,
|
|
714
|
+
isAncestor: ancestorTaskIds.has(predecessorTaskId),
|
|
715
|
+
disposition: predecessorMembership?.disposition ?? null,
|
|
716
|
+
status: predecessor.status,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const now = this.now();
|
|
721
|
+
const timestamp = now.toISOString();
|
|
722
|
+
const remediationTaskId = createEntityId('TASK', now.getTime());
|
|
723
|
+
const briefContent = renderBrief(discovery, title);
|
|
724
|
+
const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
|
|
725
|
+
const briefHash = sha256Hex(normalizedBriefContent);
|
|
726
|
+
const remediationTask = {
|
|
727
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
728
|
+
revision: 1,
|
|
729
|
+
id: remediationTaskId,
|
|
730
|
+
projectId: identity.projectId,
|
|
731
|
+
createdAt: timestamp,
|
|
732
|
+
updatedAt: timestamp,
|
|
733
|
+
kind: 'task',
|
|
734
|
+
status: 'planning',
|
|
735
|
+
title,
|
|
736
|
+
slug: slugify(title),
|
|
737
|
+
discoveryId: discovery.id,
|
|
738
|
+
milestoneId: milestone.id,
|
|
739
|
+
repositoryRoot: identity.repositoryRoot,
|
|
740
|
+
baseBranch: milestone.baseBranch,
|
|
741
|
+
baseCommit: null,
|
|
742
|
+
taskBranch: null,
|
|
743
|
+
workspaceOwner: null,
|
|
744
|
+
briefHash,
|
|
745
|
+
planHash: null,
|
|
746
|
+
planObjective: null,
|
|
747
|
+
resultHash: null,
|
|
748
|
+
evidenceHash: null,
|
|
749
|
+
requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
|
|
750
|
+
acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
|
|
751
|
+
risks: discovery.assumptions,
|
|
752
|
+
knowledgeImpact: null,
|
|
753
|
+
knowledgeImpactReason: null,
|
|
754
|
+
knowledgeTargets: [],
|
|
755
|
+
knowledgeMapRevision: null,
|
|
756
|
+
knowledgeMapHash: null,
|
|
757
|
+
graphUse: null,
|
|
758
|
+
steps: [],
|
|
759
|
+
authorizations: [],
|
|
760
|
+
knowledgeRebinds: [],
|
|
761
|
+
review: emptyReview(),
|
|
762
|
+
blockReason: null,
|
|
763
|
+
mergedCommit: null,
|
|
764
|
+
systemCommits: [],
|
|
765
|
+
};
|
|
766
|
+
const consumedDiscovery = {
|
|
767
|
+
...discovery,
|
|
768
|
+
revision: discovery.revision + 1,
|
|
769
|
+
updatedAt: timestamp,
|
|
770
|
+
status: 'materialized',
|
|
771
|
+
materializedEntityId: remediationTaskId,
|
|
772
|
+
};
|
|
773
|
+
const remediationEvidence = {
|
|
774
|
+
blockedTaskId: blockedTask.id,
|
|
775
|
+
blockedTaskRevision: blockedTask.revision,
|
|
776
|
+
blockedBriefHash: blockedTask.briefHash,
|
|
777
|
+
blockedPlanHash: blockedTask.planHash,
|
|
778
|
+
planRiskAuditEventId: audit.eventId,
|
|
779
|
+
planRiskAuditEventHash: audit.eventHash,
|
|
780
|
+
remediationTaskId,
|
|
781
|
+
predecessorTaskIds,
|
|
782
|
+
};
|
|
783
|
+
const reason = `Audited remediation for ${blockedTask.id} after stop-escalate ${audit.eventId}.`;
|
|
784
|
+
const plan = {
|
|
785
|
+
outcome: milestone.outcome,
|
|
786
|
+
successSignal: milestone.successSignal,
|
|
787
|
+
acceptance: [...milestone.acceptance],
|
|
788
|
+
checks: [...milestone.checks],
|
|
789
|
+
memberships: [
|
|
790
|
+
...milestone.memberships.map((membership) => membership.taskId === blockedTask.id
|
|
791
|
+
? {
|
|
792
|
+
...membership,
|
|
793
|
+
dependsOnTaskIds: [...new Set([...(membership.dependsOnTaskIds ?? []), remediationTaskId])],
|
|
794
|
+
}
|
|
795
|
+
: { ...membership, dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])] }),
|
|
796
|
+
{
|
|
797
|
+
taskId: remediationTaskId,
|
|
798
|
+
disposition: 'required',
|
|
799
|
+
reason,
|
|
800
|
+
dependsOnTaskIds: predecessorTaskIds,
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
};
|
|
804
|
+
const readCandidateTask = (taskId) => taskId === remediationTaskId
|
|
805
|
+
? remediationTask
|
|
806
|
+
: this.store.readTask(identity.projectId, taskId);
|
|
807
|
+
const actor = `agent:milestone-remediation:${milestone.id}`;
|
|
808
|
+
const preparedCandidate = prepareMilestoneScopeChangeCandidate({
|
|
809
|
+
milestone,
|
|
810
|
+
plan,
|
|
811
|
+
readTask: readCandidateTask,
|
|
812
|
+
actor,
|
|
813
|
+
auditedRemediationEvidence: remediationEvidence,
|
|
814
|
+
});
|
|
815
|
+
const sourceAuthorization = [...milestone.authorizations]
|
|
816
|
+
.reverse()
|
|
817
|
+
.find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
|
|
818
|
+
if (!sourceAuthorization) {
|
|
819
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Audited remediation has no current Milestone execution authority.');
|
|
820
|
+
}
|
|
821
|
+
const derivedAuthorization = {
|
|
822
|
+
kind: 'execution',
|
|
823
|
+
decision: 'approved',
|
|
824
|
+
actor,
|
|
825
|
+
recordedAt: timestamp,
|
|
826
|
+
briefHash: null,
|
|
827
|
+
planHash: preparedCandidate.candidatePlanHash,
|
|
828
|
+
resultHash: null,
|
|
829
|
+
evidenceHash: sha256Hex(canonicalJsonStringify({
|
|
830
|
+
sourcePlanHash: sourceAuthorization.planHash,
|
|
831
|
+
sourceAuthorizationMode: sourceAuthorization.authorizationMode ?? 'human',
|
|
832
|
+
planRiskAuditEventHash: audit.eventHash,
|
|
833
|
+
remediationEvidence,
|
|
834
|
+
})),
|
|
835
|
+
headCommit: headCommit(identity.repositoryRoot),
|
|
836
|
+
reason: 'Derived from the current Milestone execution authority and exact blocking Plan Risk Audit.',
|
|
837
|
+
authorizationMode: 'corrective-derived',
|
|
838
|
+
};
|
|
839
|
+
const prepared = {
|
|
840
|
+
...preparedCandidate,
|
|
841
|
+
authorizations: [...preparedCandidate.authorizations, derivedAuthorization],
|
|
842
|
+
};
|
|
843
|
+
const bundle = prepareMilestoneScopeChangeBundle({
|
|
844
|
+
store: this.store,
|
|
845
|
+
milestone,
|
|
846
|
+
prepared,
|
|
847
|
+
now,
|
|
848
|
+
readTask: readCandidateTask,
|
|
849
|
+
});
|
|
850
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
851
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
852
|
+
applyProjectTransaction({
|
|
853
|
+
projectRoot,
|
|
854
|
+
projectId: identity.projectId,
|
|
855
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
856
|
+
kind: 'audited-remediation-materialization',
|
|
857
|
+
now,
|
|
858
|
+
targets: [
|
|
859
|
+
{
|
|
860
|
+
path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')),
|
|
861
|
+
content: `${JSON.stringify(consumedDiscovery, null, 2)}\n`,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'brief.md')),
|
|
865
|
+
content: normalizedBriefContent,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
path: relative(path.join(this.store.taskRoot(identity.projectId, remediationTaskId), 'state.json')),
|
|
869
|
+
content: `${JSON.stringify(remediationTask, null, 2)}\n`,
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')),
|
|
873
|
+
content: bundle.planContent,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')),
|
|
877
|
+
content: bundle.sidecarContent,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')),
|
|
881
|
+
content: bundle.stateContent,
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
validate: () => {
|
|
885
|
+
const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
|
|
886
|
+
const savedRemediationTask = this.store.readTask(identity.projectId, remediationTaskId);
|
|
887
|
+
const savedMilestone = this.readMilestoneCurrent(identity.projectId, milestone.id);
|
|
888
|
+
const savedBlockedMembership = savedMilestone.memberships
|
|
889
|
+
.find((membership) => membership.taskId === blockedTask.id);
|
|
890
|
+
const savedRemediationMembership = savedMilestone.memberships
|
|
891
|
+
.find((membership) => membership.taskId === remediationTaskId);
|
|
892
|
+
const savedAuthorization = [...savedMilestone.authorizations]
|
|
893
|
+
.reverse()
|
|
894
|
+
.find((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved');
|
|
895
|
+
if (savedDiscovery.materializedEntityId !== remediationTaskId
|
|
896
|
+
|| savedRemediationTask.milestoneId !== milestone.id
|
|
897
|
+
|| !savedBlockedMembership?.dependsOnTaskIds?.includes(remediationTaskId)
|
|
898
|
+
|| savedRemediationMembership?.disposition !== 'required'
|
|
899
|
+
|| savedAuthorization?.authorizationMode !== 'corrective-derived'
|
|
900
|
+
|| savedMilestone.status !== 'active'
|
|
901
|
+
|| locks.inspect(blockedTask.id) !== null) {
|
|
902
|
+
throw new WorkflowError('STATE_CORRUPT', 'Audited remediation transaction readback failed.');
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
return {
|
|
907
|
+
discovery: this.store.readDiscovery(identity.projectId, discovery.id),
|
|
908
|
+
remediationTask: this.store.readTask(identity.projectId, remediationTaskId),
|
|
909
|
+
blockedTask: this.store.readTask(identity.projectId, blockedTask.id),
|
|
910
|
+
milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
|
|
911
|
+
remediationEvidence,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
381
914
|
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
382
915
|
const { identity, locks } = this.#mutationContext(repository);
|
|
383
916
|
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
@@ -1038,8 +1571,13 @@ export class WorkflowService {
|
|
|
1038
1571
|
const passedBoundary = latestPassedVerifiedStepReviewEvent(this.store, task, event.stepId);
|
|
1039
1572
|
return !passedBoundary || passedBoundary.recordedAt <= event.recordedAt;
|
|
1040
1573
|
});
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1574
|
+
const overriddenStopDecisionEventIds = stopEscalateOverrideDecisionEventIds(this.store, task);
|
|
1575
|
+
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required'
|
|
1576
|
+
|| (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
|
|
1577
|
+
const downstreamReplanRecovered = readDownstreamProofReplanRecoveries(this.store, task)
|
|
1578
|
+
.some((event) => event.resultTaskRevision === task.revision && event.reboundPlanHash === task.planHash);
|
|
1579
|
+
const currentPlanRiskAudit = (task.status === 'needs_fix' || currentReplanDecisions.length > 0)
|
|
1580
|
+
&& !downstreamReplanRecovered
|
|
1043
1581
|
? readCurrentPlanRiskAudit(this.store, task)
|
|
1044
1582
|
: null;
|
|
1045
1583
|
const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
|
|
@@ -1172,6 +1710,10 @@ export class WorkflowService {
|
|
|
1172
1710
|
status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
|
|
1173
1711
|
evidence: null,
|
|
1174
1712
|
}));
|
|
1713
|
+
const invalidatedStepCommits = [...new Set([
|
|
1714
|
+
...(task.invalidatedStepCommits ?? []),
|
|
1715
|
+
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1716
|
+
])];
|
|
1175
1717
|
if (task.status === 'needs_fix' &&
|
|
1176
1718
|
task.review.status === 'failed' &&
|
|
1177
1719
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
@@ -1210,6 +1752,7 @@ export class WorkflowService {
|
|
|
1210
1752
|
knowledgeMapHash: plan.knowledgeMapHash,
|
|
1211
1753
|
graphUse: plan.graphUse,
|
|
1212
1754
|
steps: plannedSteps,
|
|
1755
|
+
invalidatedStepCommits,
|
|
1213
1756
|
authorizations,
|
|
1214
1757
|
review: emptyReview(),
|
|
1215
1758
|
resultHash: null,
|
|
@@ -1445,7 +1988,12 @@ export class WorkflowService {
|
|
|
1445
1988
|
requiredAction: 'task plan-set',
|
|
1446
1989
|
});
|
|
1447
1990
|
}
|
|
1448
|
-
const
|
|
1991
|
+
const correctiveAuthority = delegationGrantId === null
|
|
1992
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
1993
|
+
: null;
|
|
1994
|
+
const delegation = correctiveAuthority
|
|
1995
|
+
? null
|
|
1996
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1449
1997
|
const event = {
|
|
1450
1998
|
kind: 'execution',
|
|
1451
1999
|
decision: 'approved',
|
|
@@ -1457,7 +2005,11 @@ export class WorkflowService {
|
|
|
1457
2005
|
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1458
2006
|
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1459
2007
|
reason,
|
|
1460
|
-
authorizationMode:
|
|
2008
|
+
authorizationMode: correctiveAuthority
|
|
2009
|
+
? 'corrective-derived'
|
|
2010
|
+
: delegation
|
|
2011
|
+
? 'delegated'
|
|
2012
|
+
: 'human',
|
|
1461
2013
|
...(delegation ? { delegation } : {}),
|
|
1462
2014
|
mechanicalFeasibility,
|
|
1463
2015
|
};
|
|
@@ -1758,8 +2310,11 @@ export class WorkflowService {
|
|
|
1758
2310
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
1759
2311
|
}
|
|
1760
2312
|
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2313
|
+
const overriddenStopDecisionEventIds = stopEscalateOverrideDecisionEventIds(this.store, task);
|
|
1761
2314
|
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1762
|
-
.filter((event) => event.planHash === task.planHash
|
|
2315
|
+
.filter((event) => event.planHash === task.planHash
|
|
2316
|
+
&& event.decision !== 'continue-fix'
|
|
2317
|
+
&& !(event.decision === 'stop-escalate' && overriddenStopDecisionEventIds.has(event.eventId)));
|
|
1763
2318
|
if (currentPlanBlockingDecisions.length > 0) {
|
|
1764
2319
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run any Step while the current Plan has active blocking corrective decisions.`, {
|
|
1765
2320
|
taskId,
|
|
@@ -1797,14 +2352,26 @@ export class WorkflowService {
|
|
|
1797
2352
|
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1798
2353
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1799
2354
|
: null;
|
|
1800
|
-
const
|
|
2355
|
+
const stopOverrideContextRebind = staleDecision?.decision === 'stop-escalate'
|
|
2356
|
+
? readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, context.identity.repositoryRoot)
|
|
2357
|
+
: null;
|
|
2358
|
+
const gate = assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null, stopOverrideContextRebind?.stopDecision.eventId ?? null);
|
|
1801
2359
|
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1802
2360
|
}
|
|
1803
2361
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1804
2362
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1805
|
-
|
|
2363
|
+
const downstreamProofCarryover = step.status === 'planned' && dirty.length > 0
|
|
2364
|
+
? assessDownstreamProofCarryover(this.store, context.identity.repositoryRoot, task, stepId)
|
|
2365
|
+
: null;
|
|
2366
|
+
if (step.status === 'planned'
|
|
2367
|
+
&& dirty.length > 0
|
|
2368
|
+
&& !resumesCorrectiveReplan
|
|
2369
|
+
&& !downstreamProofCarryover?.eligible) {
|
|
1806
2370
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1807
2371
|
changedFiles: dirty,
|
|
2372
|
+
...(downstreamProofCarryover?.applicable
|
|
2373
|
+
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2374
|
+
: {}),
|
|
1808
2375
|
});
|
|
1809
2376
|
}
|
|
1810
2377
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
@@ -2035,7 +2602,7 @@ export class WorkflowService {
|
|
|
2035
2602
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2036
2603
|
reviewEventHash: null,
|
|
2037
2604
|
reviewerAttestationHash: null,
|
|
2038
|
-
}, planRiskAudit, this.now());
|
|
2605
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2039
2606
|
}
|
|
2040
2607
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
|
|
2041
2608
|
return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
|
|
@@ -2155,7 +2722,7 @@ export class WorkflowService {
|
|
|
2155
2722
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2156
2723
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
2157
2724
|
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
2158
|
-
}, planRiskAudit, this.now());
|
|
2725
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2159
2726
|
}
|
|
2160
2727
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
2161
2728
|
}
|
|
@@ -2183,7 +2750,7 @@ export class WorkflowService {
|
|
|
2183
2750
|
checksHash: reviewRecordedEvent.checksHash,
|
|
2184
2751
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
2185
2752
|
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
2186
|
-
}, planRiskAudit, this.now());
|
|
2753
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2187
2754
|
}
|
|
2188
2755
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2189
2756
|
}
|
|
@@ -2255,7 +2822,7 @@ export class WorkflowService {
|
|
|
2255
2822
|
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2256
2823
|
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2257
2824
|
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2258
|
-
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2825
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2259
2826
|
}
|
|
2260
2827
|
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2261
2828
|
return {
|
|
@@ -2491,16 +3058,23 @@ export class WorkflowService {
|
|
|
2491
3058
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
2492
3059
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
2493
3060
|
}
|
|
2494
|
-
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
2495
|
-
const
|
|
3061
|
+
setTaskResult(repository, taskId, expectedRevision, summary, limitations, writerToken, actor) {
|
|
3062
|
+
const context = this.#mutationContext(repository);
|
|
3063
|
+
const { identity } = context;
|
|
2496
3064
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2497
3065
|
assertExpectedRevision(task, expectedRevision);
|
|
2498
3066
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
2499
3067
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Result requires a passing independent review.');
|
|
2500
3068
|
}
|
|
2501
3069
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
3070
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
3071
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
2502
3072
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
2503
3073
|
const stagedResult = this.store.stageArtifact(root, 'result.md', renderResult(task, summary, limitations));
|
|
3074
|
+
// Human final acceptance is an intentionally unbounded pause. Release the
|
|
3075
|
+
// execution lease before entering that gate so elapsed human time cannot
|
|
3076
|
+
// turn a successful Task into a repository-wide stale-lock repair route.
|
|
3077
|
+
context.locks.release(taskId, lease.token);
|
|
2504
3078
|
const saved = this.store.writeTask({ ...task, status: 'awaiting_final_acceptance', resultHash: stagedResult.hash, blockReason: null }, expectedRevision);
|
|
2505
3079
|
this.store.publishArtifact(stagedResult);
|
|
2506
3080
|
return saved;
|
|
@@ -2520,7 +3094,12 @@ export class WorkflowService {
|
|
|
2520
3094
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
2521
3095
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
2522
3096
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
2523
|
-
const
|
|
3097
|
+
const correctiveAuthority = delegationGrantId === null
|
|
3098
|
+
? this.currentCorrectiveDerivedTaskAuthority(identity.projectId, task, actor)
|
|
3099
|
+
: null;
|
|
3100
|
+
const delegation = correctiveAuthority
|
|
3101
|
+
? null
|
|
3102
|
+
: this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
|
|
2524
3103
|
const event = {
|
|
2525
3104
|
kind: 'final_acceptance',
|
|
2526
3105
|
decision: 'approved',
|
|
@@ -2532,7 +3111,11 @@ export class WorkflowService {
|
|
|
2532
3111
|
evidenceHash,
|
|
2533
3112
|
headCommit: headCommit(identity.repositoryRoot),
|
|
2534
3113
|
reason: '',
|
|
2535
|
-
authorizationMode:
|
|
3114
|
+
authorizationMode: correctiveAuthority
|
|
3115
|
+
? 'corrective-derived'
|
|
3116
|
+
: delegation
|
|
3117
|
+
? 'delegated'
|
|
3118
|
+
: 'human',
|
|
2536
3119
|
...(delegation ? { delegation } : {}),
|
|
2537
3120
|
};
|
|
2538
3121
|
return this.store.writeTask({
|
|
@@ -2763,17 +3346,304 @@ export class WorkflowService {
|
|
|
2763
3346
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2764
3347
|
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2765
3348
|
}
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
const
|
|
2774
|
-
|
|
3349
|
+
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
3350
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3351
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3352
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step provenance recovery requires a coherent observation boundary.', {
|
|
3353
|
+
observation: snapshot.observation,
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3357
|
+
if (!task)
|
|
3358
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3359
|
+
return this.inspectHistoricalStepProvenanceCandidate(snapshot, task);
|
|
3360
|
+
}
|
|
3361
|
+
historicalStepDependencyRecoveryPreflight(repository, taskId) {
|
|
3362
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3363
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3364
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Historical Step dependency recovery requires a coherent observation boundary.', {
|
|
3365
|
+
observation: snapshot.observation,
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3369
|
+
if (!task)
|
|
3370
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3371
|
+
let candidateParent = null;
|
|
3372
|
+
try {
|
|
3373
|
+
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
3374
|
+
}
|
|
3375
|
+
catch {
|
|
3376
|
+
candidateParent = null;
|
|
3377
|
+
}
|
|
3378
|
+
const assessment = candidateParent
|
|
3379
|
+
? this.inspectHistoricalStepProvenanceCandidate(snapshot, task, {
|
|
3380
|
+
historyBoundary: 'HEAD^',
|
|
3381
|
+
preservedHeadCommit: candidateParent,
|
|
3382
|
+
})
|
|
3383
|
+
: null;
|
|
3384
|
+
if (!assessment?.eligible
|
|
3385
|
+
|| !assessment.activeStepId
|
|
3386
|
+
|| !assessment.commitSha
|
|
3387
|
+
|| !task.planHash) {
|
|
3388
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3389
|
+
return {
|
|
3390
|
+
...ordinary,
|
|
3391
|
+
eligible: false,
|
|
3392
|
+
action: null,
|
|
3393
|
+
historicalStepProvenance: null,
|
|
3394
|
+
blockers: [...new Set([
|
|
3395
|
+
...ordinary.blockers,
|
|
3396
|
+
...(assessment?.blockers ?? []),
|
|
3397
|
+
'The dependency candidate parent must contain exactly one accepted and strictly reviewed historical Step commit omitted by the current corrective Plan.',
|
|
3398
|
+
])],
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
let dirtyWorktreeHash;
|
|
3402
|
+
try {
|
|
3403
|
+
dirtyWorktreeHash = hashDirtyWorktree(snapshot.identity.repositoryRoot, assessment.dirtyFiles);
|
|
3404
|
+
}
|
|
3405
|
+
catch (error) {
|
|
3406
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3407
|
+
return {
|
|
3408
|
+
...ordinary,
|
|
3409
|
+
eligible: false,
|
|
3410
|
+
action: null,
|
|
3411
|
+
historicalStepProvenance: null,
|
|
3412
|
+
blockers: [...new Set([
|
|
3413
|
+
...ordinary.blockers,
|
|
3414
|
+
error instanceof Error ? error.message : String(error),
|
|
3415
|
+
])],
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, {
|
|
3419
|
+
activeStepId: assessment.activeStepId,
|
|
3420
|
+
planHash: task.planHash,
|
|
3421
|
+
dirtyFiles: assessment.dirtyFiles,
|
|
3422
|
+
dirtyWorktreeHash,
|
|
3423
|
+
retainedCommitSha: assessment.commitSha,
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
recoverHistoricalStepProvenance(repository, taskId, expectedRevision, commitSha, actor, reason) {
|
|
3427
|
+
const normalizedActor = actor.trim();
|
|
3428
|
+
const normalizedReason = reason.trim();
|
|
3429
|
+
if (!normalizedActor)
|
|
3430
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires an actor.');
|
|
3431
|
+
if (!normalizedReason)
|
|
3432
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Historical Step provenance recovery requires a reason.');
|
|
3433
|
+
const context = this.#mutationContext(repository);
|
|
3434
|
+
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
3435
|
+
const prior = current.historicalStepProvenanceRecoveries?.find((record) => record.commitSha === commitSha);
|
|
3436
|
+
if (prior && (current.invalidatedStepCommits ?? []).includes(commitSha)) {
|
|
3437
|
+
validateTaskHistory(context.identity.repositoryRoot, current);
|
|
3438
|
+
return current;
|
|
3439
|
+
}
|
|
3440
|
+
assertExpectedRevision(current, expectedRevision);
|
|
3441
|
+
const preflight = this.historicalStepProvenanceRecoveryPreflight(repository, taskId);
|
|
3442
|
+
if (!preflight.eligible || preflight.commitSha !== commitSha || !preflight.sourceFinalAcceptanceRecordedAt) {
|
|
3443
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded historical Step provenance recovery preflight failed.', {
|
|
3444
|
+
taskId,
|
|
3445
|
+
requestedCommit: commitSha,
|
|
3446
|
+
candidateCommit: preflight.commitSha,
|
|
3447
|
+
blockers: preflight.blockers,
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
const sourceAcceptance = current.authorizations.find((authorization) => authorization.kind === 'final_acceptance'
|
|
3451
|
+
&& ['approved', 'superseded'].includes(authorization.decision)
|
|
3452
|
+
&& authorization.headCommit === commitSha
|
|
3453
|
+
&& authorization.recordedAt === preflight.sourceFinalAcceptanceRecordedAt
|
|
3454
|
+
&& authorization.planHash
|
|
3455
|
+
&& authorization.resultHash
|
|
3456
|
+
&& authorization.evidenceHash);
|
|
3457
|
+
if (!sourceAcceptance?.planHash || !sourceAcceptance.resultHash || !sourceAcceptance.evidenceHash) {
|
|
3458
|
+
throw new WorkflowError('STATE_CONFLICT', 'The source final acceptance changed after historical provenance preflight.');
|
|
3459
|
+
}
|
|
3460
|
+
const saved = context.store.writeTask({
|
|
3461
|
+
...current,
|
|
3462
|
+
invalidatedStepCommits: [...new Set([...(current.invalidatedStepCommits ?? []), commitSha])],
|
|
3463
|
+
historicalStepProvenanceRecoveries: [
|
|
3464
|
+
...(current.historicalStepProvenanceRecoveries ?? []),
|
|
3465
|
+
{
|
|
3466
|
+
commitSha,
|
|
3467
|
+
actor: normalizedActor,
|
|
3468
|
+
reason: normalizedReason,
|
|
3469
|
+
recordedAt: this.now().toISOString(),
|
|
3470
|
+
sourceFinalAcceptanceRecordedAt: sourceAcceptance.recordedAt,
|
|
3471
|
+
sourcePlanHash: sourceAcceptance.planHash,
|
|
3472
|
+
sourceResultHash: sourceAcceptance.resultHash,
|
|
3473
|
+
sourceEvidenceHash: sourceAcceptance.evidenceHash,
|
|
3474
|
+
},
|
|
3475
|
+
],
|
|
3476
|
+
}, expectedRevision);
|
|
3477
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
3478
|
+
return saved;
|
|
3479
|
+
}
|
|
3480
|
+
activeDownstreamProofDependencyRecoveryPreflight(repository, taskId) {
|
|
3481
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3482
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3483
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Active downstream-proof dependency recovery requires a coherent observation boundary.', {
|
|
3484
|
+
observation: snapshot.observation,
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3488
|
+
if (!task)
|
|
3489
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3490
|
+
const assessment = assessDownstreamProofRecovery(this.store, snapshot.identity.repositoryRoot, task, { historyBoundary: 'HEAD^', preservedHeadCommit: headCommit(snapshot.identity.repositoryRoot) });
|
|
3491
|
+
if (!assessment.applicable
|
|
3492
|
+
|| !assessment.eligible
|
|
3493
|
+
|| !assessment.activeStepId
|
|
3494
|
+
|| !assessment.dirtyWorktreeHash
|
|
3495
|
+
|| !task.planHash) {
|
|
3496
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3497
|
+
return {
|
|
3498
|
+
...ordinary,
|
|
3499
|
+
eligible: false,
|
|
3500
|
+
action: null,
|
|
3501
|
+
activeDownstreamProof: null,
|
|
3502
|
+
blockers: [...new Set([
|
|
3503
|
+
...ordinary.blockers,
|
|
3504
|
+
...assessment.blockers,
|
|
3505
|
+
'The Task must have one eligible active downstream-proof predecessor invalidation at the candidate parent boundary.',
|
|
3506
|
+
])],
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, {
|
|
3510
|
+
activeStepId: assessment.activeStepId,
|
|
3511
|
+
planHash: task.planHash,
|
|
3512
|
+
dirtyFiles: assessment.dirtyFiles,
|
|
3513
|
+
dirtyWorktreeHash: assessment.dirtyWorktreeHash,
|
|
3514
|
+
invalidatedStepIds: assessment.invalidatedSteps.map((step) => step.stepId),
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
downstreamProofReplanUpdatePreflight(repository, taskId) {
|
|
3518
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3519
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3520
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan update preflight requires a coherent observation boundary.', {
|
|
3521
|
+
observation: snapshot.observation,
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3525
|
+
if (!task)
|
|
3526
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3527
|
+
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
3528
|
+
const assessment = assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, { currentPlanRiskAuditExists });
|
|
3529
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
3530
|
+
const sourceVersion = '2.0.0-beta.12.6';
|
|
3531
|
+
const versionSurfaces = {
|
|
3532
|
+
declared: versions.declared,
|
|
3533
|
+
locked: versions.locked,
|
|
3534
|
+
installed: versions.installed,
|
|
3535
|
+
currentBranch: versions.currentBranch,
|
|
3536
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
3537
|
+
};
|
|
3538
|
+
const blockers = [
|
|
3539
|
+
...assessment.blockers,
|
|
3540
|
+
...(String(PACKAGE_VERSION) === sourceVersion ? ['Recovery runner must be newer than the beta.12.6 source package.'] : []),
|
|
3541
|
+
...Object.entries(versionSurfaces)
|
|
3542
|
+
.filter(([, version]) => version !== sourceVersion)
|
|
3543
|
+
.map(([surface]) => `${surface} Workflow version must equal the stranded source ${sourceVersion}.`),
|
|
3544
|
+
...(snapshot.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
3545
|
+
...(snapshot.observation.pendingProjectTransactions.length > 0
|
|
3546
|
+
|| snapshot.observation.pendingMilestoneTransactions.length > 0
|
|
3547
|
+
|| snapshot.observation.pendingTaskTransactions.length > 0
|
|
3548
|
+
? ['No pending Workflow transaction may exist.'] : []),
|
|
3549
|
+
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
3550
|
+
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
3551
|
+
];
|
|
3552
|
+
return {
|
|
3553
|
+
readOnly: true,
|
|
3554
|
+
eligible: assessment.applicable && assessment.eligible && blockers.length === 0,
|
|
3555
|
+
action: assessment.applicable && assessment.eligible && blockers.length === 0
|
|
3556
|
+
? 'update downstream-proof-replan-transport'
|
|
3557
|
+
: null,
|
|
3558
|
+
projectId: task.projectId,
|
|
3559
|
+
taskId: task.id,
|
|
3560
|
+
taskRevision: task.revision,
|
|
3561
|
+
sourceVersion,
|
|
3562
|
+
targetVersion: PACKAGE_VERSION,
|
|
3563
|
+
sourceHeadCommit: assessment.invalidation?.preservedHeadCommit ?? null,
|
|
3564
|
+
taskBranch: task.taskBranch,
|
|
3565
|
+
baseBranch: task.baseBranch,
|
|
3566
|
+
dirtyFiles: assessment.compatibility?.dirtyFiles ?? [],
|
|
3567
|
+
dirtyWorktreeHash: assessment.compatibility?.dirtyWorktreeHash ?? null,
|
|
3568
|
+
requiredTransport: [
|
|
3569
|
+
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
3570
|
+
'Commit only the same dependency files on the Task branch while preserving all product bytes.',
|
|
3571
|
+
'Install the exact target package, then follow fresh next to dependency registration.',
|
|
3572
|
+
],
|
|
3573
|
+
blockers,
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
downstreamProofReplanDependencyRecoveryPreflight(repository, taskId) {
|
|
3577
|
+
const snapshot = this.observationSnapshot(repository);
|
|
3578
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
3579
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Downstream-proof replan dependency recovery requires a coherent observation boundary.', {
|
|
3580
|
+
observation: snapshot.observation,
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3584
|
+
if (!task)
|
|
3585
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3586
|
+
let candidateParent = null;
|
|
3587
|
+
try {
|
|
3588
|
+
candidateParent = runGit(snapshot.identity.repositoryRoot, ['rev-parse', 'HEAD^']);
|
|
3589
|
+
}
|
|
3590
|
+
catch {
|
|
3591
|
+
candidateParent = null;
|
|
3592
|
+
}
|
|
3593
|
+
const currentPlanRiskAuditExists = readPlanRiskAuditEvents(this.store, task).some((event) => event.taskRevision === task.revision && event.planHash === task.planHash);
|
|
3594
|
+
const assessment = candidateParent
|
|
3595
|
+
? assessDownstreamProofReplanRecovery(this.store, snapshot.identity.repositoryRoot, task, {
|
|
3596
|
+
historyBoundary: 'HEAD^',
|
|
3597
|
+
preservedHeadCommit: candidateParent,
|
|
3598
|
+
currentPlanRiskAuditExists,
|
|
3599
|
+
})
|
|
3600
|
+
: null;
|
|
3601
|
+
if (!assessment?.applicable || !assessment.eligible || !assessment.compatibility) {
|
|
3602
|
+
const ordinary = this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
3603
|
+
return {
|
|
3604
|
+
...ordinary,
|
|
3605
|
+
eligible: false,
|
|
3606
|
+
action: null,
|
|
3607
|
+
activeDownstreamProof: null,
|
|
3608
|
+
downstreamProofReplan: null,
|
|
3609
|
+
blockers: [...new Set([
|
|
3610
|
+
...ordinary.blockers,
|
|
3611
|
+
...(assessment?.blockers ?? []),
|
|
3612
|
+
'The Task must preserve the exact beta.12.6 downstream-proof invalidation and accidental knowledge-rebind boundary.',
|
|
3613
|
+
])],
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
return this.inspectDependencyProvenanceCandidate(snapshot, task, null, assessment.compatibility);
|
|
3617
|
+
}
|
|
3618
|
+
recoverDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3619
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, false);
|
|
3620
|
+
}
|
|
3621
|
+
recoverActiveDownstreamProofDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3622
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, true);
|
|
3623
|
+
}
|
|
3624
|
+
recoverDownstreamProofReplanDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3625
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'downstream-replan');
|
|
3626
|
+
}
|
|
3627
|
+
recoverHistoricalStepDependencyProvenance(repository, taskId, expectedRevision, actor, reason) {
|
|
3628
|
+
return this.recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, 'historical-step');
|
|
3629
|
+
}
|
|
3630
|
+
recoverDependencyProvenanceCandidate(repository, taskId, expectedRevision, actor, reason, recoveryMode) {
|
|
3631
|
+
const normalizedActor = actor.trim();
|
|
3632
|
+
const normalizedReason = reason.trim();
|
|
3633
|
+
if (!normalizedActor)
|
|
3634
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires an actor.');
|
|
3635
|
+
if (!normalizedReason)
|
|
3636
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Dependency provenance recovery requires a reason.');
|
|
3637
|
+
const context = this.#mutationContext(repository);
|
|
3638
|
+
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
2775
3639
|
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2776
|
-
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead
|
|
3640
|
+
const prior = current.dependencyProvenanceRecoveries?.find((record) => record.commitSha === currentHead
|
|
3641
|
+
&& record.packageVersion === PACKAGE_VERSION
|
|
3642
|
+
&& (recoveryMode === 'downstream-replan'
|
|
3643
|
+
? Boolean(record.downstreamProofReplan)
|
|
3644
|
+
: recoveryMode === 'historical-step'
|
|
3645
|
+
? Boolean(record.historicalStepProvenance)
|
|
3646
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
2777
3647
|
if (prior && current.systemCommits.includes(currentHead))
|
|
2778
3648
|
return current;
|
|
2779
3649
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -2781,7 +3651,13 @@ export class WorkflowService {
|
|
|
2781
3651
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2782
3652
|
if (!task)
|
|
2783
3653
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2784
|
-
const preflight =
|
|
3654
|
+
const preflight = recoveryMode === 'downstream-replan'
|
|
3655
|
+
? this.downstreamProofReplanDependencyRecoveryPreflight(repository, taskId)
|
|
3656
|
+
: recoveryMode === 'historical-step'
|
|
3657
|
+
? this.historicalStepDependencyRecoveryPreflight(repository, taskId)
|
|
3658
|
+
: recoveryMode
|
|
3659
|
+
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
3660
|
+
: this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2785
3661
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
2786
3662
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
2787
3663
|
taskId,
|
|
@@ -2803,6 +3679,15 @@ export class WorkflowService {
|
|
|
2803
3679
|
reason: normalizedReason,
|
|
2804
3680
|
recordedAt: this.now().toISOString(),
|
|
2805
3681
|
retainedHistoricalCommits: preflight.retainedHistoricalCommits,
|
|
3682
|
+
...(preflight.activeDownstreamProof
|
|
3683
|
+
? { activeDownstreamProof: preflight.activeDownstreamProof }
|
|
3684
|
+
: {}),
|
|
3685
|
+
...(preflight.downstreamProofReplan
|
|
3686
|
+
? { downstreamProofReplan: preflight.downstreamProofReplan }
|
|
3687
|
+
: {}),
|
|
3688
|
+
...(preflight.historicalStepProvenance
|
|
3689
|
+
? { historicalStepProvenance: preflight.historicalStepProvenance }
|
|
3690
|
+
: {}),
|
|
2806
3691
|
},
|
|
2807
3692
|
],
|
|
2808
3693
|
}, expectedRevision);
|
|
@@ -3202,9 +4087,53 @@ export class WorkflowService {
|
|
|
3202
4087
|
...(dependency ? { dependency } : {}),
|
|
3203
4088
|
});
|
|
3204
4089
|
}
|
|
4090
|
+
const prioritizedRemediationTask = status.milestones.flatMap((milestone) => {
|
|
4091
|
+
if (milestone.status !== 'active')
|
|
4092
|
+
return [];
|
|
4093
|
+
const event = [...readMilestoneScopeChangeEvents(this.store, status.projectId, milestone.id)]
|
|
4094
|
+
.reverse()
|
|
4095
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
4096
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
4097
|
+
&& candidate.remediationEvidence?.remediationTaskId);
|
|
4098
|
+
if (!event?.remediationEvidence)
|
|
4099
|
+
return [];
|
|
4100
|
+
const task = tasksById.get(event.remediationEvidence.remediationTaskId);
|
|
4101
|
+
return task
|
|
4102
|
+
&& taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4103
|
+
? [task]
|
|
4104
|
+
: [];
|
|
4105
|
+
}).at(0) ?? null;
|
|
3205
4106
|
const activeTask = requestedTask
|
|
4107
|
+
?? prioritizedRemediationTask
|
|
3206
4108
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3207
4109
|
if (activeTask) {
|
|
4110
|
+
const historicalStepProvenanceRecovery = this.historicalStepProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4111
|
+
if (historicalStepProvenanceRecovery.eligible) {
|
|
4112
|
+
return withAdoption({
|
|
4113
|
+
scope: 'task',
|
|
4114
|
+
id: activeTask.id,
|
|
4115
|
+
status: activeTask.status,
|
|
4116
|
+
action: 'task historical-step-provenance-recover',
|
|
4117
|
+
revision: activeTask.revision,
|
|
4118
|
+
historicalStepProvenanceRecovery,
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
const historicalStepDependencyRecovery = this.historicalStepDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4122
|
+
if (historicalStepDependencyRecovery.eligible) {
|
|
4123
|
+
return withAdoption({
|
|
4124
|
+
scope: 'task',
|
|
4125
|
+
id: activeTask.id,
|
|
4126
|
+
status: activeTask.status,
|
|
4127
|
+
action: 'update historical-step-dependency-recover',
|
|
4128
|
+
revision: activeTask.revision,
|
|
4129
|
+
dependencyProvenanceRecovery: historicalStepDependencyRecovery,
|
|
4130
|
+
compatibilityBoundary: {
|
|
4131
|
+
preservesWorktree: true,
|
|
4132
|
+
prerequisite: 'The active Milestone base and Task branch contain only the exact target dependency commits; the candidate parent still binds the reviewed historical Step commit.',
|
|
4133
|
+
continuation: 'Register the dependency commit and historical provenance atomically, then follow fresh next.',
|
|
4134
|
+
},
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
3208
4137
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3209
4138
|
if (dependencyRecovery.eligible) {
|
|
3210
4139
|
return withAdoption({
|
|
@@ -3216,6 +4145,38 @@ export class WorkflowService {
|
|
|
3216
4145
|
dependencyProvenanceRecovery: dependencyRecovery,
|
|
3217
4146
|
});
|
|
3218
4147
|
}
|
|
4148
|
+
const activeDownstreamProofDependencyRecovery = this.activeDownstreamProofDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4149
|
+
if (activeDownstreamProofDependencyRecovery.eligible) {
|
|
4150
|
+
return withAdoption({
|
|
4151
|
+
scope: 'task',
|
|
4152
|
+
id: activeTask.id,
|
|
4153
|
+
status: activeTask.status,
|
|
4154
|
+
action: 'update downstream-proof-dependency-recover',
|
|
4155
|
+
revision: activeTask.revision,
|
|
4156
|
+
dependencyProvenanceRecovery: activeDownstreamProofDependencyRecovery,
|
|
4157
|
+
compatibilityBoundary: {
|
|
4158
|
+
preservesWorktree: true,
|
|
4159
|
+
prerequisite: 'The active Milestone base and Task branch already contain only the exact dependency update commits.',
|
|
4160
|
+
continuation: 'Run task downstream-proof-recover after this provenance registration.',
|
|
4161
|
+
},
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
const downstreamProofReplanDependencyRecovery = this.downstreamProofReplanDependencyRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
4165
|
+
if (downstreamProofReplanDependencyRecovery.eligible) {
|
|
4166
|
+
return withAdoption({
|
|
4167
|
+
scope: 'task',
|
|
4168
|
+
id: activeTask.id,
|
|
4169
|
+
status: activeTask.status,
|
|
4170
|
+
action: 'update downstream-proof-replan-dependency-recover',
|
|
4171
|
+
revision: activeTask.revision,
|
|
4172
|
+
dependencyProvenanceRecovery: downstreamProofReplanDependencyRecovery,
|
|
4173
|
+
compatibilityBoundary: {
|
|
4174
|
+
preservesWorktree: true,
|
|
4175
|
+
prerequisite: 'A newer package preflight approved the exact beta.12.6 stranded replan transport before dependency-only commits were created.',
|
|
4176
|
+
continuation: 'Register this dependency commit, then run task downstream-proof-replan-recover.',
|
|
4177
|
+
},
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
3219
4180
|
const activeTaskMilestone = activeTask.milestoneId
|
|
3220
4181
|
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
3221
4182
|
: null;
|
|
@@ -3239,36 +4200,200 @@ export class WorkflowService {
|
|
|
3239
4200
|
},
|
|
3240
4201
|
}
|
|
3241
4202
|
: value);
|
|
4203
|
+
const currentPlanRiskAuditExists = activeTask.planHash
|
|
4204
|
+
? readPlanRiskAuditEvents(this.store, activeTask).some((event) => event.taskRevision === activeTask.revision && event.planHash === activeTask.planHash)
|
|
4205
|
+
: false;
|
|
4206
|
+
const downstreamReplanRecovery = assessDownstreamProofReplanRecovery(this.store, repositoryRoot, activeTask, { currentPlanRiskAuditExists });
|
|
4207
|
+
if (downstreamReplanRecovery.applicable) {
|
|
4208
|
+
const environmentBlockers = [
|
|
4209
|
+
...(!downstreamReplanRecovery.dependencyRecoveryCommit
|
|
4210
|
+
? ['The exact beta.12.7 dependency compatibility must be registered before Task recovery.']
|
|
4211
|
+
: []),
|
|
4212
|
+
...(status.observation.leases.length > 0 ? ['No active or stale Task writer lease may exist.'] : []),
|
|
4213
|
+
...(status.observation.pendingProjectTransactions.length > 0
|
|
4214
|
+
|| status.observation.pendingMilestoneTransactions.length > 0
|
|
4215
|
+
|| status.observation.pendingTaskTransactions.length > 0
|
|
4216
|
+
? ['No pending Workflow transaction may exist.'] : []),
|
|
4217
|
+
...(status.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4218
|
+
...(status.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4219
|
+
];
|
|
4220
|
+
return withAdoption(withMilestoneCancellation(downstreamReplanRecovery.eligible && environmentBlockers.length === 0
|
|
4221
|
+
? {
|
|
4222
|
+
scope: 'task',
|
|
4223
|
+
id: activeTask.id,
|
|
4224
|
+
status: activeTask.status,
|
|
4225
|
+
action: 'task downstream-proof-replan-recover',
|
|
4226
|
+
revision: activeTask.revision,
|
|
4227
|
+
downstreamProofReplanRecovery: {
|
|
4228
|
+
state: 'eligible',
|
|
4229
|
+
invalidationEventId: downstreamReplanRecovery.invalidation.eventId,
|
|
4230
|
+
sourcePlanHash: downstreamReplanRecovery.invalidation.planHash,
|
|
4231
|
+
reboundPlanHash: activeTask.planHash,
|
|
4232
|
+
dirtyFiles: downstreamReplanRecovery.compatibility.dirtyFiles,
|
|
4233
|
+
dirtyWorktreeHash: downstreamReplanRecovery.compatibility.dirtyWorktreeHash,
|
|
4234
|
+
dependencyRecoveryCommit: downstreamReplanRecovery.dependencyRecoveryCommit,
|
|
4235
|
+
boundedEffect: 'restore-needs-fix-and-return-same-task-to-plan-set',
|
|
4236
|
+
},
|
|
4237
|
+
}
|
|
4238
|
+
: {
|
|
4239
|
+
scope: 'task',
|
|
4240
|
+
id: activeTask.id,
|
|
4241
|
+
status: activeTask.status,
|
|
4242
|
+
action: 'doctor',
|
|
4243
|
+
blockedAction: 'task downstream-proof-replan-recover',
|
|
4244
|
+
revision: activeTask.revision,
|
|
4245
|
+
downstreamProofReplanRecovery: {
|
|
4246
|
+
state: 'blocked',
|
|
4247
|
+
blockers: [...downstreamReplanRecovery.blockers, ...environmentBlockers],
|
|
4248
|
+
},
|
|
4249
|
+
}));
|
|
4250
|
+
}
|
|
3242
4251
|
let activePlanRiskAudit = null;
|
|
3243
4252
|
let planRiskAuditRecovery = null;
|
|
3244
|
-
|
|
4253
|
+
const requiresReplacementPlan = activeTask.status === 'needs_fix'
|
|
4254
|
+
&& !activeTask.steps.some((step) => step.status === 'failed');
|
|
4255
|
+
if (activeTask.planHash && !requiresReplacementPlan) {
|
|
3245
4256
|
try {
|
|
3246
4257
|
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
3247
4258
|
}
|
|
3248
4259
|
catch (error) {
|
|
3249
4260
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3250
4261
|
throw error;
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
status
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
4262
|
+
const blockingDecision = error.details.decision;
|
|
4263
|
+
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
4264
|
+
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
4265
|
+
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
4266
|
+
.reverse()
|
|
4267
|
+
.find((event) => event.authorizationMode === 'audited-remediation'
|
|
4268
|
+
&& event.remediationEvidence?.planRiskAuditEventHash === blockingAudit.eventHash) ?? null
|
|
4269
|
+
: null;
|
|
4270
|
+
const existingRemediationTask = existingRemediationEvent?.remediationEvidence
|
|
4271
|
+
? tasksById.get(existingRemediationEvent.remediationEvidence.remediationTaskId) ?? null
|
|
4272
|
+
: null;
|
|
4273
|
+
const remediationDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status)) ?? null;
|
|
4274
|
+
const remediationMilestone = activeTask.milestoneId
|
|
4275
|
+
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
4276
|
+
: null;
|
|
4277
|
+
const remediationMemberships = remediationMilestone
|
|
4278
|
+
? new Map(remediationMilestone.memberships.map((membership) => [membership.taskId, membership]))
|
|
4279
|
+
: new Map();
|
|
4280
|
+
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
4281
|
+
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
4282
|
+
&& remediationMilestone?.status === 'active'
|
|
4283
|
+
&& remediationMembership?.disposition === 'required';
|
|
4284
|
+
const remediationAncestorIds = new Set();
|
|
4285
|
+
const visitRemediationAncestors = (candidateTaskId) => {
|
|
4286
|
+
for (const predecessorTaskId of remediationMemberships.get(candidateTaskId)?.dependsOnTaskIds ?? []) {
|
|
4287
|
+
if (remediationAncestorIds.has(predecessorTaskId))
|
|
4288
|
+
continue;
|
|
4289
|
+
remediationAncestorIds.add(predecessorTaskId);
|
|
4290
|
+
visitRemediationAncestors(predecessorTaskId);
|
|
4291
|
+
}
|
|
3265
4292
|
};
|
|
4293
|
+
visitRemediationAncestors(activeTask.id);
|
|
4294
|
+
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
4295
|
+
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
4296
|
+
.sort();
|
|
4297
|
+
planRiskAuditRecovery = blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
4298
|
+
? {
|
|
4299
|
+
scope: 'task',
|
|
4300
|
+
id: activeTask.id,
|
|
4301
|
+
status: activeTask.status,
|
|
4302
|
+
action: existingRemediationTask?.status === 'merged'
|
|
4303
|
+
? 'task plan-set'
|
|
4304
|
+
: existingRemediationTask
|
|
4305
|
+
? 'doctor'
|
|
4306
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4307
|
+
? 'milestone remediation-materialize'
|
|
4308
|
+
: remediationDiscovery
|
|
4309
|
+
? 'discovery update'
|
|
4310
|
+
: 'discovery start',
|
|
4311
|
+
blockedAction: 'task authorize',
|
|
4312
|
+
revision: activeTask.revision,
|
|
4313
|
+
planRiskAuditDecision: {
|
|
4314
|
+
state: 'recorded-blocking',
|
|
4315
|
+
decision: blockingDecision,
|
|
4316
|
+
reason: error.message,
|
|
4317
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
4318
|
+
},
|
|
4319
|
+
auditedRemediation: {
|
|
4320
|
+
state: existingRemediationTask?.status === 'merged'
|
|
4321
|
+
? 'merged-replan-required'
|
|
4322
|
+
: existingRemediationTask
|
|
4323
|
+
? 'materialized-not-merged'
|
|
4324
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4325
|
+
? 'ready-to-materialize'
|
|
4326
|
+
: 'discovery-required',
|
|
4327
|
+
humanApprovalRequired: false,
|
|
4328
|
+
blockedTaskId: activeTask.id,
|
|
4329
|
+
blockedTaskRevision: activeTask.revision,
|
|
4330
|
+
milestoneId: remediationMilestone.id,
|
|
4331
|
+
milestoneRevision: remediationMilestone.revision,
|
|
4332
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
4333
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
4334
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
4335
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
4336
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
4337
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
4338
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
4339
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
4340
|
+
command: existingRemediationTask?.status === 'merged'
|
|
4341
|
+
? 'task plan-set'
|
|
4342
|
+
: existingRemediationTask
|
|
4343
|
+
? 'doctor'
|
|
4344
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
4345
|
+
? 'milestone remediation-materialize'
|
|
4346
|
+
: remediationDiscovery
|
|
4347
|
+
? 'discovery update'
|
|
4348
|
+
: 'discovery start',
|
|
4349
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
4350
|
+
preservesMilestoneSemantics: true,
|
|
4351
|
+
},
|
|
4352
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
4353
|
+
}
|
|
4354
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
4355
|
+
? {
|
|
4356
|
+
scope: 'task',
|
|
4357
|
+
id: activeTask.id,
|
|
4358
|
+
status: activeTask.status,
|
|
4359
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
4360
|
+
blockedAction: 'task authorize',
|
|
4361
|
+
revision: activeTask.revision,
|
|
4362
|
+
planRiskAuditDecision: {
|
|
4363
|
+
state: 'recorded-blocking',
|
|
4364
|
+
decision: blockingDecision,
|
|
4365
|
+
reason: error.message,
|
|
4366
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
4367
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
4368
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
4369
|
+
},
|
|
4370
|
+
...(blockingDecision === 'stop-escalate'
|
|
4371
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
4372
|
+
: {}),
|
|
4373
|
+
}
|
|
4374
|
+
: {
|
|
4375
|
+
scope: 'task',
|
|
4376
|
+
id: activeTask.id,
|
|
4377
|
+
status: activeTask.status,
|
|
4378
|
+
action: 'task plan-risk-audit',
|
|
4379
|
+
revision: activeTask.revision,
|
|
4380
|
+
planRiskAuditRecovery: {
|
|
4381
|
+
state: 'required',
|
|
4382
|
+
reason: error.message,
|
|
4383
|
+
currentBriefHash: activeTask.briefHash,
|
|
4384
|
+
currentPlanHash: activeTask.planHash,
|
|
4385
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
4386
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
4387
|
+
},
|
|
4388
|
+
};
|
|
3266
4389
|
}
|
|
3267
4390
|
}
|
|
3268
4391
|
const taskNext = activeTask.status === 'planning'
|
|
3269
4392
|
? null
|
|
3270
|
-
:
|
|
3271
|
-
|
|
4393
|
+
: requiresReplacementPlan
|
|
4394
|
+
? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
|
|
4395
|
+
: planRiskAuditRecovery
|
|
4396
|
+
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
3272
4397
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3273
4398
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3274
4399
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3276,6 +4401,7 @@ export class WorkflowService {
|
|
|
3276
4401
|
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
3277
4402
|
if (taskNext?.action === 'task step-complete'
|
|
3278
4403
|
|| taskNext?.action === 'task step-review'
|
|
4404
|
+
|| taskNext?.action === 'task downstream-proof-recover'
|
|
3279
4405
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
3280
4406
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3281
4407
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
@@ -3315,7 +4441,9 @@ export class WorkflowService {
|
|
|
3315
4441
|
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
3316
4442
|
: null)
|
|
3317
4443
|
: null;
|
|
3318
|
-
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4444
|
+
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4445
|
+
&& taskNext?.action !== 'task plan-set'
|
|
4446
|
+
&& refreshAssessment?.eligible
|
|
3319
4447
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
3320
4448
|
: [];
|
|
3321
4449
|
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
@@ -3363,6 +4491,9 @@ export class WorkflowService {
|
|
|
3363
4491
|
}));
|
|
3364
4492
|
}
|
|
3365
4493
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
4494
|
+
if (taskNext?.action === 'task plan-set') {
|
|
4495
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
4496
|
+
}
|
|
3366
4497
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
3367
4498
|
activeTask.planHash &&
|
|
3368
4499
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
@@ -3663,6 +4794,32 @@ export class WorkflowService {
|
|
|
3663
4794
|
milestonePlanHash: milestone.planHash,
|
|
3664
4795
|
};
|
|
3665
4796
|
}
|
|
4797
|
+
currentCorrectiveDerivedTaskAuthority(projectId, task, actor) {
|
|
4798
|
+
if (!task.milestoneId || actor.trim() !== `agent:milestone-remediation:${task.milestoneId}`)
|
|
4799
|
+
return null;
|
|
4800
|
+
const milestone = this.readMilestoneCurrent(projectId, task.milestoneId);
|
|
4801
|
+
if (milestone.status !== 'active' || !milestone.planHash)
|
|
4802
|
+
return null;
|
|
4803
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
|
|
4804
|
+
if (membership?.disposition !== 'required')
|
|
4805
|
+
return null;
|
|
4806
|
+
const authorization = [...milestone.authorizations]
|
|
4807
|
+
.reverse()
|
|
4808
|
+
.find((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved');
|
|
4809
|
+
if (authorization?.authorizationMode !== 'corrective-derived'
|
|
4810
|
+
|| authorization.planHash !== milestone.planHash
|
|
4811
|
+
|| authorization.actor !== actor.trim())
|
|
4812
|
+
return null;
|
|
4813
|
+
const scopeChange = [...readMilestoneScopeChangeEvents(this.store, projectId, milestone.id)]
|
|
4814
|
+
.reverse()
|
|
4815
|
+
.find((candidate) => candidate.authorizationMode === 'audited-remediation'
|
|
4816
|
+
&& candidate.candidatePlanHash === milestone.planHash
|
|
4817
|
+
&& candidate.remediationEvidence !== null
|
|
4818
|
+
&& candidate.remediationEvidence !== undefined);
|
|
4819
|
+
if (!scopeChange)
|
|
4820
|
+
return null;
|
|
4821
|
+
return { milestone, authorization, scopeChange };
|
|
4822
|
+
}
|
|
3666
4823
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
3667
4824
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
3668
4825
|
? 'task.execution_authorize'
|
|
@@ -3672,6 +4829,12 @@ export class WorkflowService {
|
|
|
3672
4829
|
const delegatedApprovalOptions = transition
|
|
3673
4830
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
3674
4831
|
: [];
|
|
4832
|
+
const correctiveDerivedActor = task.milestoneId
|
|
4833
|
+
? `agent:milestone-remediation:${task.milestoneId}`
|
|
4834
|
+
: null;
|
|
4835
|
+
const correctiveDerivedAuthority = transition && correctiveDerivedActor
|
|
4836
|
+
? this.currentCorrectiveDerivedTaskAuthority(projectId, task, correctiveDerivedActor)
|
|
4837
|
+
: null;
|
|
3675
4838
|
let remediationModeRecovery = null;
|
|
3676
4839
|
let guardedCorrectivePosture = null;
|
|
3677
4840
|
if (planRiskAudit) {
|
|
@@ -3698,6 +4861,12 @@ export class WorkflowService {
|
|
|
3698
4861
|
}
|
|
3699
4862
|
}
|
|
3700
4863
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
4864
|
+
const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
4865
|
+
const downstreamProofCarryover = next.action === 'task run'
|
|
4866
|
+
&& nextStepId
|
|
4867
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
4868
|
+
? assessDownstreamProofCarryover(this.store, repositoryRoot, task, nextStepId)
|
|
4869
|
+
: null;
|
|
3701
4870
|
let mechanicalFeasibilityOverlay = {};
|
|
3702
4871
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
3703
4872
|
try {
|
|
@@ -3754,6 +4923,7 @@ export class WorkflowService {
|
|
|
3754
4923
|
const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
|
|
3755
4924
|
? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
|
|
3756
4925
|
: null;
|
|
4926
|
+
const downstreamProofAssessment = assessDownstreamProofRecovery(this.store, repositoryRoot, task);
|
|
3757
4927
|
const strictStepReviewStep = planRiskAudit
|
|
3758
4928
|
? task.steps.find((step) => {
|
|
3759
4929
|
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
@@ -3926,6 +5096,39 @@ export class WorkflowService {
|
|
|
3926
5096
|
case 'decision-required':
|
|
3927
5097
|
if (guardedCorrectivePosture.correctiveDecision
|
|
3928
5098
|
&& guardedCorrectivePosture.correctiveDecision.planHash !== task.planHash) {
|
|
5099
|
+
if (guardedCorrectivePosture.correctiveDecision.decision === 'stop-escalate') {
|
|
5100
|
+
try {
|
|
5101
|
+
const assessment = readValidatedStopOverrideContextRebindIfApplicable(this.store, task, guardedCorrectivePosture.stepId, repositoryRoot);
|
|
5102
|
+
if (!assessment) {
|
|
5103
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stale stop decision has no validated stop-override context-rebind continuation.');
|
|
5104
|
+
}
|
|
5105
|
+
guardedCorrectiveOverlay = {
|
|
5106
|
+
action: 'task run',
|
|
5107
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5108
|
+
stopOverrideContextRebind: {
|
|
5109
|
+
state: 'validated',
|
|
5110
|
+
overrideEventId: assessment.override.eventId,
|
|
5111
|
+
stopDecisionEventId: assessment.stopDecision.eventId,
|
|
5112
|
+
sourcePlanHash: assessment.override.planHash,
|
|
5113
|
+
reboundPlanHash: task.planHash,
|
|
5114
|
+
semanticPlanHash: assessment.semanticPlanHash,
|
|
5115
|
+
appendOnly: true,
|
|
5116
|
+
},
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
catch (error) {
|
|
5120
|
+
guardedCorrectiveOverlay = {
|
|
5121
|
+
action: 'doctor',
|
|
5122
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5123
|
+
blockedAction: 'task run',
|
|
5124
|
+
stopOverrideContextRebind: {
|
|
5125
|
+
state: 'blocked',
|
|
5126
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
5127
|
+
},
|
|
5128
|
+
};
|
|
5129
|
+
}
|
|
5130
|
+
break;
|
|
5131
|
+
}
|
|
3929
5132
|
try {
|
|
3930
5133
|
const navigation = readValidatedCorrectiveDecisionRecoveryForNavigation(this.store, task, guardedCorrectivePosture.stepId);
|
|
3931
5134
|
guardedCorrectiveOverlay = navigation.recovery
|
|
@@ -4039,21 +5242,85 @@ export class WorkflowService {
|
|
|
4039
5242
|
}
|
|
4040
5243
|
break;
|
|
4041
5244
|
case 'stop-escalate':
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
5245
|
+
try {
|
|
5246
|
+
this.assertStopOverrideRepositoryBoundary(resolveRepositoryIdentity(repositoryRoot), task);
|
|
5247
|
+
prepareStopEscalateOverride(this.store, task, guardedCorrectivePosture.stepId, task.revision, 'user', 'navigation eligibility probe', headCommit(repositoryRoot));
|
|
5248
|
+
guardedCorrectiveOverlay = {
|
|
5249
|
+
action: 'task stop-override-prepare',
|
|
5250
|
+
blockedAction: 'task run',
|
|
5251
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5252
|
+
correctiveDecisionGate: {
|
|
5253
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5254
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
5255
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
5256
|
+
humanApprovalRequired: true,
|
|
5257
|
+
delegatedApprovalAllowed: false,
|
|
5258
|
+
},
|
|
5259
|
+
stopOverride: {
|
|
5260
|
+
state: 'eligible',
|
|
5261
|
+
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
5262
|
+
prepareCommand: 'task stop-override-prepare',
|
|
5263
|
+
applyCommand: 'task stop-override-apply',
|
|
5264
|
+
expectedRevision: task.revision,
|
|
5265
|
+
appendOnly: true,
|
|
5266
|
+
preservesOriginalDecision: true,
|
|
5267
|
+
requiredAction: 'Prepare with an explicit human actor and reason, then apply the exact confirmation code in a later user turn.',
|
|
5268
|
+
},
|
|
5269
|
+
};
|
|
5270
|
+
}
|
|
5271
|
+
catch (error) {
|
|
5272
|
+
guardedCorrectiveOverlay = {
|
|
5273
|
+
action: 'doctor',
|
|
5274
|
+
blockedAction: 'task stop-override-prepare',
|
|
5275
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
5276
|
+
correctiveDecisionGate: {
|
|
5277
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
5278
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
5279
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
5280
|
+
humanApprovalRequired: true,
|
|
5281
|
+
},
|
|
5282
|
+
stopOverride: {
|
|
5283
|
+
state: 'blocked',
|
|
5284
|
+
policy: 'beta11-attempt4-stop-compatibility-v1',
|
|
5285
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
5286
|
+
},
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
4052
5289
|
break;
|
|
4053
5290
|
default:
|
|
4054
5291
|
break;
|
|
4055
5292
|
}
|
|
4056
5293
|
}
|
|
5294
|
+
const downstreamProofOverlay = downstreamProofAssessment.applicable
|
|
5295
|
+
? downstreamProofAssessment.eligible
|
|
5296
|
+
? {
|
|
5297
|
+
action: 'task downstream-proof-recover',
|
|
5298
|
+
blockedAction: 'task step-complete',
|
|
5299
|
+
stepId: downstreamProofAssessment.activeStepId,
|
|
5300
|
+
downstreamProofRecovery: {
|
|
5301
|
+
state: 'eligible',
|
|
5302
|
+
expectedRevision: task.revision,
|
|
5303
|
+
preservesWorktree: true,
|
|
5304
|
+
preservedHeadCommit: downstreamProofAssessment.preservedHeadCommit,
|
|
5305
|
+
dirtyFiles: downstreamProofAssessment.dirtyFiles,
|
|
5306
|
+
outsideActiveStepFiles: downstreamProofAssessment.outsideActiveStepFiles,
|
|
5307
|
+
invalidatedSteps: downstreamProofAssessment.invalidatedSteps,
|
|
5308
|
+
boundedEffect: 'invalidate-predecessor-completion-and-return-to-plan-set',
|
|
5309
|
+
},
|
|
5310
|
+
}
|
|
5311
|
+
: {
|
|
5312
|
+
action: 'doctor',
|
|
5313
|
+
blockedAction: 'task downstream-proof-recover',
|
|
5314
|
+
stepId: downstreamProofAssessment.activeStepId,
|
|
5315
|
+
downstreamProofRecovery: {
|
|
5316
|
+
state: 'blocked',
|
|
5317
|
+
expectedRevision: task.revision,
|
|
5318
|
+
blockers: downstreamProofAssessment.blockers,
|
|
5319
|
+
dirtyFiles: downstreamProofAssessment.dirtyFiles,
|
|
5320
|
+
outsideActiveStepFiles: downstreamProofAssessment.outsideActiveStepFiles,
|
|
5321
|
+
},
|
|
5322
|
+
}
|
|
5323
|
+
: {};
|
|
4057
5324
|
const result = {
|
|
4058
5325
|
...next,
|
|
4059
5326
|
...handoffOverlay,
|
|
@@ -4107,6 +5374,7 @@ export class WorkflowService {
|
|
|
4107
5374
|
}
|
|
4108
5375
|
: {}),
|
|
4109
5376
|
...guardedCorrectiveOverlay,
|
|
5377
|
+
...downstreamProofOverlay,
|
|
4110
5378
|
...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
|
|
4111
5379
|
? {
|
|
4112
5380
|
action: 'task plan-set',
|
|
@@ -4121,7 +5389,44 @@ export class WorkflowService {
|
|
|
4121
5389
|
}
|
|
4122
5390
|
: {}),
|
|
4123
5391
|
...mechanicalFeasibilityOverlay,
|
|
5392
|
+
...(downstreamProofCarryover?.applicable
|
|
5393
|
+
? downstreamProofCarryover.eligible
|
|
5394
|
+
? {
|
|
5395
|
+
downstreamProofCarryover: {
|
|
5396
|
+
state: 'validated',
|
|
5397
|
+
invalidationEventId: downstreamProofCarryover.invalidationEventId,
|
|
5398
|
+
stepId: nextStepId,
|
|
5399
|
+
dirtyFiles: downstreamProofCarryover.dirtyFiles,
|
|
5400
|
+
dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
|
|
5401
|
+
meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
|
|
5402
|
+
},
|
|
5403
|
+
}
|
|
5404
|
+
: {
|
|
5405
|
+
action: 'doctor',
|
|
5406
|
+
blockedAction: 'task run',
|
|
5407
|
+
downstreamProofCarryover: {
|
|
5408
|
+
state: 'blocked',
|
|
5409
|
+
invalidationEventId: downstreamProofCarryover.invalidationEventId,
|
|
5410
|
+
stepId: nextStepId,
|
|
5411
|
+
blockers: downstreamProofCarryover.blockers,
|
|
5412
|
+
},
|
|
5413
|
+
}
|
|
5414
|
+
: {}),
|
|
4124
5415
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
5416
|
+
...(correctiveDerivedAuthority && correctiveDerivedActor
|
|
5417
|
+
? {
|
|
5418
|
+
correctiveDerivedApproval: {
|
|
5419
|
+
action: transition === 'task.execution_authorize' ? 'task authorize' : 'task accept',
|
|
5420
|
+
actor: correctiveDerivedActor,
|
|
5421
|
+
humanApprovalRequired: false,
|
|
5422
|
+
milestoneId: correctiveDerivedAuthority.milestone.id,
|
|
5423
|
+
milestoneRevision: correctiveDerivedAuthority.milestone.revision,
|
|
5424
|
+
milestonePlanHash: correctiveDerivedAuthority.milestone.planHash,
|
|
5425
|
+
planRiskAuditEventId: correctiveDerivedAuthority.scopeChange.remediationEvidence?.planRiskAuditEventId ?? null,
|
|
5426
|
+
authority: 'current-corrective-derived-milestone-execution-authorization',
|
|
5427
|
+
},
|
|
5428
|
+
}
|
|
5429
|
+
: {}),
|
|
4125
5430
|
};
|
|
4126
5431
|
if (handoffPosture.state === 'pending') {
|
|
4127
5432
|
return this.withTaskNavigationContracts(task, {
|
|
@@ -4157,6 +5462,12 @@ export class WorkflowService {
|
|
|
4157
5462
|
};
|
|
4158
5463
|
}
|
|
4159
5464
|
assertTaskContextChangeAllowed(task) {
|
|
5465
|
+
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
5466
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
5467
|
+
taskId: task.id,
|
|
5468
|
+
requiredAction: 'Run task plan-set with the full unfinished remainder before requesting authorization.',
|
|
5469
|
+
});
|
|
5470
|
+
}
|
|
4160
5471
|
const posture = this.taskTerminalCorrectivePosture(task);
|
|
4161
5472
|
if (!posture)
|
|
4162
5473
|
return;
|
|
@@ -4170,6 +5481,9 @@ export class WorkflowService {
|
|
|
4170
5481
|
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
4171
5482
|
});
|
|
4172
5483
|
}
|
|
5484
|
+
stopOverrideContextRebindDecisionId(task, stepId, repositoryRoot) {
|
|
5485
|
+
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
5486
|
+
}
|
|
4173
5487
|
taskHasTerminalCorrectiveDecision(task) {
|
|
4174
5488
|
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
4175
5489
|
}
|
|
@@ -4184,6 +5498,42 @@ export class WorkflowService {
|
|
|
4184
5498
|
? posture
|
|
4185
5499
|
: null;
|
|
4186
5500
|
}
|
|
5501
|
+
assertStopOverrideRepositoryBoundary(identity, task, observedLeases) {
|
|
5502
|
+
const blockers = [];
|
|
5503
|
+
const dirty = changedFiles(identity.repositoryRoot);
|
|
5504
|
+
if (dirty.length > 0)
|
|
5505
|
+
blockers.push('Repository checkout must be clean.');
|
|
5506
|
+
if (task.workspaceOwner !== 'local')
|
|
5507
|
+
blockers.push('Stop override is limited to a local Task workspace.');
|
|
5508
|
+
if (!task.taskBranch || currentBranch(identity.repositoryRoot) !== task.taskBranch) {
|
|
5509
|
+
blockers.push('The current branch must be the recorded Task branch.');
|
|
5510
|
+
}
|
|
5511
|
+
if (task.steps.some((step) => step.status === 'in_progress'))
|
|
5512
|
+
blockers.push('No Step may be in progress.');
|
|
5513
|
+
const leases = observedLeases
|
|
5514
|
+
?? new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, this.store.root).list();
|
|
5515
|
+
if (leases.some((lease) => lease.entityId === task.id))
|
|
5516
|
+
blockers.push('No active or stale Task writer lease may exist.');
|
|
5517
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
5518
|
+
const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, this.store.root);
|
|
5519
|
+
const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, this.store.root);
|
|
5520
|
+
if (replanTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
5521
|
+
|| replanTransactions.corrupt.some((transaction) => transaction.taskId === task.id)
|
|
5522
|
+
|| yieldTransactions.pending.some((transaction) => transaction.taskId === task.id)
|
|
5523
|
+
|| yieldTransactions.corrupt.some((transaction) => transaction.taskId === task.id)) {
|
|
5524
|
+
blockers.push('No pending or corrupt Task transaction may exist.');
|
|
5525
|
+
}
|
|
5526
|
+
if (listCoreTaskOperationMutexes(this.store.lockRoot(identity.projectId), this.store.root)
|
|
5527
|
+
.some((operation) => operation.entityId === task.id)) {
|
|
5528
|
+
blockers.push('No Core Task operation may exist.');
|
|
5529
|
+
}
|
|
5530
|
+
if (blockers.length > 0) {
|
|
5531
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Stop override repository preconditions are not satisfied.', {
|
|
5532
|
+
taskId: task.id,
|
|
5533
|
+
blockers,
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
4187
5537
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
4188
5538
|
return this.store.listDelegations(projectId)
|
|
4189
5539
|
.filter((grant) => {
|
|
@@ -4239,6 +5589,7 @@ export class WorkflowService {
|
|
|
4239
5589
|
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4240
5590
|
|| !task.planHash
|
|
4241
5591
|
|| !taskCanRebindKnowledge(task)
|
|
5592
|
+
|| (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed'))
|
|
4242
5593
|
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
4243
5594
|
continue;
|
|
4244
5595
|
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
@@ -4297,6 +5648,10 @@ export class WorkflowService {
|
|
|
4297
5648
|
statePlanHash: task.planHash,
|
|
4298
5649
|
});
|
|
4299
5650
|
}
|
|
5651
|
+
if (authorization.authorizationMode === 'corrective-derived'
|
|
5652
|
+
&& !this.currentCorrectiveDerivedTaskAuthority(task.projectId, task, authorization.actor)) {
|
|
5653
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective-derived Task authorization is no longer backed by the current Milestone authority.', { taskId: task.id, milestoneId: task.milestoneId, actor: authorization.actor });
|
|
5654
|
+
}
|
|
4300
5655
|
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4301
5656
|
if (!mechanicalFeasibility)
|
|
4302
5657
|
return authorization;
|
|
@@ -4368,7 +5723,7 @@ export class WorkflowService {
|
|
|
4368
5723
|
},
|
|
4369
5724
|
};
|
|
4370
5725
|
}
|
|
4371
|
-
inspectDependencyProvenanceCandidate(snapshot, task) {
|
|
5726
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null) {
|
|
4372
5727
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4373
5728
|
const now = this.now().getTime();
|
|
4374
5729
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -4388,7 +5743,20 @@ export class WorkflowService {
|
|
|
4388
5743
|
currentBranch: versions.currentBranch,
|
|
4389
5744
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4390
5745
|
},
|
|
4391
|
-
});
|
|
5746
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance);
|
|
5747
|
+
}
|
|
5748
|
+
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
5749
|
+
const now = this.now().getTime();
|
|
5750
|
+
return inspectHistoricalStepProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
5751
|
+
activeLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now).length,
|
|
5752
|
+
staleLeaseCount: snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now).length,
|
|
5753
|
+
pendingTransactionCount: snapshot.observation.pendingProjectTransactions.length
|
|
5754
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
5755
|
+
+ snapshot.observation.pendingTaskTransactions.length,
|
|
5756
|
+
corruptTransactionCount: snapshot.observation.corruptTaskTransactions.length,
|
|
5757
|
+
coreOperationCount: snapshot.observation.coreTaskOperations.length,
|
|
5758
|
+
verifiedHistoricalStepBindings: verifiedHistoricalStepReviewBindings(this.store, task),
|
|
5759
|
+
}, options);
|
|
4392
5760
|
}
|
|
4393
5761
|
withTaskNavigationContracts(task, next) {
|
|
4394
5762
|
const action = typeof next.action === 'string' ? next.action : null;
|
|
@@ -4426,7 +5794,7 @@ export class WorkflowService {
|
|
|
4426
5794
|
return activeLeaseOwner;
|
|
4427
5795
|
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
4428
5796
|
? 'final_acceptance'
|
|
4429
|
-
: ['task step-complete', 'task submit'].includes(action)
|
|
5797
|
+
: ['task step-complete', 'task downstream-proof-recover', 'task submit', 'task result-set'].includes(action)
|
|
4430
5798
|
? 'execution'
|
|
4431
5799
|
: null;
|
|
4432
5800
|
if (!authorizationKind)
|
|
@@ -5210,7 +6578,14 @@ function gitMutationPolicyForAction(task, action) {
|
|
|
5210
6578
|
}
|
|
5211
6579
|
}
|
|
5212
6580
|
function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
5213
|
-
const alwaysLeaseBound = [
|
|
6581
|
+
const alwaysLeaseBound = [
|
|
6582
|
+
'task step-complete',
|
|
6583
|
+
'task downstream-proof-recover',
|
|
6584
|
+
'task submit',
|
|
6585
|
+
'task result-set',
|
|
6586
|
+
'task merge',
|
|
6587
|
+
'task merge-confirm',
|
|
6588
|
+
];
|
|
5214
6589
|
const claimedC1LeaseBound = [
|
|
5215
6590
|
'task run',
|
|
5216
6591
|
'task step-review',
|