codex-workflow-v2 2.0.0-alpha.7.2.1 → 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 +12 -4
- 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/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 +19 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +20 -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 +10 -1
- package/dist/src/workflow.js +473 -47
- package/dist/src/workflow.js.map +1 -1
- 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/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
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, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.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);
|
|
@@ -1921,11 +2204,34 @@ export class WorkflowService {
|
|
|
1921
2204
|
const now = this.now().getTime();
|
|
1922
2205
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1923
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
|
+
: [];
|
|
1924
2229
|
const blockers = [
|
|
1925
2230
|
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1926
2231
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1927
2232
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1928
2233
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2234
|
+
...versionBlockers,
|
|
1929
2235
|
];
|
|
1930
2236
|
const adoption = buildAdoptionPreparation({
|
|
1931
2237
|
projectId: context.identity.projectId,
|
|
@@ -1952,6 +2258,20 @@ export class WorkflowService {
|
|
|
1952
2258
|
activeLeases,
|
|
1953
2259
|
staleLeases,
|
|
1954
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
|
+
},
|
|
1955
2275
|
adoption,
|
|
1956
2276
|
};
|
|
1957
2277
|
}
|
|
@@ -2101,11 +2421,30 @@ export class WorkflowService {
|
|
|
2101
2421
|
};
|
|
2102
2422
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
2103
2423
|
}
|
|
2104
|
-
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
2424
|
+
reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
|
|
2105
2425
|
const { identity } = this.context(repository);
|
|
2106
2426
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2107
2427
|
assertExpectedRevision(current, expectedRevision);
|
|
2108
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
|
+
}
|
|
2109
2448
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
2110
2449
|
}
|
|
2111
2450
|
graphRefreshRequest(repository, graphKind, mode = 'moderate') {
|
|
@@ -2168,10 +2507,13 @@ export class WorkflowService {
|
|
|
2168
2507
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2169
2508
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
2170
2509
|
&& taskNext.correctiveDecisionGate?.hardStop === true;
|
|
2510
|
+
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
2171
2511
|
if (taskNext?.action === 'task step-complete'
|
|
2172
2512
|
|| taskNext?.action === 'task step-review'
|
|
2173
2513
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
2174
|
-
|| hardStop
|
|
2514
|
+
|| hardStop
|
|
2515
|
+
|| terminalCorrectiveDecision === 'split-required'
|
|
2516
|
+
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2175
2517
|
return withAdoption(taskNext);
|
|
2176
2518
|
}
|
|
2177
2519
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
@@ -2194,11 +2536,17 @@ export class WorkflowService {
|
|
|
2194
2536
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2195
2537
|
: [];
|
|
2196
2538
|
return withAdoption({
|
|
2197
|
-
scope: 'project-memory',
|
|
2539
|
+
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2198
2540
|
status: knowledgeMap.status,
|
|
2199
|
-
action,
|
|
2541
|
+
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
2200
2542
|
revision: knowledgeMap.revision,
|
|
2201
|
-
|
|
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
|
+
}),
|
|
2202
2550
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2203
2551
|
...(contextRefreshOptions.length > 0
|
|
2204
2552
|
? {
|
|
@@ -2593,6 +2941,56 @@ export class WorkflowService {
|
|
|
2593
2941
|
};
|
|
2594
2942
|
break;
|
|
2595
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;
|
|
2596
2994
|
case 'stop-escalate':
|
|
2597
2995
|
guardedCorrectiveOverlay = {
|
|
2598
2996
|
action: 'doctor',
|
|
@@ -2661,6 +3059,34 @@ export class WorkflowService {
|
|
|
2661
3059
|
}
|
|
2662
3060
|
return result;
|
|
2663
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
|
+
}
|
|
2664
3090
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
2665
3091
|
return this.store.listDelegations(projectId)
|
|
2666
3092
|
.filter((grant) => {
|