codex-workflow-v2 2.0.0-beta.12.1 → 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 +64 -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 +2 -2
- package/dist/src/alpha6/remediation.js +18 -2
- 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 +117 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +112 -3
- package/dist/src/dependency-provenance.d.ts +9 -2
- package/dist/src/dependency-provenance.js +65 -5
- 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 +37 -2
- package/dist/src/workflow.js +1318 -43
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +73 -4
- 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 +87 -8
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +65 -8
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +135 -32
- package/docs/release.md +46 -8
- package/docs/updating-existing-project.md +134 -6
- package/docs/validation-report.md +101 -95
- package/package.json +7 -2
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +55 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +140 -2
- 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/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';
|
|
17
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
16
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';
|
|
@@ -170,6 +172,202 @@ export class WorkflowService {
|
|
|
170
172
|
now: this.now,
|
|
171
173
|
}).recover({ taskId, transactionId });
|
|
172
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
|
+
}
|
|
173
371
|
correctiveReplanController(repository) {
|
|
174
372
|
const identity = resolveRepositoryIdentity(repository);
|
|
175
373
|
return createCorrectiveReplanPublicController({
|
|
@@ -403,6 +601,316 @@ export class WorkflowService {
|
|
|
403
601
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
404
602
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
405
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
|
+
}
|
|
406
914
|
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
407
915
|
const { identity, locks } = this.#mutationContext(repository);
|
|
408
916
|
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
@@ -1066,7 +1574,10 @@ export class WorkflowService {
|
|
|
1066
1574
|
const overriddenStopDecisionEventIds = stopEscalateOverrideDecisionEventIds(this.store, task);
|
|
1067
1575
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required'
|
|
1068
1576
|
|| (event.decision === 'stop-escalate' && !overriddenStopDecisionEventIds.has(event.eventId)));
|
|
1069
|
-
const
|
|
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
|
|
1070
1581
|
? readCurrentPlanRiskAudit(this.store, task)
|
|
1071
1582
|
: null;
|
|
1072
1583
|
const classifiedCorrectivePosture = currentReplanDecisions.length > 0 && currentPlanRiskAudit
|
|
@@ -1199,6 +1710,10 @@ export class WorkflowService {
|
|
|
1199
1710
|
status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
|
|
1200
1711
|
evidence: null,
|
|
1201
1712
|
}));
|
|
1713
|
+
const invalidatedStepCommits = [...new Set([
|
|
1714
|
+
...(task.invalidatedStepCommits ?? []),
|
|
1715
|
+
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1716
|
+
])];
|
|
1202
1717
|
if (task.status === 'needs_fix' &&
|
|
1203
1718
|
task.review.status === 'failed' &&
|
|
1204
1719
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
@@ -1237,6 +1752,7 @@ export class WorkflowService {
|
|
|
1237
1752
|
knowledgeMapHash: plan.knowledgeMapHash,
|
|
1238
1753
|
graphUse: plan.graphUse,
|
|
1239
1754
|
steps: plannedSteps,
|
|
1755
|
+
invalidatedStepCommits,
|
|
1240
1756
|
authorizations,
|
|
1241
1757
|
review: emptyReview(),
|
|
1242
1758
|
resultHash: null,
|
|
@@ -1472,7 +1988,12 @@ export class WorkflowService {
|
|
|
1472
1988
|
requiredAction: 'task plan-set',
|
|
1473
1989
|
});
|
|
1474
1990
|
}
|
|
1475
|
-
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);
|
|
1476
1997
|
const event = {
|
|
1477
1998
|
kind: 'execution',
|
|
1478
1999
|
decision: 'approved',
|
|
@@ -1484,7 +2005,11 @@ export class WorkflowService {
|
|
|
1484
2005
|
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1485
2006
|
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1486
2007
|
reason,
|
|
1487
|
-
authorizationMode:
|
|
2008
|
+
authorizationMode: correctiveAuthority
|
|
2009
|
+
? 'corrective-derived'
|
|
2010
|
+
: delegation
|
|
2011
|
+
? 'delegated'
|
|
2012
|
+
: 'human',
|
|
1488
2013
|
...(delegation ? { delegation } : {}),
|
|
1489
2014
|
mechanicalFeasibility,
|
|
1490
2015
|
};
|
|
@@ -1827,14 +2352,26 @@ export class WorkflowService {
|
|
|
1827
2352
|
const recovery = staleDecision?.decision === 'continue-fix'
|
|
1828
2353
|
? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
|
|
1829
2354
|
: null;
|
|
1830
|
-
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);
|
|
1831
2359
|
resumesCorrectiveReplan = gate.posture === 'replan-ready';
|
|
1832
2360
|
}
|
|
1833
2361
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1834
2362
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1835
|
-
|
|
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) {
|
|
1836
2370
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
1837
2371
|
changedFiles: dirty,
|
|
2372
|
+
...(downstreamProofCarryover?.applicable
|
|
2373
|
+
? { downstreamProofCarryoverBlockers: downstreamProofCarryover.blockers }
|
|
2374
|
+
: {}),
|
|
1838
2375
|
});
|
|
1839
2376
|
}
|
|
1840
2377
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, step.allowedWrites));
|
|
@@ -2065,7 +2602,7 @@ export class WorkflowService {
|
|
|
2065
2602
|
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
2066
2603
|
reviewEventHash: null,
|
|
2067
2604
|
reviewerAttestationHash: null,
|
|
2068
|
-
}, planRiskAudit, this.now());
|
|
2605
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2069
2606
|
}
|
|
2070
2607
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
|
|
2071
2608
|
return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
|
|
@@ -2185,7 +2722,7 @@ export class WorkflowService {
|
|
|
2185
2722
|
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
2186
2723
|
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
2187
2724
|
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
2188
|
-
}, planRiskAudit, this.now());
|
|
2725
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2189
2726
|
}
|
|
2190
2727
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
2191
2728
|
}
|
|
@@ -2213,7 +2750,7 @@ export class WorkflowService {
|
|
|
2213
2750
|
checksHash: reviewRecordedEvent.checksHash,
|
|
2214
2751
|
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
2215
2752
|
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
2216
|
-
}, planRiskAudit, this.now());
|
|
2753
|
+
}, planRiskAudit, this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2217
2754
|
}
|
|
2218
2755
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
2219
2756
|
}
|
|
@@ -2285,7 +2822,7 @@ export class WorkflowService {
|
|
|
2285
2822
|
checksHash: recorded.reviewRecordedEvent.checksHash,
|
|
2286
2823
|
reviewEventHash: recorded.reviewRecordedEvent.eventHash,
|
|
2287
2824
|
reviewerAttestationHash: recorded.reviewerAttestationEvent.eventHash,
|
|
2288
|
-
}, readCurrentPlanRiskAudit(this.store, task), this.now());
|
|
2825
|
+
}, readCurrentPlanRiskAudit(this.store, task), this.now(), this.stopOverrideContextRebindDecisionId(task, stepId, context.identity.repositoryRoot));
|
|
2289
2826
|
}
|
|
2290
2827
|
const saved = this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, review.summary), expectedRevision);
|
|
2291
2828
|
return {
|
|
@@ -2521,16 +3058,23 @@ export class WorkflowService {
|
|
|
2521
3058
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
2522
3059
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
2523
3060
|
}
|
|
2524
|
-
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
2525
|
-
const
|
|
3061
|
+
setTaskResult(repository, taskId, expectedRevision, summary, limitations, writerToken, actor) {
|
|
3062
|
+
const context = this.#mutationContext(repository);
|
|
3063
|
+
const { identity } = context;
|
|
2526
3064
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2527
3065
|
assertExpectedRevision(task, expectedRevision);
|
|
2528
3066
|
if (task.status !== 'validating' || task.review.status !== 'passed') {
|
|
2529
3067
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Result requires a passing independent review.');
|
|
2530
3068
|
}
|
|
2531
3069
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
3070
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
3071
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
2532
3072
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
2533
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);
|
|
2534
3078
|
const saved = this.store.writeTask({ ...task, status: 'awaiting_final_acceptance', resultHash: stagedResult.hash, blockReason: null }, expectedRevision);
|
|
2535
3079
|
this.store.publishArtifact(stagedResult);
|
|
2536
3080
|
return saved;
|
|
@@ -2550,7 +3094,12 @@ export class WorkflowService {
|
|
|
2550
3094
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
2551
3095
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
2552
3096
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
2553
|
-
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);
|
|
2554
3103
|
const event = {
|
|
2555
3104
|
kind: 'final_acceptance',
|
|
2556
3105
|
decision: 'approved',
|
|
@@ -2562,7 +3111,11 @@ export class WorkflowService {
|
|
|
2562
3111
|
evidenceHash,
|
|
2563
3112
|
headCommit: headCommit(identity.repositoryRoot),
|
|
2564
3113
|
reason: '',
|
|
2565
|
-
authorizationMode:
|
|
3114
|
+
authorizationMode: correctiveAuthority
|
|
3115
|
+
? 'corrective-derived'
|
|
3116
|
+
: delegation
|
|
3117
|
+
? 'delegated'
|
|
3118
|
+
: 'human',
|
|
2566
3119
|
...(delegation ? { delegation } : {}),
|
|
2567
3120
|
};
|
|
2568
3121
|
return this.store.writeTask({
|
|
@@ -2793,7 +3346,288 @@ export class WorkflowService {
|
|
|
2793
3346
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2794
3347
|
return this.inspectDependencyProvenanceCandidate(snapshot, task);
|
|
2795
3348
|
}
|
|
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
|
+
}
|
|
2796
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) {
|
|
2797
3631
|
const normalizedActor = actor.trim();
|
|
2798
3632
|
const normalizedReason = reason.trim();
|
|
2799
3633
|
if (!normalizedActor)
|
|
@@ -2803,7 +3637,13 @@ export class WorkflowService {
|
|
|
2803
3637
|
const context = this.#mutationContext(repository);
|
|
2804
3638
|
const current = context.store.readTask(context.identity.projectId, taskId);
|
|
2805
3639
|
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2806
|
-
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));
|
|
2807
3647
|
if (prior && current.systemCommits.includes(currentHead))
|
|
2808
3648
|
return current;
|
|
2809
3649
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -2811,7 +3651,13 @@ export class WorkflowService {
|
|
|
2811
3651
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
2812
3652
|
if (!task)
|
|
2813
3653
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
2814
|
-
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);
|
|
2815
3661
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
2816
3662
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
2817
3663
|
taskId,
|
|
@@ -2833,6 +3679,15 @@ export class WorkflowService {
|
|
|
2833
3679
|
reason: normalizedReason,
|
|
2834
3680
|
recordedAt: this.now().toISOString(),
|
|
2835
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
|
+
: {}),
|
|
2836
3691
|
},
|
|
2837
3692
|
],
|
|
2838
3693
|
}, expectedRevision);
|
|
@@ -3232,9 +4087,53 @@ export class WorkflowService {
|
|
|
3232
4087
|
...(dependency ? { dependency } : {}),
|
|
3233
4088
|
});
|
|
3234
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;
|
|
3235
4106
|
const activeTask = requestedTask
|
|
4107
|
+
?? prioritizedRemediationTask
|
|
3236
4108
|
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3237
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
|
+
}
|
|
3238
4137
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3239
4138
|
if (dependencyRecovery.eligible) {
|
|
3240
4139
|
return withAdoption({
|
|
@@ -3246,6 +4145,38 @@ export class WorkflowService {
|
|
|
3246
4145
|
dependencyProvenanceRecovery: dependencyRecovery,
|
|
3247
4146
|
});
|
|
3248
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
|
+
}
|
|
3249
4180
|
const activeTaskMilestone = activeTask.milestoneId
|
|
3250
4181
|
? milestonesById.get(activeTask.milestoneId) ?? null
|
|
3251
4182
|
: null;
|
|
@@ -3269,36 +4200,200 @@ export class WorkflowService {
|
|
|
3269
4200
|
},
|
|
3270
4201
|
}
|
|
3271
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
|
+
}
|
|
3272
4251
|
let activePlanRiskAudit = null;
|
|
3273
4252
|
let planRiskAuditRecovery = null;
|
|
3274
|
-
|
|
4253
|
+
const requiresReplacementPlan = activeTask.status === 'needs_fix'
|
|
4254
|
+
&& !activeTask.steps.some((step) => step.status === 'failed');
|
|
4255
|
+
if (activeTask.planHash && !requiresReplacementPlan) {
|
|
3275
4256
|
try {
|
|
3276
4257
|
activePlanRiskAudit = readCurrentPlanRiskAudit(this.store, activeTask);
|
|
3277
4258
|
}
|
|
3278
4259
|
catch (error) {
|
|
3279
4260
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
3280
4261
|
throw error;
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
status
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
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
|
+
}
|
|
3295
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
|
+
};
|
|
3296
4389
|
}
|
|
3297
4390
|
}
|
|
3298
4391
|
const taskNext = activeTask.status === 'planning'
|
|
3299
4392
|
? null
|
|
3300
|
-
:
|
|
3301
|
-
|
|
4393
|
+
: requiresReplacementPlan
|
|
4394
|
+
? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)
|
|
4395
|
+
: planRiskAuditRecovery
|
|
4396
|
+
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
3302
4397
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3303
4398
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3304
4399
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3306,6 +4401,7 @@ export class WorkflowService {
|
|
|
3306
4401
|
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
3307
4402
|
if (taskNext?.action === 'task step-complete'
|
|
3308
4403
|
|| taskNext?.action === 'task step-review'
|
|
4404
|
+
|| taskNext?.action === 'task downstream-proof-recover'
|
|
3309
4405
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
3310
4406
|
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3311
4407
|
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
@@ -3345,7 +4441,9 @@ export class WorkflowService {
|
|
|
3345
4441
|
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
3346
4442
|
: null)
|
|
3347
4443
|
: null;
|
|
3348
|
-
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4444
|
+
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
4445
|
+
&& taskNext?.action !== 'task plan-set'
|
|
4446
|
+
&& refreshAssessment?.eligible
|
|
3349
4447
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
3350
4448
|
: [];
|
|
3351
4449
|
if (requestedTask && action === 'project-memory reconcile' && contextRefreshOptions.length === 0) {
|
|
@@ -3393,6 +4491,9 @@ export class WorkflowService {
|
|
|
3393
4491
|
}));
|
|
3394
4492
|
}
|
|
3395
4493
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
4494
|
+
if (taskNext?.action === 'task plan-set') {
|
|
4495
|
+
return withAdoption(withMilestoneCancellation(taskNext));
|
|
4496
|
+
}
|
|
3396
4497
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
3397
4498
|
activeTask.planHash &&
|
|
3398
4499
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
@@ -3693,6 +4794,32 @@ export class WorkflowService {
|
|
|
3693
4794
|
milestonePlanHash: milestone.planHash,
|
|
3694
4795
|
};
|
|
3695
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
|
+
}
|
|
3696
4823
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
3697
4824
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
3698
4825
|
? 'task.execution_authorize'
|
|
@@ -3702,6 +4829,12 @@ export class WorkflowService {
|
|
|
3702
4829
|
const delegatedApprovalOptions = transition
|
|
3703
4830
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
3704
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;
|
|
3705
4838
|
let remediationModeRecovery = null;
|
|
3706
4839
|
let guardedCorrectivePosture = null;
|
|
3707
4840
|
if (planRiskAudit) {
|
|
@@ -3728,6 +4861,12 @@ export class WorkflowService {
|
|
|
3728
4861
|
}
|
|
3729
4862
|
}
|
|
3730
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;
|
|
3731
4870
|
let mechanicalFeasibilityOverlay = {};
|
|
3732
4871
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
3733
4872
|
try {
|
|
@@ -3784,6 +4923,7 @@ export class WorkflowService {
|
|
|
3784
4923
|
const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
|
|
3785
4924
|
? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
|
|
3786
4925
|
: null;
|
|
4926
|
+
const downstreamProofAssessment = assessDownstreamProofRecovery(this.store, repositoryRoot, task);
|
|
3787
4927
|
const strictStepReviewStep = planRiskAudit
|
|
3788
4928
|
? task.steps.find((step) => {
|
|
3789
4929
|
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
@@ -3956,6 +5096,39 @@ export class WorkflowService {
|
|
|
3956
5096
|
case 'decision-required':
|
|
3957
5097
|
if (guardedCorrectivePosture.correctiveDecision
|
|
3958
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
|
+
}
|
|
3959
5132
|
try {
|
|
3960
5133
|
const navigation = readValidatedCorrectiveDecisionRecoveryForNavigation(this.store, task, guardedCorrectivePosture.stepId);
|
|
3961
5134
|
guardedCorrectiveOverlay = navigation.recovery
|
|
@@ -4118,6 +5291,36 @@ export class WorkflowService {
|
|
|
4118
5291
|
break;
|
|
4119
5292
|
}
|
|
4120
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
|
+
: {};
|
|
4121
5324
|
const result = {
|
|
4122
5325
|
...next,
|
|
4123
5326
|
...handoffOverlay,
|
|
@@ -4171,6 +5374,7 @@ export class WorkflowService {
|
|
|
4171
5374
|
}
|
|
4172
5375
|
: {}),
|
|
4173
5376
|
...guardedCorrectiveOverlay,
|
|
5377
|
+
...downstreamProofOverlay,
|
|
4174
5378
|
...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
|
|
4175
5379
|
? {
|
|
4176
5380
|
action: 'task plan-set',
|
|
@@ -4185,7 +5389,44 @@ export class WorkflowService {
|
|
|
4185
5389
|
}
|
|
4186
5390
|
: {}),
|
|
4187
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
|
+
: {}),
|
|
4188
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
|
+
: {}),
|
|
4189
5430
|
};
|
|
4190
5431
|
if (handoffPosture.state === 'pending') {
|
|
4191
5432
|
return this.withTaskNavigationContracts(task, {
|
|
@@ -4221,6 +5462,12 @@ export class WorkflowService {
|
|
|
4221
5462
|
};
|
|
4222
5463
|
}
|
|
4223
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
|
+
}
|
|
4224
5471
|
const posture = this.taskTerminalCorrectivePosture(task);
|
|
4225
5472
|
if (!posture)
|
|
4226
5473
|
return;
|
|
@@ -4234,6 +5481,9 @@ export class WorkflowService {
|
|
|
4234
5481
|
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
4235
5482
|
});
|
|
4236
5483
|
}
|
|
5484
|
+
stopOverrideContextRebindDecisionId(task, stepId, repositoryRoot) {
|
|
5485
|
+
return readValidatedStopOverrideContextRebindIfApplicable(this.store, task, stepId, repositoryRoot)?.stopDecision.eventId ?? null;
|
|
5486
|
+
}
|
|
4237
5487
|
taskHasTerminalCorrectiveDecision(task) {
|
|
4238
5488
|
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
4239
5489
|
}
|
|
@@ -4339,6 +5589,7 @@ export class WorkflowService {
|
|
|
4339
5589
|
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
4340
5590
|
|| !task.planHash
|
|
4341
5591
|
|| !taskCanRebindKnowledge(task)
|
|
5592
|
+
|| (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed'))
|
|
4342
5593
|
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
4343
5594
|
continue;
|
|
4344
5595
|
const assessment = assessDelegatedKnowledgeRefresh(approved, inspected, task, this.store.hashArtifact(this.store.taskRoot(projectId, task.id), 'plan.md'));
|
|
@@ -4397,6 +5648,10 @@ export class WorkflowService {
|
|
|
4397
5648
|
statePlanHash: task.planHash,
|
|
4398
5649
|
});
|
|
4399
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
|
+
}
|
|
4400
5655
|
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4401
5656
|
if (!mechanicalFeasibility)
|
|
4402
5657
|
return authorization;
|
|
@@ -4468,7 +5723,7 @@ export class WorkflowService {
|
|
|
4468
5723
|
},
|
|
4469
5724
|
};
|
|
4470
5725
|
}
|
|
4471
|
-
inspectDependencyProvenanceCandidate(snapshot, task) {
|
|
5726
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null) {
|
|
4472
5727
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4473
5728
|
const now = this.now().getTime();
|
|
4474
5729
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -4488,7 +5743,20 @@ export class WorkflowService {
|
|
|
4488
5743
|
currentBranch: versions.currentBranch,
|
|
4489
5744
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4490
5745
|
},
|
|
4491
|
-
});
|
|
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);
|
|
4492
5760
|
}
|
|
4493
5761
|
withTaskNavigationContracts(task, next) {
|
|
4494
5762
|
const action = typeof next.action === 'string' ? next.action : null;
|
|
@@ -4526,7 +5794,7 @@ export class WorkflowService {
|
|
|
4526
5794
|
return activeLeaseOwner;
|
|
4527
5795
|
const authorizationKind = ['task merge', 'task merge-confirm'].includes(action)
|
|
4528
5796
|
? 'final_acceptance'
|
|
4529
|
-
: ['task step-complete', 'task submit'].includes(action)
|
|
5797
|
+
: ['task step-complete', 'task downstream-proof-recover', 'task submit', 'task result-set'].includes(action)
|
|
4530
5798
|
? 'execution'
|
|
4531
5799
|
: null;
|
|
4532
5800
|
if (!authorizationKind)
|
|
@@ -5310,7 +6578,14 @@ function gitMutationPolicyForAction(task, action) {
|
|
|
5310
6578
|
}
|
|
5311
6579
|
}
|
|
5312
6580
|
function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
5313
|
-
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
|
+
];
|
|
5314
6589
|
const claimedC1LeaseBound = [
|
|
5315
6590
|
'task run',
|
|
5316
6591
|
'task step-review',
|