codex-workflow-v2 2.0.0-alpha.7.2.1 → 2.0.0-beta.2
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 +24 -5
- package/dist/src/alpha6/milestone.d.ts +27 -0
- package/dist/src/alpha6/milestone.js +152 -1
- 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 +239 -4
- 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/reviewer.d.ts +4 -0
- package/dist/src/reviewer.js +24 -7
- package/dist/src/reviewer.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 +28 -1
- package/dist/src/workflow.js +658 -54
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +11 -3
- package/docs/beta1-stabilization-brief.md +165 -0
- package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
- package/docs/delegated-approval.md +4 -3
- package/docs/development-flow.md +23 -6
- package/docs/project-memory.md +7 -5
- package/docs/release.md +16 -2
- package/docs/split-required-recovery.md +54 -0
- package/docs/stable-release-defect-register.md +597 -0
- package/docs/updating-existing-project.md +2 -0
- package/package.json +2 -2
- package/plugins/codex-workflow-gateway/references/protocol.md +62 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +53 -2
- package/references/state-machine.md +4 -1
- 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 { assessMilestoneInitialAssembly, 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);
|
|
@@ -1871,11 +2154,15 @@ export class WorkflowService {
|
|
|
1871
2154
|
const tasks = this.store.listTasks(identity.projectId);
|
|
1872
2155
|
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
1873
2156
|
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
|
|
2157
|
+
const milestones = this.listMilestonesCurrent(identity.projectId, adoption.currentPosture);
|
|
1874
2158
|
return {
|
|
1875
2159
|
projectId: identity.projectId,
|
|
1876
2160
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
1877
2161
|
tasks,
|
|
1878
|
-
milestones
|
|
2162
|
+
milestones,
|
|
2163
|
+
milestoneInitialAssembly: milestones
|
|
2164
|
+
.map((milestone) => this.describeMilestoneInitialAssembly(identity.projectId, milestone, tasks))
|
|
2165
|
+
.filter((value) => value !== null),
|
|
1879
2166
|
delegations: this.store.listDelegations(identity.projectId),
|
|
1880
2167
|
adoption,
|
|
1881
2168
|
};
|
|
@@ -1921,11 +2208,34 @@ export class WorkflowService {
|
|
|
1921
2208
|
const now = this.now().getTime();
|
|
1922
2209
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1923
2210
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2211
|
+
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2212
|
+
const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
2213
|
+
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
2214
|
+
...base,
|
|
2215
|
+
milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
|
|
2216
|
+
}));
|
|
2217
|
+
const dependencyPresent = inspectedVersions.declared !== null
|
|
2218
|
+
|| inspectedVersions.locked !== null
|
|
2219
|
+
|| inspectedVersions.installed !== null;
|
|
2220
|
+
const versionValues = dependencyPresent
|
|
2221
|
+
? [
|
|
2222
|
+
inspectedVersions.declared,
|
|
2223
|
+
inspectedVersions.locked,
|
|
2224
|
+
inspectedVersions.installed,
|
|
2225
|
+
inspectedVersions.currentBranch,
|
|
2226
|
+
...milestoneBases.map((base) => base.version),
|
|
2227
|
+
]
|
|
2228
|
+
: [];
|
|
2229
|
+
const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
|
|
2230
|
+
const versionBlockers = dependencyPresent && !versionsAligned
|
|
2231
|
+
? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
|
|
2232
|
+
: [];
|
|
1924
2233
|
const blockers = [
|
|
1925
2234
|
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1926
2235
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1927
2236
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1928
2237
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2238
|
+
...versionBlockers,
|
|
1929
2239
|
];
|
|
1930
2240
|
const adoption = buildAdoptionPreparation({
|
|
1931
2241
|
projectId: context.identity.projectId,
|
|
@@ -1952,6 +2262,20 @@ export class WorkflowService {
|
|
|
1952
2262
|
activeLeases,
|
|
1953
2263
|
staleLeases,
|
|
1954
2264
|
blockers,
|
|
2265
|
+
workflowVersions: {
|
|
2266
|
+
runtime: inspectedVersions.runtime,
|
|
2267
|
+
declared: inspectedVersions.declared,
|
|
2268
|
+
locked: inspectedVersions.locked,
|
|
2269
|
+
installed: inspectedVersions.installed,
|
|
2270
|
+
currentBranch: inspectedVersions.currentBranch,
|
|
2271
|
+
milestoneBases,
|
|
2272
|
+
},
|
|
2273
|
+
dependencyTransport: {
|
|
2274
|
+
safe: versionBlockers.length === 0,
|
|
2275
|
+
requiredAction: versionBlockers.length === 0
|
|
2276
|
+
? null
|
|
2277
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
2278
|
+
},
|
|
1955
2279
|
adoption,
|
|
1956
2280
|
};
|
|
1957
2281
|
}
|
|
@@ -2101,11 +2425,30 @@ export class WorkflowService {
|
|
|
2101
2425
|
};
|
|
2102
2426
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
2103
2427
|
}
|
|
2104
|
-
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
2428
|
+
reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
|
|
2105
2429
|
const { identity } = this.context(repository);
|
|
2106
2430
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2107
2431
|
assertExpectedRevision(current, expectedRevision);
|
|
2108
2432
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
2433
|
+
if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
|
|
2434
|
+
const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
|
|
2435
|
+
const activeTask = this.store
|
|
2436
|
+
.listTasks(identity.projectId)
|
|
2437
|
+
.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2438
|
+
if (activeTask?.planHash
|
|
2439
|
+
&& taskCanRebindKnowledge(activeTask)
|
|
2440
|
+
&& !this.taskHasTerminalCorrectiveDecision(activeTask)) {
|
|
2441
|
+
const inspected = inspectKnowledgeMap(current, scanned);
|
|
2442
|
+
const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
|
|
2443
|
+
if (assessment.eligible
|
|
2444
|
+
&& this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
|
|
2445
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
|
|
2446
|
+
taskId: activeTask.id,
|
|
2447
|
+
requiredAction: 'Run the exact task context-refresh action returned by next.',
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2109
2452
|
return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
|
|
2110
2453
|
}
|
|
2111
2454
|
graphRefreshRequest(repository, graphKind, mode = 'moderate') {
|
|
@@ -2168,10 +2511,13 @@ export class WorkflowService {
|
|
|
2168
2511
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
2169
2512
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
2170
2513
|
&& taskNext.correctiveDecisionGate?.hardStop === true;
|
|
2514
|
+
const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
|
|
2171
2515
|
if (taskNext?.action === 'task step-complete'
|
|
2172
2516
|
|| taskNext?.action === 'task step-review'
|
|
2173
2517
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
2174
|
-
|| hardStop
|
|
2518
|
+
|| hardStop
|
|
2519
|
+
|| terminalCorrectiveDecision === 'split-required'
|
|
2520
|
+
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
2175
2521
|
return withAdoption(taskNext);
|
|
2176
2522
|
}
|
|
2177
2523
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
@@ -2194,11 +2540,17 @@ export class WorkflowService {
|
|
|
2194
2540
|
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2195
2541
|
: [];
|
|
2196
2542
|
return withAdoption({
|
|
2197
|
-
scope: 'project-memory',
|
|
2543
|
+
scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
|
|
2198
2544
|
status: knowledgeMap.status,
|
|
2199
|
-
action,
|
|
2545
|
+
action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
|
|
2200
2546
|
revision: knowledgeMap.revision,
|
|
2201
|
-
|
|
2547
|
+
...(contextRefreshOptions.length > 0
|
|
2548
|
+
? { taskId: activeTask.id }
|
|
2549
|
+
: {
|
|
2550
|
+
stalenessOriginTaskId: activeTask.id,
|
|
2551
|
+
authorizesTask: false,
|
|
2552
|
+
provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
|
|
2553
|
+
}),
|
|
2202
2554
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2203
2555
|
...(contextRefreshOptions.length > 0
|
|
2204
2556
|
? {
|
|
@@ -2240,6 +2592,31 @@ export class WorkflowService {
|
|
|
2240
2592
|
}
|
|
2241
2593
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2242
2594
|
if (activeMilestone) {
|
|
2595
|
+
const initialAssembly = status.milestoneInitialAssembly
|
|
2596
|
+
.find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
|
|
2597
|
+
if (initialAssembly) {
|
|
2598
|
+
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
2599
|
+
if (activeDiscovery) {
|
|
2600
|
+
return withAdoption({
|
|
2601
|
+
scope: 'discovery',
|
|
2602
|
+
id: activeDiscovery.id,
|
|
2603
|
+
status: activeDiscovery.status,
|
|
2604
|
+
action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
|
|
2605
|
+
readiness: assessDiscovery(activeDiscovery),
|
|
2606
|
+
milestoneInitialAssembly: initialAssembly,
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
return withAdoption({
|
|
2610
|
+
scope: 'milestone',
|
|
2611
|
+
id: activeMilestone.id,
|
|
2612
|
+
status: activeMilestone.status,
|
|
2613
|
+
action: 'milestone initial-assembly',
|
|
2614
|
+
revision: activeMilestone.revision,
|
|
2615
|
+
linkedTaskIds: initialAssembly.linkedTaskIds,
|
|
2616
|
+
executionAvailable: initialAssembly.executionAvailable,
|
|
2617
|
+
milestoneInitialAssembly: initialAssembly,
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2243
2620
|
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
2244
2621
|
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
2245
2622
|
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
@@ -2446,7 +2823,7 @@ export class WorkflowService {
|
|
|
2446
2823
|
throw error;
|
|
2447
2824
|
}
|
|
2448
2825
|
}
|
|
2449
|
-
const next = nextForTask(task);
|
|
2826
|
+
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
2450
2827
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2451
2828
|
const strictStepReviewStep = planRiskAudit
|
|
2452
2829
|
? task.steps.find((step) => {
|
|
@@ -2593,6 +2970,56 @@ export class WorkflowService {
|
|
|
2593
2970
|
};
|
|
2594
2971
|
break;
|
|
2595
2972
|
case 'split-required':
|
|
2973
|
+
{
|
|
2974
|
+
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
2975
|
+
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
2976
|
+
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
2977
|
+
const replacementMilestone = task.milestoneId
|
|
2978
|
+
? this.store.readMilestone(projectId, task.milestoneId)
|
|
2979
|
+
: null;
|
|
2980
|
+
guardedCorrectiveOverlay = {
|
|
2981
|
+
action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
|
|
2982
|
+
...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
|
|
2983
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2984
|
+
correctiveDecisionGate: {
|
|
2985
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2986
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2987
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2988
|
+
humanApprovalRequired: false,
|
|
2989
|
+
},
|
|
2990
|
+
replacementGate: {
|
|
2991
|
+
milestoneId: task.milestoneId,
|
|
2992
|
+
oldTaskId: task.id,
|
|
2993
|
+
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
2994
|
+
oldMembershipDisposition: 'cancelled',
|
|
2995
|
+
replacementMembershipDisposition: 'required',
|
|
2996
|
+
transaction: 'journal-recoverable',
|
|
2997
|
+
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
2998
|
+
id: discovery.id,
|
|
2999
|
+
revision: discovery.revision,
|
|
3000
|
+
goal: discovery.goal,
|
|
3001
|
+
})),
|
|
3002
|
+
...(replacementDiscovery && replacementMilestone
|
|
3003
|
+
? {
|
|
3004
|
+
exactTransition: {
|
|
3005
|
+
command: 'task replacement-materialize',
|
|
3006
|
+
taskId: task.id,
|
|
3007
|
+
expectedTaskRevision: task.revision,
|
|
3008
|
+
discoveryId: replacementDiscovery.id,
|
|
3009
|
+
expectedDiscoveryRevision: replacementDiscovery.revision,
|
|
3010
|
+
milestoneId: replacementMilestone.id,
|
|
3011
|
+
expectedMilestoneRevision: replacementMilestone.revision,
|
|
3012
|
+
},
|
|
3013
|
+
}
|
|
3014
|
+
: {
|
|
3015
|
+
requiredAction: replacementDiscoveries.length === 0
|
|
3016
|
+
? 'Create and complete one thin replacement Discovery, then run next again.'
|
|
3017
|
+
: 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
|
|
3018
|
+
}),
|
|
3019
|
+
},
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
break;
|
|
2596
3023
|
case 'stop-escalate':
|
|
2597
3024
|
guardedCorrectiveOverlay = {
|
|
2598
3025
|
action: 'doctor',
|
|
@@ -2653,13 +3080,59 @@ export class WorkflowService {
|
|
|
2653
3080
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2654
3081
|
};
|
|
2655
3082
|
if (handoffPosture.state === 'pending') {
|
|
2656
|
-
return {
|
|
3083
|
+
return this.withTaskNavigationContracts(task, {
|
|
2657
3084
|
...result,
|
|
2658
3085
|
action: 'task claim',
|
|
2659
3086
|
blockedAction: next.action,
|
|
2660
|
-
};
|
|
3087
|
+
});
|
|
2661
3088
|
}
|
|
2662
|
-
|
|
3089
|
+
const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
|
|
3090
|
+
.includes(String(result.action))
|
|
3091
|
+
? {
|
|
3092
|
+
...result,
|
|
3093
|
+
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
3094
|
+
}
|
|
3095
|
+
: result;
|
|
3096
|
+
return this.withTaskNavigationContracts(task, navigableResult);
|
|
3097
|
+
}
|
|
3098
|
+
taskPlanContract(projectId, task) {
|
|
3099
|
+
const map = this.store.readKnowledgeMap(projectId);
|
|
3100
|
+
return {
|
|
3101
|
+
requiredRequirementIds: [...task.requirements],
|
|
3102
|
+
requiredAcceptanceIds: [...task.acceptance],
|
|
3103
|
+
currentRisks: [...task.risks],
|
|
3104
|
+
exactInclusionRequired: true,
|
|
3105
|
+
briefHash: task.briefHash,
|
|
3106
|
+
knowledgeMap: knowledgeSummary(map),
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
assertTaskContextChangeAllowed(task) {
|
|
3110
|
+
const posture = this.taskTerminalCorrectivePosture(task);
|
|
3111
|
+
if (!posture)
|
|
3112
|
+
return;
|
|
3113
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
|
|
3114
|
+
taskId: task.id,
|
|
3115
|
+
stepId: posture.stepId,
|
|
3116
|
+
decision: posture.posture,
|
|
3117
|
+
decisionEventId: posture.correctiveDecision?.eventId ?? null,
|
|
3118
|
+
requiredAction: posture.posture === 'split-required'
|
|
3119
|
+
? 'Materialize an atomic replacement Task through the exact replacement route.'
|
|
3120
|
+
: 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
taskHasTerminalCorrectiveDecision(task) {
|
|
3124
|
+
return this.taskTerminalCorrectivePosture(task) !== null;
|
|
3125
|
+
}
|
|
3126
|
+
taskTerminalCorrectivePosture(task) {
|
|
3127
|
+
if (!task.planHash)
|
|
3128
|
+
return null;
|
|
3129
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
3130
|
+
if (!planRiskAudit)
|
|
3131
|
+
return null;
|
|
3132
|
+
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
3133
|
+
return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
|
|
3134
|
+
? posture
|
|
3135
|
+
: null;
|
|
2663
3136
|
}
|
|
2664
3137
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
2665
3138
|
return this.store.listDelegations(projectId)
|
|
@@ -2779,6 +3252,70 @@ export class WorkflowService {
|
|
|
2779
3252
|
? milestone
|
|
2780
3253
|
: readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
2781
3254
|
}
|
|
3255
|
+
taskGitNavigationOverride(task) {
|
|
3256
|
+
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
3260
|
+
if (currentBase === task.baseCommit)
|
|
3261
|
+
return null;
|
|
3262
|
+
return {
|
|
3263
|
+
scope: 'task',
|
|
3264
|
+
id: task.id,
|
|
3265
|
+
status: task.status,
|
|
3266
|
+
action: 'task sync-base',
|
|
3267
|
+
revision: task.revision,
|
|
3268
|
+
baseSyncRequired: {
|
|
3269
|
+
baseBranch: task.baseBranch,
|
|
3270
|
+
recordedBaseCommit: task.baseCommit,
|
|
3271
|
+
currentBaseCommit: currentBase,
|
|
3272
|
+
},
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
withTaskNavigationContracts(task, next) {
|
|
3276
|
+
const action = typeof next.action === 'string' ? next.action : null;
|
|
3277
|
+
if (!action)
|
|
3278
|
+
return next;
|
|
3279
|
+
const policy = gitMutationPolicyForAction(task, action);
|
|
3280
|
+
const claimedC1Posture = readTaskC1Posture(this.store, task).state === 'claimed';
|
|
3281
|
+
const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
|
|
3282
|
+
return {
|
|
3283
|
+
...next,
|
|
3284
|
+
...(policy ? { gitMutationPolicy: policy } : {}),
|
|
3285
|
+
...(writerTokenContract ? { writerTokenContract } : {}),
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
describeMilestoneInitialAssembly(projectId, milestone, tasks) {
|
|
3289
|
+
const assessment = assessMilestoneInitialAssembly({
|
|
3290
|
+
store: this.store,
|
|
3291
|
+
projectId,
|
|
3292
|
+
milestone,
|
|
3293
|
+
listTasks: () => tasks,
|
|
3294
|
+
});
|
|
3295
|
+
if (!assessment.eligible)
|
|
3296
|
+
return null;
|
|
3297
|
+
const options = [
|
|
3298
|
+
{
|
|
3299
|
+
action: 'linked-task-assembly',
|
|
3300
|
+
commands: ['discovery start', 'discovery materialize'],
|
|
3301
|
+
description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
|
|
3302
|
+
},
|
|
3303
|
+
];
|
|
3304
|
+
if (assessment.orphanTaskIds.length > 0) {
|
|
3305
|
+
options.push({
|
|
3306
|
+
action: 'milestone plan-set',
|
|
3307
|
+
commands: ['milestone plan-set'],
|
|
3308
|
+
description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
return {
|
|
3312
|
+
milestoneId: milestone.id,
|
|
3313
|
+
revision: milestone.revision,
|
|
3314
|
+
linkedTaskIds: assessment.orphanTaskIds,
|
|
3315
|
+
executionAvailable: false,
|
|
3316
|
+
options,
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
2782
3319
|
prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
|
|
2783
3320
|
validateMilestonePlan(plan);
|
|
2784
3321
|
return prepareMilestoneScopeChangeCandidate({
|
|
@@ -3434,13 +3971,13 @@ function nextForTask(task) {
|
|
|
3434
3971
|
awaiting_execution_authorization: 'task authorize',
|
|
3435
3972
|
ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
|
|
3436
3973
|
in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
|
|
3437
|
-
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-
|
|
3974
|
+
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
|
|
3438
3975
|
needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
|
|
3439
3976
|
awaiting_final_acceptance: 'task accept',
|
|
3440
3977
|
merging: 'task merge',
|
|
3441
3978
|
ready_to_merge: 'task merge-confirm',
|
|
3442
3979
|
merged: 'none',
|
|
3443
|
-
blocked: task.review.status === 'unverified' ? 'task review-
|
|
3980
|
+
blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
|
|
3444
3981
|
cancelled: 'none',
|
|
3445
3982
|
};
|
|
3446
3983
|
const selectedStep = activeStep ?? failedStep ?? runnableStep;
|
|
@@ -3462,6 +3999,73 @@ function nextForTask(task) {
|
|
|
3462
3999
|
: {}),
|
|
3463
4000
|
};
|
|
3464
4001
|
}
|
|
4002
|
+
function gitMutationPolicyForAction(task, action) {
|
|
4003
|
+
const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
|
|
4004
|
+
switch (action) {
|
|
4005
|
+
case 'task start':
|
|
4006
|
+
return {
|
|
4007
|
+
permission: 'git-metadata-write',
|
|
4008
|
+
requiredBeforeFirstCall: true,
|
|
4009
|
+
boundedToTransition: true,
|
|
4010
|
+
transition: 'task start',
|
|
4011
|
+
appliesWhen: { workspaceOwner: 'local' },
|
|
4012
|
+
metadataWrites,
|
|
4013
|
+
};
|
|
4014
|
+
case 'task step-complete':
|
|
4015
|
+
return {
|
|
4016
|
+
permission: 'git-metadata-write',
|
|
4017
|
+
requiredBeforeFirstCall: true,
|
|
4018
|
+
boundedToTransition: true,
|
|
4019
|
+
transition: 'task step-complete',
|
|
4020
|
+
metadataWrites,
|
|
4021
|
+
};
|
|
4022
|
+
case 'task merge':
|
|
4023
|
+
if (task.workspaceOwner !== 'local')
|
|
4024
|
+
return null;
|
|
4025
|
+
return {
|
|
4026
|
+
permission: 'git-metadata-write',
|
|
4027
|
+
requiredBeforeFirstCall: true,
|
|
4028
|
+
boundedToTransition: true,
|
|
4029
|
+
transition: 'task merge',
|
|
4030
|
+
metadataWrites,
|
|
4031
|
+
};
|
|
4032
|
+
case 'task sync-base':
|
|
4033
|
+
return {
|
|
4034
|
+
permission: 'git-metadata-write',
|
|
4035
|
+
requiredBeforeFirstCall: true,
|
|
4036
|
+
boundedToTransition: true,
|
|
4037
|
+
transition: 'task sync-base',
|
|
4038
|
+
metadataWrites,
|
|
4039
|
+
};
|
|
4040
|
+
default:
|
|
4041
|
+
return null;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
4045
|
+
const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
|
|
4046
|
+
const claimedC1LeaseBound = [
|
|
4047
|
+
'task run',
|
|
4048
|
+
'task step-review',
|
|
4049
|
+
'task review-record',
|
|
4050
|
+
'task review-launch',
|
|
4051
|
+
'task corrective-decision-recover',
|
|
4052
|
+
'task remediation-mode-recover',
|
|
4053
|
+
'task corrective-decision',
|
|
4054
|
+
'task handoff',
|
|
4055
|
+
'task handoff-prepare',
|
|
4056
|
+
'task handback',
|
|
4057
|
+
'task handback-create',
|
|
4058
|
+
];
|
|
4059
|
+
if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
|
|
4060
|
+
return null;
|
|
4061
|
+
}
|
|
4062
|
+
return {
|
|
4063
|
+
required: true,
|
|
4064
|
+
option: '--writer-token',
|
|
4065
|
+
source: 'active-writer-lease',
|
|
4066
|
+
sensitive: true,
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
3465
4069
|
function knowledgeSummary(map) {
|
|
3466
4070
|
return {
|
|
3467
4071
|
revision: map.revision,
|