codex-workflow-v2 2.0.0-alpha.7.2 → 2.0.0-beta.1
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 +23 -5
- package/dist/src/alpha6/milestone.d.ts +17 -0
- package/dist/src/alpha6/milestone.js +54 -0
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +18 -1
- package/dist/src/alpha6/remediation.js +458 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +52 -0
- package/dist/src/beta1/project-transaction.js +297 -0
- package/dist/src/beta1/project-transaction.js.map +1 -0
- package/dist/src/cli.js +23 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +39 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +54 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/git.js +2 -5
- package/dist/src/git.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 +11 -1
- package/dist/src/workflow.js +519 -56
- package/dist/src/workflow.js.map +1 -1
- package/docs/alpha7.2.1-remediation-recovery-brief.md +86 -0
- package/docs/autonomy-guardrails.md +3 -2
- package/docs/beta1-stabilization-brief.md +165 -0
- package/docs/delegated-approval.md +4 -3
- package/docs/development-flow.md +6 -4
- package/docs/project-memory.md +7 -5
- package/docs/release.md +9 -2
- package/docs/split-required-recovery.md +53 -0
- package/docs/stable-release-defect-register.md +268 -0
- package/package.json +2 -2
- package/plugins/codex-workflow-gateway/references/protocol.md +6 -2
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +9 -2
- package/schemas/remediation-mode-recovery-event.schema.json +42 -0
- package/schemas/task.schema.json +3 -1
package/dist/src/workflow.js
CHANGED
|
@@ -8,12 +8,13 @@ import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
|
8
8
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
9
9
|
import { WorkflowError } from './errors.js';
|
|
10
10
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
11
|
-
import { inspectLocalDependency } from './diagnostics.js';
|
|
11
|
+
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
12
12
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
13
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
14
14
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
15
|
-
import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
|
-
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
15
|
+
import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
|
+
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
|
+
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
17
18
|
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
18
19
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
19
20
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
@@ -35,7 +36,9 @@ export class WorkflowService {
|
|
|
35
36
|
}
|
|
36
37
|
context(repository) {
|
|
37
38
|
const identity = resolveRepositoryIdentity(repository);
|
|
38
|
-
const
|
|
39
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
40
|
+
recoverProjectTransactions(projectRoot, identity.projectId);
|
|
41
|
+
const projectFile = path.join(projectRoot, 'project.json');
|
|
39
42
|
const projectAlreadyRegistered = existsSync(projectFile);
|
|
40
43
|
if (!projectAlreadyRegistered) {
|
|
41
44
|
initializeProjectRegistrationAdoption(this.store, {
|
|
@@ -249,6 +252,257 @@ export class WorkflowService {
|
|
|
249
252
|
const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
|
|
250
253
|
return { discovery: savedDiscovery, task: savedTask, taskRoot };
|
|
251
254
|
}
|
|
255
|
+
replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
|
|
256
|
+
const { identity, locks } = this.context(repository);
|
|
257
|
+
const oldTask = this.store.readTask(identity.projectId, oldTaskId);
|
|
258
|
+
assertExpectedRevision(oldTask, expectedOldTaskRevision);
|
|
259
|
+
if (!oldTask.milestoneId) {
|
|
260
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A split-required replacement requires a Milestone-linked Task.');
|
|
261
|
+
}
|
|
262
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, oldTask.milestoneId);
|
|
263
|
+
assertExpectedRevision(milestone, expectedMilestoneRevision);
|
|
264
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
265
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === oldTask.id);
|
|
266
|
+
if (membership?.disposition !== 'required') {
|
|
267
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'The terminal Task must still be a required Milestone member.', {
|
|
268
|
+
taskId: oldTask.id,
|
|
269
|
+
milestoneId: milestone.id,
|
|
270
|
+
disposition: membership?.disposition ?? null,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
|
|
274
|
+
if (!riskAudit)
|
|
275
|
+
throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
|
|
276
|
+
const posture = findCurrentGuardedRemediationPosture(this.store, oldTask, riskAudit);
|
|
277
|
+
if (posture?.posture !== 'split-required' || posture.correctiveDecision?.decision !== 'split-required') {
|
|
278
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task replacement is available only for the exact split-required posture.', {
|
|
279
|
+
taskId: oldTask.id,
|
|
280
|
+
posture: posture?.posture ?? null,
|
|
281
|
+
decision: posture?.correctiveDecision?.decision ?? null,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
285
|
+
assertExpectedRevision(discovery, expectedDiscoveryRevision);
|
|
286
|
+
const readiness = assessDiscovery(discovery);
|
|
287
|
+
if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
|
|
288
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Discovery is not ready to materialize.', {
|
|
289
|
+
discoveryId,
|
|
290
|
+
missing: readiness.missing,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const normalizedTitle = title.trim();
|
|
294
|
+
const normalizedActor = actor.trim();
|
|
295
|
+
if (!normalizedTitle)
|
|
296
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Replacement Task title is required.');
|
|
297
|
+
if (!normalizedActor)
|
|
298
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Replacement actor is required.');
|
|
299
|
+
let autonomyContractEventHash;
|
|
300
|
+
let activeAutonomy = null;
|
|
301
|
+
if (normalizedActor.startsWith('agent:')) {
|
|
302
|
+
activeAutonomy = requireActiveMilestoneAutonomy({
|
|
303
|
+
store: this.store,
|
|
304
|
+
projectId: identity.projectId,
|
|
305
|
+
milestone,
|
|
306
|
+
actor: normalizedActor,
|
|
307
|
+
now: this.now(),
|
|
308
|
+
});
|
|
309
|
+
autonomyContractEventHash = activeAutonomy.contract.eventHash;
|
|
310
|
+
}
|
|
311
|
+
const now = this.now();
|
|
312
|
+
const timestamp = now.toISOString();
|
|
313
|
+
const replacementTaskId = createEntityId('TASK', now.getTime());
|
|
314
|
+
const briefContent = renderBrief(discovery, normalizedTitle);
|
|
315
|
+
const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
|
|
316
|
+
const briefHash = sha256Hex(normalizedBriefContent);
|
|
317
|
+
const replacementTask = {
|
|
318
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
319
|
+
revision: 1,
|
|
320
|
+
id: replacementTaskId,
|
|
321
|
+
projectId: identity.projectId,
|
|
322
|
+
createdAt: timestamp,
|
|
323
|
+
updatedAt: timestamp,
|
|
324
|
+
kind: 'task',
|
|
325
|
+
status: 'planning',
|
|
326
|
+
title: normalizedTitle,
|
|
327
|
+
slug: slugify(normalizedTitle),
|
|
328
|
+
discoveryId,
|
|
329
|
+
milestoneId: milestone.id,
|
|
330
|
+
repositoryRoot: identity.repositoryRoot,
|
|
331
|
+
baseBranch: milestone.baseBranch,
|
|
332
|
+
baseCommit: null,
|
|
333
|
+
taskBranch: null,
|
|
334
|
+
workspaceOwner: null,
|
|
335
|
+
briefHash,
|
|
336
|
+
planHash: null,
|
|
337
|
+
planObjective: null,
|
|
338
|
+
resultHash: null,
|
|
339
|
+
evidenceHash: null,
|
|
340
|
+
requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
|
|
341
|
+
acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
|
|
342
|
+
risks: discovery.assumptions,
|
|
343
|
+
knowledgeImpact: null,
|
|
344
|
+
knowledgeImpactReason: null,
|
|
345
|
+
knowledgeTargets: [],
|
|
346
|
+
knowledgeMapRevision: null,
|
|
347
|
+
knowledgeMapHash: null,
|
|
348
|
+
graphUse: null,
|
|
349
|
+
steps: [],
|
|
350
|
+
authorizations: [],
|
|
351
|
+
knowledgeRebinds: [],
|
|
352
|
+
review: emptyReview(),
|
|
353
|
+
blockReason: null,
|
|
354
|
+
mergedCommit: null,
|
|
355
|
+
systemCommits: [],
|
|
356
|
+
replacementForTaskId: oldTask.id,
|
|
357
|
+
replacedByTaskId: null,
|
|
358
|
+
};
|
|
359
|
+
const cancelledOldTask = {
|
|
360
|
+
...oldTask,
|
|
361
|
+
revision: oldTask.revision + 1,
|
|
362
|
+
updatedAt: timestamp,
|
|
363
|
+
status: 'cancelled',
|
|
364
|
+
blockReason: `Replaced after split-required by ${replacementTaskId}.`,
|
|
365
|
+
replacedByTaskId: replacementTaskId,
|
|
366
|
+
};
|
|
367
|
+
const consumedDiscovery = {
|
|
368
|
+
...discovery,
|
|
369
|
+
revision: discovery.revision + 1,
|
|
370
|
+
updatedAt: timestamp,
|
|
371
|
+
status: 'materialized',
|
|
372
|
+
materializedEntityId: replacementTaskId,
|
|
373
|
+
};
|
|
374
|
+
const replacementReason = `Replacement for ${oldTask.id} after split-required decision ${posture.correctiveDecision.eventId}.`;
|
|
375
|
+
const plan = {
|
|
376
|
+
outcome: milestone.outcome,
|
|
377
|
+
successSignal: milestone.successSignal,
|
|
378
|
+
acceptance: milestone.acceptance,
|
|
379
|
+
checks: milestone.checks,
|
|
380
|
+
memberships: [
|
|
381
|
+
...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
|
|
382
|
+
? { ...candidate, disposition: 'cancelled', reason: replacementReason }
|
|
383
|
+
: candidate),
|
|
384
|
+
{ taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
if (autonomyContractEventHash) {
|
|
388
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
389
|
+
store: this.store,
|
|
390
|
+
projectId: identity.projectId,
|
|
391
|
+
milestone,
|
|
392
|
+
actor: normalizedActor,
|
|
393
|
+
now,
|
|
394
|
+
});
|
|
395
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
396
|
+
}
|
|
397
|
+
const readCandidateTask = (taskId) => {
|
|
398
|
+
if (taskId === oldTask.id)
|
|
399
|
+
return cancelledOldTask;
|
|
400
|
+
if (taskId === replacementTaskId)
|
|
401
|
+
return replacementTask;
|
|
402
|
+
return this.store.readTask(identity.projectId, taskId);
|
|
403
|
+
};
|
|
404
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
405
|
+
milestone,
|
|
406
|
+
plan,
|
|
407
|
+
readTask: readCandidateTask,
|
|
408
|
+
actor: normalizedActor,
|
|
409
|
+
...(autonomyContractEventHash ? { autonomyContractEventHash } : {}),
|
|
410
|
+
});
|
|
411
|
+
const bundle = prepareMilestoneScopeChangeBundle({
|
|
412
|
+
store: this.store,
|
|
413
|
+
milestone,
|
|
414
|
+
prepared,
|
|
415
|
+
now,
|
|
416
|
+
readTask: readCandidateTask,
|
|
417
|
+
});
|
|
418
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
419
|
+
const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
|
|
420
|
+
const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
421
|
+
let refreshedKnowledgeMap = null;
|
|
422
|
+
if (activeAutonomy
|
|
423
|
+
&& currentKnowledgeMap.status === 'active'
|
|
424
|
+
&& currentKnowledgeMap.approval
|
|
425
|
+
&& (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval)) {
|
|
426
|
+
const refreshAssessment = assessDelegatedKnowledgeRefresh(currentKnowledgeMap, inspectedKnowledgeMap, oldTask, oldTask.planHash
|
|
427
|
+
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, oldTask.id), 'plan.md')
|
|
428
|
+
: null);
|
|
429
|
+
if (refreshAssessment.eligible) {
|
|
430
|
+
if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
|
|
431
|
+
this.assertPlanBoundedSupportingSourceGrant(identity.projectId, activeAutonomy.grant.id, normalizedActor, oldTask);
|
|
432
|
+
}
|
|
433
|
+
const delegation = this.delegatedAuthorization(identity.projectId, activeAutonomy.grant.id, 'project_memory.approve', normalizedActor, inspectedKnowledgeMap, oldTask.id);
|
|
434
|
+
if (!delegation) {
|
|
435
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Knowledge refresh requires delegated approval evidence.');
|
|
436
|
+
}
|
|
437
|
+
const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
|
|
438
|
+
const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
|
|
439
|
+
if (!approved.approval) {
|
|
440
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement Knowledge refresh did not produce approval evidence.');
|
|
441
|
+
}
|
|
442
|
+
refreshedKnowledgeMap = {
|
|
443
|
+
...approved,
|
|
444
|
+
revision: currentKnowledgeMap.revision + 2,
|
|
445
|
+
approval: {
|
|
446
|
+
...approved.approval,
|
|
447
|
+
authorizationMode: 'delegated',
|
|
448
|
+
delegation,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
454
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
455
|
+
applyProjectTransaction({
|
|
456
|
+
projectRoot,
|
|
457
|
+
projectId: identity.projectId,
|
|
458
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
459
|
+
kind: 'split-required-task-replacement',
|
|
460
|
+
now,
|
|
461
|
+
targets: [
|
|
462
|
+
{ path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')), content: `${JSON.stringify(consumedDiscovery, null, 2)}\n` },
|
|
463
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'brief.md')), content: normalizedBriefContent },
|
|
464
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'state.json')), content: `${JSON.stringify(replacementTask, null, 2)}\n` },
|
|
465
|
+
{ path: relative(path.join(this.store.taskRoot(identity.projectId, oldTask.id), 'state.json')), content: `${JSON.stringify(cancelledOldTask, null, 2)}\n` },
|
|
466
|
+
{ path: relative(path.join(this.store.lockRoot(identity.projectId), `${oldTask.id}.json`)), content: null },
|
|
467
|
+
...(refreshedKnowledgeMap
|
|
468
|
+
? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
|
|
469
|
+
: []),
|
|
470
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')), content: bundle.planContent },
|
|
471
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')), content: bundle.sidecarContent },
|
|
472
|
+
{ path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')), content: bundle.stateContent },
|
|
473
|
+
],
|
|
474
|
+
validate: () => {
|
|
475
|
+
const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
|
|
476
|
+
const savedOldTask = this.store.readTask(identity.projectId, oldTask.id);
|
|
477
|
+
const savedReplacement = this.store.readTask(identity.projectId, replacementTaskId);
|
|
478
|
+
const savedMilestone = this.store.readMilestone(identity.projectId, milestone.id);
|
|
479
|
+
if (savedDiscovery.materializedEntityId !== replacementTaskId
|
|
480
|
+
|| savedOldTask.status !== 'cancelled'
|
|
481
|
+
|| savedOldTask.replacedByTaskId !== replacementTaskId
|
|
482
|
+
|| savedReplacement.replacementForTaskId !== oldTask.id) {
|
|
483
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction relationship validation failed.');
|
|
484
|
+
}
|
|
485
|
+
if (locks.inspect(oldTask.id)) {
|
|
486
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction retained authority for the cancelled Task.');
|
|
487
|
+
}
|
|
488
|
+
if (refreshedKnowledgeMap) {
|
|
489
|
+
const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
490
|
+
if (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
|
|
491
|
+
|| savedKnowledgeMap.status !== 'active'
|
|
492
|
+
|| savedKnowledgeMap.approval?.authorizationMode !== 'delegated') {
|
|
493
|
+
throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction Knowledge refresh validation failed.');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
assertMilestoneMembershipIntegrity(savedMilestone, (taskId) => this.store.readTask(identity.projectId, taskId));
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
oldTask: this.store.readTask(identity.projectId, oldTask.id),
|
|
501
|
+
replacementTask: this.store.readTask(identity.projectId, replacementTaskId),
|
|
502
|
+
discovery: this.store.readDiscovery(identity.projectId, discovery.id),
|
|
503
|
+
milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
252
506
|
materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
|
|
253
507
|
const { identity } = this.context(repository);
|
|
254
508
|
const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
|
|
@@ -784,6 +1038,7 @@ export class WorkflowService {
|
|
|
784
1038
|
const { identity } = this.context(repository);
|
|
785
1039
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
786
1040
|
assertExpectedRevision(task, expectedRevision);
|
|
1041
|
+
this.assertTaskContextChangeAllowed(task);
|
|
787
1042
|
if (!taskCanRebindKnowledge(task)) {
|
|
788
1043
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while Task is ${task.status}.`);
|
|
789
1044
|
}
|
|
@@ -870,6 +1125,7 @@ export class WorkflowService {
|
|
|
870
1125
|
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
871
1126
|
assertExpectedRevision(task, expectedTaskRevision);
|
|
872
1127
|
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
1128
|
+
this.assertTaskContextChangeAllowed(task);
|
|
873
1129
|
if (!taskCanRebindKnowledge(task)) {
|
|
874
1130
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
|
|
875
1131
|
}
|
|
@@ -887,45 +1143,72 @@ export class WorkflowService {
|
|
|
887
1143
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
888
1144
|
// leave the composite operation half-applied.
|
|
889
1145
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
this.
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1146
|
+
const projectRoot = this.store.projectRoot(identity.projectId);
|
|
1147
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
1148
|
+
return runRollbackProjectTransaction({
|
|
1149
|
+
projectRoot,
|
|
1150
|
+
projectId: identity.projectId,
|
|
1151
|
+
transactionId: `tx-${createEntityId('TASK', this.now().getTime()).toLowerCase()}`,
|
|
1152
|
+
kind: 'delegated-task-context-refresh',
|
|
1153
|
+
now: this.now(),
|
|
1154
|
+
targetPaths: [
|
|
1155
|
+
relative(path.join(projectRoot, 'knowledge-map.json')),
|
|
1156
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'state.json')),
|
|
1157
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan.md')),
|
|
1158
|
+
relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan-risk-audits.jsonl')),
|
|
1159
|
+
],
|
|
1160
|
+
mutate: () => {
|
|
1161
|
+
let knowledgeMap = currentMap;
|
|
1162
|
+
if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
|
|
1163
|
+
if (currentMap.status !== 'active' || !currentMap.approval) {
|
|
1164
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
1165
|
+
}
|
|
1166
|
+
assertDelegatedKnowledgeRefresh(refreshAssessment);
|
|
1167
|
+
if (inspectedMap.status !== 'stale') {
|
|
1168
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
1169
|
+
}
|
|
1170
|
+
if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
|
|
1171
|
+
this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
|
|
1172
|
+
}
|
|
1173
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
|
|
1174
|
+
knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision, true);
|
|
1175
|
+
transitions.push('project-memory reconcile');
|
|
1176
|
+
knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
|
|
1177
|
+
transitions.push('project-memory approve');
|
|
1178
|
+
}
|
|
1179
|
+
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
1180
|
+
if (task.knowledgeMapRevision === knowledgeMap.revision
|
|
1181
|
+
&& task.knowledgeMapHash === knowledgeMapHash) {
|
|
1182
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
1183
|
+
}
|
|
1184
|
+
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
1185
|
+
if (currentPlanRiskAudit) {
|
|
1186
|
+
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
1187
|
+
}
|
|
1188
|
+
transitions.push('task knowledge-rebind');
|
|
1189
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
|
|
1190
|
+
? 'Delegated Plan-bounded supporting-source context refresh.'
|
|
1191
|
+
: 'Delegated content-only context refresh.', delegationGrantId);
|
|
1192
|
+
transitions.push('task execution authorize');
|
|
1193
|
+
return {
|
|
1194
|
+
mode: refreshAssessment.mode,
|
|
1195
|
+
transitions,
|
|
1196
|
+
addedSources: refreshAssessment.addedSources,
|
|
1197
|
+
knowledgeMap,
|
|
1198
|
+
task: refreshedTask,
|
|
1199
|
+
};
|
|
1200
|
+
},
|
|
1201
|
+
validate: (result) => {
|
|
1202
|
+
const currentTask = this.store.readTask(identity.projectId, task.id);
|
|
1203
|
+
if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
|
|
1204
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
|
|
1205
|
+
}
|
|
1206
|
+
const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
|
|
1207
|
+
if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
|
|
1208
|
+
throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction Knowledge Map revision mismatch.');
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
929
1212
|
}
|
|
930
1213
|
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
931
1214
|
const { identity } = this.context(repository);
|
|
@@ -1166,7 +1449,6 @@ export class WorkflowService {
|
|
|
1166
1449
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
1167
1450
|
}
|
|
1168
1451
|
this.assertExecutionAuthorizationFresh(task);
|
|
1169
|
-
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1170
1452
|
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1171
1453
|
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1172
1454
|
if (currentPlanBlockingDecisions.length > 0) {
|
|
@@ -1207,6 +1489,7 @@ export class WorkflowService {
|
|
|
1207
1489
|
: null;
|
|
1208
1490
|
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
|
|
1209
1491
|
}
|
|
1492
|
+
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1210
1493
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1211
1494
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
1212
1495
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -1670,6 +1953,13 @@ export class WorkflowService {
|
|
|
1670
1953
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1671
1954
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1672
1955
|
}
|
|
1956
|
+
recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
1957
|
+
const context = this.context(repository);
|
|
1958
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1959
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1960
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
1961
|
+
return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
1962
|
+
}
|
|
1673
1963
|
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
1674
1964
|
const { identity } = this.context(repository);
|
|
1675
1965
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
@@ -1914,11 +2204,34 @@ export class WorkflowService {
|
|
|
1914
2204
|
const now = this.now().getTime();
|
|
1915
2205
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1916
2206
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2207
|
+
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2208
|
+
const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
2209
|
+
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
2210
|
+
...base,
|
|
2211
|
+
milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
|
|
2212
|
+
}));
|
|
2213
|
+
const dependencyPresent = inspectedVersions.declared !== null
|
|
2214
|
+
|| inspectedVersions.locked !== null
|
|
2215
|
+
|| inspectedVersions.installed !== null;
|
|
2216
|
+
const versionValues = dependencyPresent
|
|
2217
|
+
? [
|
|
2218
|
+
inspectedVersions.declared,
|
|
2219
|
+
inspectedVersions.locked,
|
|
2220
|
+
inspectedVersions.installed,
|
|
2221
|
+
inspectedVersions.currentBranch,
|
|
2222
|
+
...milestoneBases.map((base) => base.version),
|
|
2223
|
+
]
|
|
2224
|
+
: [];
|
|
2225
|
+
const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
|
|
2226
|
+
const versionBlockers = dependencyPresent && !versionsAligned
|
|
2227
|
+
? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
|
|
2228
|
+
: [];
|
|
1917
2229
|
const blockers = [
|
|
1918
2230
|
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1919
2231
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1920
2232
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1921
2233
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2234
|
+
...versionBlockers,
|
|
1922
2235
|
];
|
|
1923
2236
|
const adoption = buildAdoptionPreparation({
|
|
1924
2237
|
projectId: context.identity.projectId,
|
|
@@ -1945,6 +2258,20 @@ export class WorkflowService {
|
|
|
1945
2258
|
activeLeases,
|
|
1946
2259
|
staleLeases,
|
|
1947
2260
|
blockers,
|
|
2261
|
+
workflowVersions: {
|
|
2262
|
+
runtime: inspectedVersions.runtime,
|
|
2263
|
+
declared: inspectedVersions.declared,
|
|
2264
|
+
locked: inspectedVersions.locked,
|
|
2265
|
+
installed: inspectedVersions.installed,
|
|
2266
|
+
currentBranch: inspectedVersions.currentBranch,
|
|
2267
|
+
milestoneBases,
|
|
2268
|
+
},
|
|
2269
|
+
dependencyTransport: {
|
|
2270
|
+
safe: versionBlockers.length === 0,
|
|
2271
|
+
requiredAction: versionBlockers.length === 0
|
|
2272
|
+
? null
|
|
2273
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
2274
|
+
},
|
|
1948
2275
|
adoption,
|
|
1949
2276
|
};
|
|
1950
2277
|
}
|
|
@@ -2094,11 +2421,30 @@ export class WorkflowService {
|
|
|
2094
2421
|
};
|
|
2095
2422
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
2096
2423
|
}
|
|
2097
|
-
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
2424
|
+
reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
|
|
2098
2425
|
const { identity } = this.context(repository);
|
|
2099
2426
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2100
2427
|
assertExpectedRevision(current, expectedRevision);
|
|
2101
2428
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2429
|
+
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2430
|
+
const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
|
|
2431
|
+
const activeTask = this.store
|
|
2432
|
+
.listTasks(identity.projectId)
|
|
2433
|
+
.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2434
|
+
if (activeTask?.planHash
|
|
2435
|
+
&& taskCanRebindKnowledge(activeTask)
|
|
2436
|
+
&& !this.taskHasTerminalCorrectiveDecision(activeTask)) {
|
|
2437
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
2438
|
+
const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
|
|
2439
|
+
if (assessment.eligible
|
|
2440
|
+
&& this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
|
|
2441
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2442
|
+
taskId: activeTask.id,
|
|
2443
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2102
2448
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
2103
2449
|
}
|
|
2104
2450
|
graphRefreshRequest(repository, graphKind, mode = 'moderate') {
|
|
@@ -2157,10 +2503,17 @@ export class WorkflowService {
|
|
|
2157
2503
|
const taskNext = activeTask.status === 'planning'
|
|
2158
2504
|
? null
|
|
2159
2505
|
: this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
|
|
2163
|
-
|
|
2506
|
+
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
2507
|
+
// strict review are executable while context refresh correctly remains forbidden.
|
|
2508
|
+
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
2509
|
+
&& taskNext.correctiveDecisionGate?.hardStop === true;
|
|
2510
|
+
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
2511
|
+
if (taskNext?.action === 'task step-complete'
|
|
2512
|
+
|| taskNext?.action === 'task step-review'
|
|
2513
|
+
|| taskNext?.action === 'task remediation-mode-recover'
|
|
2514
|
+
|| hardStop
|
|
2515
|
+
|| terminalCorrectiveDecision === 'split-required'
|
|
2516
|
+
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2164
2517
|
return withAdoption(taskNext);
|
|
2165
2518
|
}
|
|
2166
2519
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
@@ -2183,11 +2536,17 @@ export class WorkflowService {
|
|
|
2183
2536
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2184
2537
|
: [];
|
|
2185
2538
|
return withAdoption({
|
|
2186
|
-
scope: 'project-memory',
|
|
2539
|
+
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2187
2540
|
status: knowledgeMap.status,
|
|
2188
|
-
action,
|
|
2541
|
+
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
2189
2542
|
revision: knowledgeMap.revision,
|
|
2190
|
-
|
|
2543
|
+
...(contextRefreshOptions.length > 0
|
|
2544
|
+
? { taskId: activeTask.id }
|
|
2545
|
+
: {
|
|
2546
|
+
stalenessOriginTaskId: activeTask.id,
|
|
2547
|
+
authorizesTask: false,
|
|
2548
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
2549
|
+
}),
|
|
2191
2550
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2192
2551
|
...(contextRefreshOptions.length > 0
|
|
2193
2552
|
? {
|
|
@@ -2423,9 +2782,18 @@ export class WorkflowService {
|
|
|
2423
2782
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
2424
2783
|
: [];
|
|
2425
2784
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2785
|
+
let remediationModeRecovery = null;
|
|
2786
|
+
let guardedCorrectivePosture = null;
|
|
2787
|
+
if (planRiskAudit) {
|
|
2788
|
+
try {
|
|
2789
|
+
guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
2790
|
+
}
|
|
2791
|
+
catch (error) {
|
|
2792
|
+
remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
|
|
2793
|
+
if (!remediationModeRecovery)
|
|
2794
|
+
throw error;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2429
2797
|
const next = nextForTask(task);
|
|
2430
2798
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2431
2799
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -2469,7 +2837,24 @@ export class WorkflowService {
|
|
|
2469
2837
|
}
|
|
2470
2838
|
: {};
|
|
2471
2839
|
let guardedCorrectiveOverlay = {};
|
|
2472
|
-
if (
|
|
2840
|
+
if (remediationModeRecovery) {
|
|
2841
|
+
guardedCorrectiveOverlay = {
|
|
2842
|
+
action: 'task remediation-mode-recover',
|
|
2843
|
+
stepId: remediationModeRecovery.remediation.stepId,
|
|
2844
|
+
remediationModeRecovery: {
|
|
2845
|
+
state: 'required',
|
|
2846
|
+
remediationEventId: remediationModeRecovery.remediation.eventId,
|
|
2847
|
+
remediationEventHash: remediationModeRecovery.remediation.eventHash,
|
|
2848
|
+
correctiveDecisionEventId: remediationModeRecovery.decision.eventId,
|
|
2849
|
+
correctiveDecisionRecoveryEventId: remediationModeRecovery.correctiveRecovery.eventId,
|
|
2850
|
+
attemptOrdinal: remediationModeRecovery.remediation.attemptOrdinal,
|
|
2851
|
+
fromMode: 'ordinary',
|
|
2852
|
+
toMode: 'corrective',
|
|
2853
|
+
requiredActor: remediationModeRecovery.actor,
|
|
2854
|
+
},
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
if (!remediationModeRecovery && !strictStepReviewStep && guardedCorrectivePosture) {
|
|
2473
2858
|
switch (guardedCorrectivePosture.posture) {
|
|
2474
2859
|
case 'decision-required':
|
|
2475
2860
|
if (guardedCorrectivePosture.correctiveDecision
|
|
@@ -2556,6 +2941,56 @@ export class WorkflowService {
|
|
|
2556
2941
|
};
|
|
2557
2942
|
break;
|
|
2558
2943
|
case 'split-required':
|
|
2944
|
+
{
|
|
2945
|
+
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
2946
|
+
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
2947
|
+
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
2948
|
+
const replacementMilestone = task.milestoneId
|
|
2949
|
+
? this.store.readMilestone(projectId, task.milestoneId)
|
|
2950
|
+
: null;
|
|
2951
|
+
guardedCorrectiveOverlay = {
|
|
2952
|
+
action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
|
|
2953
|
+
...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
|
|
2954
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2955
|
+
correctiveDecisionGate: {
|
|
2956
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2957
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2958
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2959
|
+
humanApprovalRequired: false,
|
|
2960
|
+
},
|
|
2961
|
+
replacementGate: {
|
|
2962
|
+
milestoneId: task.milestoneId,
|
|
2963
|
+
oldTaskId: task.id,
|
|
2964
|
+
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
2965
|
+
oldMembershipDisposition: 'cancelled',
|
|
2966
|
+
replacementMembershipDisposition: 'required',
|
|
2967
|
+
transaction: 'journal-recoverable',
|
|
2968
|
+
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
2969
|
+
id: discovery.id,
|
|
2970
|
+
revision: discovery.revision,
|
|
2971
|
+
goal: discovery.goal,
|
|
2972
|
+
})),
|
|
2973
|
+
...(replacementDiscovery && replacementMilestone
|
|
2974
|
+
? {
|
|
2975
|
+
exactTransition: {
|
|
2976
|
+
command: 'task replacement-materialize',
|
|
2977
|
+
taskId: task.id,
|
|
2978
|
+
expectedTaskRevision: task.revision,
|
|
2979
|
+
discoveryId: replacementDiscovery.id,
|
|
2980
|
+
expectedDiscoveryRevision: replacementDiscovery.revision,
|
|
2981
|
+
milestoneId: replacementMilestone.id,
|
|
2982
|
+
expectedMilestoneRevision: replacementMilestone.revision,
|
|
2983
|
+
},
|
|
2984
|
+
}
|
|
2985
|
+
: {
|
|
2986
|
+
requiredAction: replacementDiscoveries.length === 0
|
|
2987
|
+
? 'Create and complete one thin replacement Discovery, then run next again.'
|
|
2988
|
+
: 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
|
|
2989
|
+
}),
|
|
2990
|
+
},
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
break;
|
|
2559
2994
|
case 'stop-escalate':
|
|
2560
2995
|
guardedCorrectiveOverlay = {
|
|
2561
2996
|
action: 'doctor',
|
|
@@ -2624,6 +3059,34 @@ export class WorkflowService {
|
|
|
2624
3059
|
}
|
|
2625
3060
|
return result;
|
|
2626
3061
|
}
|
|
3062
|
+
assertTaskContextChangeAllowed(task) {
|
|
3063
|
+
const posture = this.taskTerminalCorrectivePosture(task);
|
|
3064
|
+
if (!posture)
|
|
3065
|
+
return;
|
|
3066
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
|
|
3067
|
+
taskId: task.id,
|
|
3068
|
+
stepId: posture.stepId,
|
|
3069
|
+
decision: posture.posture,
|
|
3070
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3071
|
+
requiredAction: posture.posture === 'split-required'
|
|
3072
|
+
? 'Materialize an atomic replacement Task through the exact replacement route.'
|
|
3073
|
+
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
taskHasTerminalCorrectiveDecision(task) {
|
|
3077
|
+
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
3078
|
+
}
|
|
3079
|
+
taskTerminalCorrectivePosture(task) {
|
|
3080
|
+
if (!task.planHash)
|
|
3081
|
+
return null;
|
|
3082
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
3083
|
+
if (!planRiskAudit)
|
|
3084
|
+
return null;
|
|
3085
|
+
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
3086
|
+
return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
|
|
3087
|
+
? posture
|
|
3088
|
+
: null;
|
|
3089
|
+
}
|
|
2627
3090
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
2628
3091
|
return this.store.listDelegations(projectId)
|
|
2629
3092
|
.filter((grant) => {
|