codex-workflow-v2 2.0.0-alpha.4 → 2.0.0-alpha.6.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 +114 -10
- package/dist/src/alpha6/adoption.d.ts +55 -0
- package/dist/src/alpha6/adoption.js +920 -0
- package/dist/src/alpha6/adoption.js.map +1 -0
- package/dist/src/alpha6/handoff.d.ts +39 -0
- package/dist/src/alpha6/handoff.js +975 -0
- package/dist/src/alpha6/handoff.js.map +1 -0
- package/dist/src/alpha6/journal.d.ts +30 -0
- package/dist/src/alpha6/journal.js +369 -0
- package/dist/src/alpha6/journal.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +49 -0
- package/dist/src/alpha6/milestone.js +1049 -0
- package/dist/src/alpha6/milestone.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +32 -0
- package/dist/src/alpha6/plan-risk.js +847 -0
- package/dist/src/alpha6/plan-risk.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +20 -0
- package/dist/src/alpha6/remediation.js +748 -0
- package/dist/src/alpha6/remediation.js.map +1 -0
- package/dist/src/alpha6/review.d.ts +46 -0
- package/dist/src/alpha6/review.js +785 -0
- package/dist/src/alpha6/review.js.map +1 -0
- package/dist/src/alpha6/store-sidecars.d.ts +35 -0
- package/dist/src/alpha6/store-sidecars.js +281 -0
- package/dist/src/alpha6/store-sidecars.js.map +1 -0
- package/dist/src/cli.js +95 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +259 -0
- package/dist/src/git.js +2 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/reviewer.d.ts +6 -1
- package/dist/src/reviewer.js +145 -32
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +7 -1
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/state/store.d.ts +39 -1
- package/dist/src/state/store.js +127 -1
- package/dist/src/state/store.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 +123 -9
- package/dist/src/workflow.js +1497 -76
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +143 -0
- package/docs/decisions.md +27 -0
- package/docs/delegated-approval.md +30 -5
- package/docs/development-flow.md +40 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +155 -136
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +236 -223
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +225 -206
- package/docs/project-memory.md +7 -0
- package/docs/release.md +7 -0
- package/docs/updating-existing-project.md +75 -5
- package/docs/validation-report.md +56 -34
- package/package.json +2 -2
- package/plugins/codex-workflow-gateway/references/protocol.md +155 -6
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +23 -1
- package/references/git-policy.md +5 -2
- package/references/state-machine.md +36 -0
- package/references/validation-and-review.md +28 -1
- package/roles/delivery-coordinator.md +11 -0
- package/roles/independent-reviewer.md +3 -0
- package/roles/technical-planner.md +7 -0
- package/roles/worker.md +4 -0
- package/schemas/adoption-posture-event.schema.json +129 -0
- package/schemas/corrective-decision-event.schema.json +55 -0
- package/schemas/corrective-plan-audit.schema.json +20 -0
- package/schemas/milestone-scope-change-event.schema.json +68 -0
- package/schemas/milestone-transaction-journal.schema.json +95 -0
- package/schemas/plan-risk-audit-event.schema.json +90 -0
- package/schemas/remediation-event.schema.json +53 -0
- package/schemas/reviewer-attestation-event.schema.json +49 -0
- package/schemas/step-review-event.schema.json +74 -0
- package/schemas/task-handoff-event.schema.json +116 -0
package/dist/src/workflow.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { appendFileSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
5
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
6
|
-
import { assertDelegationCanAuthorize, delegationCanAuthorize, prepareDelegationGrantGate, } from './delegation.js';
|
|
6
|
+
import { assertDelegationCanAuthorize, delegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
7
7
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
8
8
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
9
9
|
import { WorkflowError } from './errors.js';
|
|
10
|
+
import { inspectLocalDependency } from './diagnostics.js';
|
|
10
11
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
12
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
|
+
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
14
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
15
|
+
import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
|
+
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
|
+
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
11
18
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
19
|
+
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
12
20
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
13
21
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
22
|
+
import { launchStrictStepReviewer } from './reviewer.js';
|
|
14
23
|
import { WriterLockManager } from './state/lock.js';
|
|
15
24
|
import { FileStateStore } from './state/store.js';
|
|
16
25
|
import { createEntityId } from './ulid.js';
|
|
@@ -24,6 +33,18 @@ export class WorkflowService {
|
|
|
24
33
|
}
|
|
25
34
|
context(repository) {
|
|
26
35
|
const identity = resolveRepositoryIdentity(repository);
|
|
36
|
+
const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
|
|
37
|
+
const projectAlreadyRegistered = existsSync(projectFile);
|
|
38
|
+
if (!projectAlreadyRegistered) {
|
|
39
|
+
initializeProjectRegistrationAdoption(this.store, {
|
|
40
|
+
projectId: identity.projectId,
|
|
41
|
+
repositoryIdentity: identity,
|
|
42
|
+
packageVersion: PACKAGE_VERSION,
|
|
43
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
44
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
45
|
+
now: this.now(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
27
48
|
this.store.registerProject(identity);
|
|
28
49
|
return {
|
|
29
50
|
identity,
|
|
@@ -162,7 +183,7 @@ export class WorkflowService {
|
|
|
162
183
|
if (!title.trim())
|
|
163
184
|
throw new WorkflowError('INVALID_ARGUMENT', 'Task title is required.');
|
|
164
185
|
if (milestoneId) {
|
|
165
|
-
const milestone = this.
|
|
186
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
166
187
|
if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
|
|
167
188
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
168
189
|
}
|
|
@@ -271,17 +292,29 @@ export class WorkflowService {
|
|
|
271
292
|
}
|
|
272
293
|
setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
|
|
273
294
|
const { identity } = this.context(repository);
|
|
274
|
-
const milestone = this.
|
|
295
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
275
296
|
assertExpectedRevision(milestone, expectedRevision);
|
|
276
297
|
if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
|
|
277
298
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Milestone Plan while ${milestone.status}.`);
|
|
278
299
|
}
|
|
300
|
+
if (milestone.status !== 'planning'
|
|
301
|
+
|| milestone.planHash !== null
|
|
302
|
+
|| milestone.membershipRevision !== 0
|
|
303
|
+
|| milestone.taskIds.length > 0
|
|
304
|
+
|| milestone.memberships.length > 0) {
|
|
305
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan is no longer in the initial empty planning posture. Use explicit scope-change prepare/apply.', {
|
|
306
|
+
milestoneId,
|
|
307
|
+
revision: milestone.revision,
|
|
308
|
+
requiredAction: 'prepareMilestoneScopeChange -> applyMilestoneScopeChange',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
279
311
|
validateMilestonePlan(plan);
|
|
280
|
-
const
|
|
312
|
+
const normalized = normalizeMilestonePlan(plan);
|
|
313
|
+
const taskIds = normalized.taskIds;
|
|
281
314
|
if (new Set(taskIds).size !== taskIds.length) {
|
|
282
315
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone memberships must have unique Task IDs.');
|
|
283
316
|
}
|
|
284
|
-
for (const membership of
|
|
317
|
+
for (const membership of normalized.memberships) {
|
|
285
318
|
const task = this.store.readTask(identity.projectId, membership.taskId);
|
|
286
319
|
if (task.milestoneId !== milestoneId) {
|
|
287
320
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} is not linked to Milestone ${milestoneId}.`);
|
|
@@ -296,22 +329,18 @@ export class WorkflowService {
|
|
|
296
329
|
throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
|
-
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
300
|
-
const normalized = {
|
|
301
|
-
outcome: plan.outcome.trim(),
|
|
302
|
-
successSignal: plan.successSignal.trim(),
|
|
303
|
-
acceptance: [...plan.acceptance],
|
|
304
|
-
memberships: plan.memberships.map((membership) => ({
|
|
305
|
-
...membership,
|
|
306
|
-
reason: membership.reason.trim(),
|
|
307
|
-
})),
|
|
308
|
-
checks: [...plan.checks],
|
|
309
|
-
};
|
|
310
|
-
const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify(normalized, null, 2));
|
|
311
332
|
const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
312
333
|
? { ...authorization, decision: 'superseded' }
|
|
313
334
|
: authorization);
|
|
314
|
-
const
|
|
335
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
336
|
+
const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
|
|
337
|
+
outcome: normalized.outcome,
|
|
338
|
+
successSignal: normalized.successSignal,
|
|
339
|
+
acceptance: normalized.acceptance,
|
|
340
|
+
memberships: normalized.memberships,
|
|
341
|
+
checks: normalized.checks,
|
|
342
|
+
}, null, 2));
|
|
343
|
+
const candidate = {
|
|
315
344
|
...milestone,
|
|
316
345
|
status: 'awaiting_execution_authorization',
|
|
317
346
|
outcome: normalized.outcome,
|
|
@@ -327,13 +356,55 @@ export class WorkflowService {
|
|
|
327
356
|
evidenceHash: null,
|
|
328
357
|
acceptedHead: null,
|
|
329
358
|
cancellationReason: null,
|
|
330
|
-
}
|
|
359
|
+
};
|
|
360
|
+
assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
|
|
361
|
+
const saved = this.store.writeMilestone(candidate, expectedRevision);
|
|
331
362
|
this.store.publishArtifact(stagedPlan);
|
|
332
363
|
return saved;
|
|
333
364
|
}
|
|
365
|
+
prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
|
|
366
|
+
const { identity } = this.context(repository);
|
|
367
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
368
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
369
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
370
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
371
|
+
return {
|
|
372
|
+
milestoneId: prepared.milestoneId,
|
|
373
|
+
revision: prepared.expectedRevision,
|
|
374
|
+
actor: prepared.actor,
|
|
375
|
+
candidatePlanHash: prepared.candidatePlanHash,
|
|
376
|
+
candidateMembershipRevision: prepared.candidateMembershipRevision,
|
|
377
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
378
|
+
confirmationCode: prepared.confirmationCode,
|
|
379
|
+
after: prepared.after,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
383
|
+
const { identity } = this.context(repository);
|
|
384
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
385
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
386
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
387
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
388
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
389
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
|
|
390
|
+
milestoneId,
|
|
391
|
+
revision: milestone.revision,
|
|
392
|
+
actor: prepared.actor,
|
|
393
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return applyMilestoneScopeChangeTransaction({
|
|
397
|
+
store: this.store,
|
|
398
|
+
milestone,
|
|
399
|
+
expectedRevision,
|
|
400
|
+
prepared,
|
|
401
|
+
now: this.now(),
|
|
402
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
334
405
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
335
406
|
const { identity } = this.context(repository);
|
|
336
|
-
const milestone = this.
|
|
407
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
337
408
|
assertExpectedRevision(milestone, expectedRevision);
|
|
338
409
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
339
410
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
@@ -362,7 +433,7 @@ export class WorkflowService {
|
|
|
362
433
|
}
|
|
363
434
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
364
435
|
const { identity } = this.context(repository);
|
|
365
|
-
const milestone = this.
|
|
436
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
366
437
|
assertExpectedRevision(milestone, expectedRevision);
|
|
367
438
|
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
368
439
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
@@ -425,7 +496,7 @@ export class WorkflowService {
|
|
|
425
496
|
}
|
|
426
497
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
427
498
|
const { identity } = this.context(repository);
|
|
428
|
-
const milestone = this.
|
|
499
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
429
500
|
assertExpectedRevision(milestone, expectedRevision);
|
|
430
501
|
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
431
502
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
|
|
@@ -480,23 +551,93 @@ export class WorkflowService {
|
|
|
480
551
|
if (!reason.trim())
|
|
481
552
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
482
553
|
const { identity } = this.context(repository);
|
|
483
|
-
const milestone = this.
|
|
554
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
484
555
|
assertExpectedRevision(milestone, expectedRevision);
|
|
485
556
|
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
486
557
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
487
558
|
}
|
|
488
559
|
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
489
560
|
}
|
|
490
|
-
setTaskPlan(repository, taskId, expectedRevision, plan) {
|
|
561
|
+
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
491
562
|
validatePlan(plan);
|
|
492
563
|
const { identity } = this.context(repository);
|
|
493
564
|
const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
|
|
494
565
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
495
566
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
496
567
|
assertExpectedRevision(task, expectedRevision);
|
|
497
|
-
if (!['planning', 'awaiting_execution_authorization', 'needs_fix'].includes(task.status)) {
|
|
568
|
+
if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
498
569
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
499
570
|
}
|
|
571
|
+
const currentPlanCorrectiveDecisions = task.planHash
|
|
572
|
+
? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
|
|
573
|
+
: [];
|
|
574
|
+
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
575
|
+
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
|
|
576
|
+
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
577
|
+
const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
|
|
578
|
+
if (currentHardBlockingDecisions.length > 0) {
|
|
579
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
|
|
580
|
+
taskId: task.id,
|
|
581
|
+
requiredAction: 'Escalate or split the work according to the recorded corrective decision.',
|
|
582
|
+
decisions: currentHardBlockingDecisions.map((event) => ({
|
|
583
|
+
eventId: event.eventId,
|
|
584
|
+
stepId: event.stepId,
|
|
585
|
+
decision: event.decision,
|
|
586
|
+
})),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (task.status === 'blocked' && currentReplanDecisions.length === 0) {
|
|
590
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task ${task.id} is blocked unless the current Plan already carries a replan-required corrective decision.`, {
|
|
591
|
+
taskId: task.id,
|
|
592
|
+
status: task.status,
|
|
593
|
+
requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (currentContinueFixDecisions.length > 0) {
|
|
597
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current continue-fix corrective decision; use the current Plan retry path instead of issuing another plan-set.`, {
|
|
598
|
+
taskId: task.id,
|
|
599
|
+
requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
|
|
600
|
+
decisions: currentContinueFixDecisions.map((event) => ({
|
|
601
|
+
eventId: event.eventId,
|
|
602
|
+
stepId: event.stepId,
|
|
603
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
604
|
+
})),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
608
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
609
|
+
: null;
|
|
610
|
+
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
611
|
+
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
612
|
+
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
613
|
+
}
|
|
614
|
+
const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
|
|
615
|
+
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
616
|
+
: null;
|
|
617
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
|
|
618
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
|
|
619
|
+
taskId: task.id,
|
|
620
|
+
stepId: guardedRemediationGate.stepId,
|
|
621
|
+
attemptCount: guardedRemediationGate.attemptCount,
|
|
622
|
+
requiredSidecar: 'corrective-decisions.jsonl',
|
|
623
|
+
allowedDecision: 'continue-fix',
|
|
624
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (currentReplanDecisions.length > 0 && correctiveAudit) {
|
|
628
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current replan-required corrective decision; submit the replacement Plan without recording another decision in plan-set.`, {
|
|
629
|
+
taskId: task.id,
|
|
630
|
+
decision: correctiveAudit.decision,
|
|
631
|
+
requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
|
|
635
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective-decision record before execution may continue; plan-set only supports continue-fix.`, {
|
|
636
|
+
taskId: task.id,
|
|
637
|
+
stepId: guardedRemediationGate.stepId,
|
|
638
|
+
decision: correctiveAudit?.decision ?? null,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
500
641
|
for (const requirement of task.requirements) {
|
|
501
642
|
if (!plan.requirements.includes(requirement)) {
|
|
502
643
|
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
|
|
@@ -525,7 +666,24 @@ export class WorkflowService {
|
|
|
525
666
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
526
667
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
527
668
|
}
|
|
528
|
-
|
|
669
|
+
if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
|
|
670
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Replacement Plan under replan-required must retain every guarded Step targeted by the current corrective decision; removing or renaming one would reset remediation history.`, {
|
|
671
|
+
taskId: task.id,
|
|
672
|
+
requiredStepIds: currentReplanRequiredStepIds,
|
|
673
|
+
plannedStepIds: plannedSteps.map((step) => step.id),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
677
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
|
|
678
|
+
taskId: task.id,
|
|
679
|
+
stepId: guardedRemediationGate.stepId,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
683
|
+
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
684
|
+
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
685
|
+
}
|
|
686
|
+
const candidate = {
|
|
529
687
|
...task,
|
|
530
688
|
status: 'awaiting_execution_authorization',
|
|
531
689
|
planHash: stagedPlan.hash,
|
|
@@ -545,8 +703,16 @@ export class WorkflowService {
|
|
|
545
703
|
resultHash: null,
|
|
546
704
|
evidenceHash: null,
|
|
547
705
|
blockReason: null,
|
|
548
|
-
}
|
|
706
|
+
};
|
|
707
|
+
if (planRiskAudit)
|
|
708
|
+
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
709
|
+
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
549
710
|
this.store.publishArtifact(stagedPlan);
|
|
711
|
+
if (planRiskAudit)
|
|
712
|
+
appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
|
|
713
|
+
if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
|
|
714
|
+
appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
|
|
715
|
+
}
|
|
550
716
|
return saved;
|
|
551
717
|
}
|
|
552
718
|
rebindTaskKnowledge(repository, taskId, expectedRevision) {
|
|
@@ -630,8 +796,62 @@ export class WorkflowService {
|
|
|
630
796
|
this.store.publishArtifact(stagedPlan);
|
|
631
797
|
return saved;
|
|
632
798
|
}
|
|
799
|
+
refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
|
|
800
|
+
if (!delegationGrantId) {
|
|
801
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
|
|
802
|
+
}
|
|
803
|
+
const { identity } = this.context(repository);
|
|
804
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
805
|
+
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
806
|
+
assertExpectedRevision(task, expectedTaskRevision);
|
|
807
|
+
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
808
|
+
if (!taskCanRebindKnowledge(task)) {
|
|
809
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
|
|
810
|
+
}
|
|
811
|
+
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
812
|
+
if (activeStep) {
|
|
813
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
|
|
814
|
+
}
|
|
815
|
+
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
816
|
+
const transitions = [];
|
|
817
|
+
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
818
|
+
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
819
|
+
// leave the composite operation half-applied.
|
|
820
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
821
|
+
let knowledgeMap = currentMap;
|
|
822
|
+
if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
|
|
823
|
+
if (currentMap.status !== 'active' || !currentMap.approval || inspectedMap.status !== 'stale') {
|
|
824
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
825
|
+
}
|
|
826
|
+
assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
|
|
827
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap);
|
|
828
|
+
knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
|
|
829
|
+
transitions.push('project-memory reconcile');
|
|
830
|
+
knowledgeMap = this.approveKnowledgeMap(repository, knowledgeMap.revision, [], actor, delegationGrantId);
|
|
831
|
+
transitions.push('project-memory approve');
|
|
832
|
+
}
|
|
833
|
+
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
834
|
+
if (task.knowledgeMapRevision === knowledgeMap.revision
|
|
835
|
+
&& task.knowledgeMapHash === knowledgeMapHash) {
|
|
836
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
837
|
+
}
|
|
838
|
+
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
839
|
+
if (currentPlanRiskAudit) {
|
|
840
|
+
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
841
|
+
}
|
|
842
|
+
transitions.push('task knowledge-rebind');
|
|
843
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, 'Delegated content-only context refresh.', delegationGrantId);
|
|
844
|
+
transitions.push('task execution authorize');
|
|
845
|
+
return {
|
|
846
|
+
mode: 'delegated-content-only',
|
|
847
|
+
transitions,
|
|
848
|
+
knowledgeMap,
|
|
849
|
+
task: refreshedTask,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
633
852
|
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
634
853
|
const { identity } = this.context(repository);
|
|
854
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
635
855
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
636
856
|
assertExpectedRevision(task, expectedRevision);
|
|
637
857
|
if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
|
|
@@ -641,6 +861,7 @@ export class WorkflowService {
|
|
|
641
861
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
642
862
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
643
863
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
864
|
+
readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
|
|
644
865
|
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
645
866
|
const event = {
|
|
646
867
|
kind: 'execution',
|
|
@@ -658,15 +879,164 @@ export class WorkflowService {
|
|
|
658
879
|
};
|
|
659
880
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
660
881
|
}
|
|
882
|
+
recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
|
|
883
|
+
const { identity } = this.context(repository);
|
|
884
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
885
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
886
|
+
assertExpectedRevision(task, expectedRevision);
|
|
887
|
+
this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
888
|
+
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
889
|
+
}
|
|
890
|
+
showTaskHandoff(repository, taskId) {
|
|
891
|
+
const { identity } = this.context(repository);
|
|
892
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
893
|
+
return {
|
|
894
|
+
task,
|
|
895
|
+
posture: readTaskC1Posture(this.store, task),
|
|
896
|
+
claimTokenAvailable: false,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
900
|
+
const context = this.context(repository);
|
|
901
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
902
|
+
assertExpectedRevision(task, expectedRevision);
|
|
903
|
+
const normalizedActor = actor.trim();
|
|
904
|
+
if (!normalizedActor) {
|
|
905
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
|
|
906
|
+
}
|
|
907
|
+
if (grantId)
|
|
908
|
+
this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
|
|
909
|
+
const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
|
|
910
|
+
const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
|
|
911
|
+
const milestoneDisplayNumber = milestone
|
|
912
|
+
? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
|
|
913
|
+
: null;
|
|
914
|
+
const taskDisplayNumber = milestone
|
|
915
|
+
? milestone.memberships.findIndex((membership) => membership.taskId === task.id) + 1
|
|
916
|
+
: null;
|
|
917
|
+
const currentLease = context.locks.inspect(taskId);
|
|
918
|
+
let lease;
|
|
919
|
+
let acquiredHere = false;
|
|
920
|
+
if (currentLease) {
|
|
921
|
+
if (currentLease.owner !== normalizedActor) {
|
|
922
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} active writer lease belongs to a different actor.`, {
|
|
923
|
+
taskId,
|
|
924
|
+
leaseOwner: currentLease.owner,
|
|
925
|
+
actor: normalizedActor,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if (writerToken === null) {
|
|
929
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} handoff requires the active writer token while a lease is present.`, {
|
|
930
|
+
taskId,
|
|
931
|
+
leaseOwner: currentLease.owner,
|
|
932
|
+
expiresAt: currentLease.expiresAt,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
if (writerToken !== null) {
|
|
939
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
940
|
+
taskId,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
acquiredHere = true;
|
|
944
|
+
lease = context.locks.acquire(taskId, normalizedActor);
|
|
945
|
+
}
|
|
946
|
+
const releaseLease = () => context.locks.release(taskId, lease.token);
|
|
947
|
+
try {
|
|
948
|
+
const prepared = appendTaskHandoff(this.store, task, {
|
|
949
|
+
actor: normalizedActor,
|
|
950
|
+
targetActor,
|
|
951
|
+
reason,
|
|
952
|
+
writerToken: lease.token,
|
|
953
|
+
milestoneDisplayNumber,
|
|
954
|
+
taskDisplayNumber,
|
|
955
|
+
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
956
|
+
grantId,
|
|
957
|
+
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
958
|
+
}, this.now());
|
|
959
|
+
releaseLease();
|
|
960
|
+
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (acquiredHere)
|
|
964
|
+
releaseLease();
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
|
|
969
|
+
const context = this.context(repository);
|
|
970
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
971
|
+
assertExpectedRevision(task, expectedRevision);
|
|
972
|
+
const normalizedActor = actor.trim();
|
|
973
|
+
if (!normalizedActor) {
|
|
974
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
975
|
+
}
|
|
976
|
+
const pendingHandoff = readTaskC1Posture(this.store, task);
|
|
977
|
+
if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
|
|
978
|
+
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
979
|
+
}
|
|
980
|
+
const existingLease = context.locks.inspect(taskId);
|
|
981
|
+
let lease;
|
|
982
|
+
let acquiredHere = false;
|
|
983
|
+
if (existingLease) {
|
|
984
|
+
if (existingLease.owner !== normalizedActor) {
|
|
985
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} existing writer lease belongs to a different actor.`, {
|
|
986
|
+
taskId,
|
|
987
|
+
leaseOwner: existingLease.owner,
|
|
988
|
+
actor: normalizedActor,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
if (writerToken === null) {
|
|
992
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claim requires the existing writer token.`, {
|
|
993
|
+
taskId,
|
|
994
|
+
leaseOwner: existingLease.owner,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
if (writerToken !== null) {
|
|
1001
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
1002
|
+
taskId,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
acquiredHere = true;
|
|
1006
|
+
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1010
|
+
return { task, event, lease };
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
if (acquiredHere)
|
|
1014
|
+
context.locks.release(taskId, lease.token);
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1019
|
+
const context = this.context(repository);
|
|
1020
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1021
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1022
|
+
const lease = task.status === 'merged' || task.status === 'cancelled'
|
|
1023
|
+
? context.locks.inspect(taskId)
|
|
1024
|
+
: this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
1025
|
+
const event = appendTaskHandback(this.store, task, actor, reason, lease?.token ?? writerToken, limitations, recommendedNext, this.now());
|
|
1026
|
+
if (lease)
|
|
1027
|
+
context.locks.release(taskId, lease.token);
|
|
1028
|
+
return { task, event };
|
|
1029
|
+
}
|
|
661
1030
|
startTask(repository, taskId, expectedRevision, workspaceOwner) {
|
|
662
1031
|
const { identity } = this.context(repository);
|
|
663
1032
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
664
1033
|
assertExpectedRevision(task, expectedRevision);
|
|
1034
|
+
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
665
1035
|
if (task.status !== 'ready')
|
|
666
1036
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
|
|
667
1037
|
this.assertExecutionAuthorizationFresh(task);
|
|
668
1038
|
if (task.milestoneId) {
|
|
669
|
-
const milestone = this.
|
|
1039
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
|
|
670
1040
|
const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
|
|
671
1041
|
if (milestone.status !== 'active' || membership?.disposition !== 'required') {
|
|
672
1042
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
|
|
@@ -675,6 +1045,15 @@ export class WorkflowService {
|
|
|
675
1045
|
disposition: membership?.disposition ?? null,
|
|
676
1046
|
});
|
|
677
1047
|
}
|
|
1048
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1049
|
+
if (handoffPosture.state !== 'claimed') {
|
|
1050
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
|
|
1051
|
+
taskId: task.id,
|
|
1052
|
+
milestoneId: milestone.id,
|
|
1053
|
+
c1Posture: handoffPosture.state,
|
|
1054
|
+
requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
678
1057
|
}
|
|
679
1058
|
if (!isClean(identity.repositoryRoot)) {
|
|
680
1059
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
@@ -700,20 +1079,48 @@ export class WorkflowService {
|
|
|
700
1079
|
}
|
|
701
1080
|
return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
|
|
702
1081
|
}
|
|
703
|
-
runStep(repository, taskId, stepId, expectedRevision,
|
|
1082
|
+
runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
704
1083
|
const context = this.context(repository);
|
|
705
1084
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
706
1085
|
assertExpectedRevision(task, expectedRevision);
|
|
1086
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
707
1087
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
708
1088
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
709
1089
|
}
|
|
710
1090
|
this.assertExecutionAuthorizationFresh(task);
|
|
711
1091
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1092
|
+
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1093
|
+
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1094
|
+
if (currentPlanBlockingDecisions.length > 0) {
|
|
1095
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run any Step while the current Plan has active blocking corrective decisions.`, {
|
|
1096
|
+
taskId,
|
|
1097
|
+
decisions: currentPlanBlockingDecisions.map((event) => ({
|
|
1098
|
+
eventId: event.eventId,
|
|
1099
|
+
stepId: event.stepId,
|
|
1100
|
+
decision: event.decision,
|
|
1101
|
+
planHash: event.planHash,
|
|
1102
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
1103
|
+
})),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
712
1106
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
713
1107
|
const step = requireStep(task, stepId);
|
|
1108
|
+
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
714
1109
|
if (!['planned', 'failed'].includes(step.status)) {
|
|
715
1110
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
716
1111
|
}
|
|
1112
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1113
|
+
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1114
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} has remediation history but is no longer marked for guarded review by the current Plan Risk Audit; execution is blocked until the audit and Step posture are reconciled.`, {
|
|
1115
|
+
taskId,
|
|
1116
|
+
stepId,
|
|
1117
|
+
remediationAttemptCount: remediationHistoryForStep.length,
|
|
1118
|
+
currentAuditPresent: Boolean(planRiskAudit),
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1122
|
+
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
|
|
1123
|
+
}
|
|
717
1124
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
718
1125
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
719
1126
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -732,18 +1139,41 @@ export class WorkflowService {
|
|
|
732
1139
|
incompleteDependencies,
|
|
733
1140
|
});
|
|
734
1141
|
}
|
|
1142
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1143
|
+
if (handoffPosture.state === 'pending') {
|
|
1144
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, null);
|
|
1145
|
+
}
|
|
735
1146
|
const existing = context.locks.inspect(taskId);
|
|
736
1147
|
let acquiredHere = false;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1148
|
+
let lease;
|
|
1149
|
+
if (handoffPosture.state === 'claimed') {
|
|
1150
|
+
if (!existing) {
|
|
1151
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires an active writer lease before task run.`, {
|
|
1152
|
+
taskId,
|
|
1153
|
+
claimant: handoffPosture.claimant,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
if (!writerToken) {
|
|
1157
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires the bound writer token for task run.`, {
|
|
1158
|
+
taskId,
|
|
1159
|
+
claimant: handoffPosture.claimant,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
1163
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, lease.token);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
lease = existing
|
|
1167
|
+
? writerToken
|
|
1168
|
+
? context.locks.heartbeat(taskId, writerToken)
|
|
1169
|
+
: (() => {
|
|
1170
|
+
throw new WorkflowError('LOCKED', `${taskId} already has a writer.`, { owner: existing.owner });
|
|
1171
|
+
})()
|
|
740
1172
|
: (() => {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
return context.locks.acquire(taskId, owner.trim() || 'worker');
|
|
746
|
-
})();
|
|
1173
|
+
acquiredHere = true;
|
|
1174
|
+
return context.locks.acquire(taskId, actor.trim() || 'worker');
|
|
1175
|
+
})();
|
|
1176
|
+
}
|
|
747
1177
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
|
|
748
1178
|
let saved;
|
|
749
1179
|
try {
|
|
@@ -761,7 +1191,7 @@ export class WorkflowService {
|
|
|
761
1191
|
protocolVersion: PROTOCOL_VERSION,
|
|
762
1192
|
packageVersion: PACKAGE_VERSION,
|
|
763
1193
|
role: 'worker',
|
|
764
|
-
objective: step.objective
|
|
1194
|
+
objective: `${step.objective} Leave all Step changes uncommitted; Workflow core creates the atomic commit through task step-complete.`,
|
|
765
1195
|
inputs: [
|
|
766
1196
|
path.join(this.store.taskRoot(task.projectId, task.id), 'brief.md'),
|
|
767
1197
|
path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
|
|
@@ -769,7 +1199,10 @@ export class WorkflowService {
|
|
|
769
1199
|
allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
|
|
770
1200
|
allowedWrites: step.allowedWrites,
|
|
771
1201
|
commands: step.checks,
|
|
772
|
-
forbiddenScope:
|
|
1202
|
+
forbiddenScope: [
|
|
1203
|
+
...step.forbiddenScope,
|
|
1204
|
+
'Manual Git staging, commits, amendments, resets, rebases, or other history mutations. Workflow core owns the Step commit.',
|
|
1205
|
+
],
|
|
773
1206
|
resultSchema: {
|
|
774
1207
|
type: 'object',
|
|
775
1208
|
required: ['summary', 'changedFiles', 'checks'],
|
|
@@ -783,6 +1216,7 @@ export class WorkflowService {
|
|
|
783
1216
|
'Required change falls outside allowedWrites.',
|
|
784
1217
|
'Implementation requires changing Brief, Plan, or acceptance.',
|
|
785
1218
|
'A configured check cannot be run or interpreted safely.',
|
|
1219
|
+
'Do not run git add or git commit. Return uncommitted changes to the coordinator, which must invoke task step-complete.',
|
|
786
1220
|
],
|
|
787
1221
|
contextBudget: 'medium',
|
|
788
1222
|
taskId,
|
|
@@ -794,13 +1228,26 @@ export class WorkflowService {
|
|
|
794
1228
|
},
|
|
795
1229
|
};
|
|
796
1230
|
}
|
|
797
|
-
taskContext(repository, taskId, role) {
|
|
1231
|
+
taskContext(repository, taskId, role, allowHistoricalStrictReview = false) {
|
|
798
1232
|
const { identity } = this.context(repository);
|
|
799
1233
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1234
|
+
const historicalStrictReview = allowHistoricalStrictReview
|
|
1235
|
+
&& role === 'independent-reviewer'
|
|
1236
|
+
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
800
1237
|
const knowledgeMap = task.knowledgeMapRevision === null
|
|
801
1238
|
? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
|
|
802
|
-
:
|
|
1239
|
+
: historicalStrictReview
|
|
1240
|
+
? this.strictReviewKnowledgeMap(identity.repositoryRoot, task)
|
|
1241
|
+
: this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
803
1242
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1243
|
+
const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
|
|
1244
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
1245
|
+
: null;
|
|
1246
|
+
const planRiskReviewScope = planRiskAudit
|
|
1247
|
+
? planRiskAudit.reviewRequiredStepIds.length > 0
|
|
1248
|
+
? ` Current Plan Risk Audit marks ${planRiskAudit.reviewRequiredStepIds.join(', ')} for strict review.`
|
|
1249
|
+
: ' Current Plan Risk Audit marks no Steps for strict review.'
|
|
1250
|
+
: '';
|
|
804
1251
|
const common = {
|
|
805
1252
|
protocolVersion: PROTOCOL_VERSION,
|
|
806
1253
|
packageVersion: PACKAGE_VERSION,
|
|
@@ -822,17 +1269,25 @@ export class WorkflowService {
|
|
|
822
1269
|
if (role === 'technical-planner') {
|
|
823
1270
|
return {
|
|
824
1271
|
...common,
|
|
825
|
-
objective: `Produce an execution-ready Plan for ${task.title}
|
|
1272
|
+
objective: `Produce an execution-ready Plan for ${task.title}. Perform an explicit risk review before plan-set: migrations, concurrency, restart/replay, crash windows, and external-provider behavior require failure-specific checks in the original Plan.${planRiskReviewScope}`,
|
|
826
1273
|
resultSchema: {
|
|
827
1274
|
type: 'object',
|
|
828
1275
|
required: ['objective', 'requirements', 'acceptance', 'risks', 'steps'],
|
|
829
1276
|
},
|
|
1277
|
+
stopConditions: [
|
|
1278
|
+
...common.stopConditions,
|
|
1279
|
+
'A migration is planned without a populated forward-upgrade check.',
|
|
1280
|
+
'Concurrency or cursor/state ownership is planned without deterministic race checks.',
|
|
1281
|
+
'Restart, replay, or crash recovery is claimed without restart/crash-window evidence.',
|
|
1282
|
+
'An external provider is used without failure-injection and hidden-default verification.',
|
|
1283
|
+
'The safe risk slice crosses persistence, recovery orchestration, and executable composition and should be split.',
|
|
1284
|
+
],
|
|
830
1285
|
};
|
|
831
1286
|
}
|
|
832
1287
|
if (role === 'independent-reviewer') {
|
|
833
1288
|
return {
|
|
834
1289
|
...common,
|
|
835
|
-
objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files
|
|
1290
|
+
objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.${planRiskReviewScope}`,
|
|
836
1291
|
inputs: [...common.inputs, ...(task.evidenceHash ? [path.join(root, 'evidence.json')] : [])],
|
|
837
1292
|
resultSchema: {
|
|
838
1293
|
type: 'object',
|
|
@@ -842,28 +1297,65 @@ export class WorkflowService {
|
|
|
842
1297
|
...common.stopConditions,
|
|
843
1298
|
'The checkout is dirty or the evidence does not identify the reviewed commit.',
|
|
844
1299
|
'Read-only reviewer isolation cannot be established; return unverified.',
|
|
1300
|
+
'High-risk behavior lacks populated-upgrade, race, restart/replay, crash-window, or provider-failure evidence required by the Plan.',
|
|
845
1301
|
],
|
|
846
1302
|
};
|
|
847
1303
|
}
|
|
848
1304
|
return {
|
|
849
1305
|
...common,
|
|
850
|
-
objective: `Coordinate the next valid state transition for ${task.title}
|
|
1306
|
+
objective: `Coordinate the next valid state transition for ${task.title}.${planRiskReviewScope}`,
|
|
851
1307
|
resultSchema: { type: 'object', required: ['recommendedAction', 'reason'] },
|
|
852
1308
|
};
|
|
853
1309
|
}
|
|
854
|
-
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes) {
|
|
1310
|
+
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
|
|
855
1311
|
const context = this.context(repository);
|
|
856
|
-
context.locks.heartbeat(taskId, writerToken);
|
|
1312
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1313
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
857
1314
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
858
1315
|
assertExpectedRevision(task, expectedRevision);
|
|
1316
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
859
1317
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
860
1318
|
const step = requireStep(task, stepId);
|
|
861
1319
|
if (task.status !== 'in_progress' || step.status !== 'in_progress') {
|
|
862
1320
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
|
|
863
1321
|
}
|
|
1322
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1323
|
+
const reviewRequired = planRiskAudit
|
|
1324
|
+
? isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
|
|
1325
|
+
: false;
|
|
1326
|
+
if (reviewRequired && step.evidence) {
|
|
1327
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1328
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
1329
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires a clean Task checkout.', {
|
|
1330
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1334
|
+
if (currentHead !== step.evidence.commitSha) {
|
|
1335
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires HEAD to remain at the recorded completion commit.', {
|
|
1336
|
+
stepId,
|
|
1337
|
+
expectedCommit: step.evidence.commitSha,
|
|
1338
|
+
actualCommit: currentHead,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1342
|
+
if (!currentCycle.pendingEvent && !currentCycle.latestMatchingEvent) {
|
|
1343
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1344
|
+
}
|
|
1345
|
+
return task;
|
|
1346
|
+
}
|
|
1347
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
864
1348
|
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
865
1349
|
const failed = checks.filter((check) => !check.passed);
|
|
866
1350
|
if (failed.length > 0) {
|
|
1351
|
+
if (reviewRequired && planRiskAudit) {
|
|
1352
|
+
appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
1353
|
+
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
1354
|
+
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
1355
|
+
reviewEventHash: null,
|
|
1356
|
+
reviewerAttestationHash: null,
|
|
1357
|
+
}, planRiskAudit, this.now());
|
|
1358
|
+
}
|
|
867
1359
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
|
|
868
1360
|
return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
|
|
869
1361
|
}
|
|
@@ -875,6 +1367,18 @@ export class WorkflowService {
|
|
|
875
1367
|
checks,
|
|
876
1368
|
notes,
|
|
877
1369
|
};
|
|
1370
|
+
if (reviewRequired) {
|
|
1371
|
+
const saved = this.store.writeTask({
|
|
1372
|
+
...task,
|
|
1373
|
+
status: 'in_progress',
|
|
1374
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
1375
|
+
? { ...candidate, status: 'in_progress', evidence }
|
|
1376
|
+
: candidate),
|
|
1377
|
+
blockReason: null,
|
|
1378
|
+
}, expectedRevision);
|
|
1379
|
+
ensurePendingStrictStepReview(this.store, saved, stepId, this.now());
|
|
1380
|
+
return saved;
|
|
1381
|
+
}
|
|
878
1382
|
const completedIds = new Set([
|
|
879
1383
|
...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
|
|
880
1384
|
stepId,
|
|
@@ -889,11 +1393,266 @@ export class WorkflowService {
|
|
|
889
1393
|
});
|
|
890
1394
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
891
1395
|
}
|
|
892
|
-
|
|
1396
|
+
alpha6C1StrictReviewRescuePreflight(repository) {
|
|
1397
|
+
const context = this.context(repository);
|
|
1398
|
+
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
1399
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1400
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1401
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1402
|
+
const now = this.now().getTime();
|
|
1403
|
+
const candidates = [];
|
|
1404
|
+
const malformedCandidates = [];
|
|
1405
|
+
for (const task of tasks) {
|
|
1406
|
+
if (task.status !== 'in_progress')
|
|
1407
|
+
continue;
|
|
1408
|
+
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
1409
|
+
if (!planRiskAudit)
|
|
1410
|
+
continue;
|
|
1411
|
+
for (const step of task.steps) {
|
|
1412
|
+
if (step.status !== 'in_progress' || !step.evidence)
|
|
1413
|
+
continue;
|
|
1414
|
+
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
1415
|
+
continue;
|
|
1416
|
+
try {
|
|
1417
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
1418
|
+
if (cycle.resolution !== 'pending')
|
|
1419
|
+
continue;
|
|
1420
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1421
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
1422
|
+
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
1423
|
+
const knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
1424
|
+
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
1425
|
+
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
1426
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1427
|
+
const lease = context.locks.inspect(task.id);
|
|
1428
|
+
const claimant = posture.state === 'claimed' ? posture.claimant : null;
|
|
1429
|
+
const tokenBound = posture.state === 'claimed'
|
|
1430
|
+
&& lease !== null
|
|
1431
|
+
&& posture.writerLeaseTokenHash !== null
|
|
1432
|
+
&& posture.writerLeaseTokenHash === sha256Hex(lease.token);
|
|
1433
|
+
candidates.push({
|
|
1434
|
+
taskId: task.id,
|
|
1435
|
+
taskRevision: task.revision,
|
|
1436
|
+
stepId: step.id,
|
|
1437
|
+
completionCommit: step.evidence.commitSha,
|
|
1438
|
+
currentHead,
|
|
1439
|
+
knowledgeBindingStale,
|
|
1440
|
+
claimant,
|
|
1441
|
+
handoffExpiresAt: posture.state === 'claimed' ? posture.latestEvent.expiresAt : null,
|
|
1442
|
+
lease: lease
|
|
1443
|
+
? {
|
|
1444
|
+
entityId: lease.entityId,
|
|
1445
|
+
owner: lease.owner,
|
|
1446
|
+
pid: lease.pid,
|
|
1447
|
+
hostname: lease.hostname,
|
|
1448
|
+
acquiredAt: lease.acquiredAt,
|
|
1449
|
+
heartbeatAt: lease.heartbeatAt,
|
|
1450
|
+
expiresAt: lease.expiresAt,
|
|
1451
|
+
stale: Date.parse(lease.expiresAt) <= now,
|
|
1452
|
+
tokenBound,
|
|
1453
|
+
}
|
|
1454
|
+
: null,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const candidate = candidates.length === 1 ? candidates[0] : null;
|
|
1463
|
+
const unrelatedLeases = context.locks.list().filter((lease) => lease.entityId !== candidate?.taskId);
|
|
1464
|
+
const blockers = [
|
|
1465
|
+
...(dependency.declared !== '2.0.0-alpha.6'
|
|
1466
|
+
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
1467
|
+
: []),
|
|
1468
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1469
|
+
...(candidates.length !== 1
|
|
1470
|
+
? [`Expected exactly one pending alpha.6 strict review; found ${candidates.length}.`]
|
|
1471
|
+
: []),
|
|
1472
|
+
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
1473
|
+
? ['HEAD does not match the pending strict review completion commit.']
|
|
1474
|
+
: []),
|
|
1475
|
+
...(candidate && !candidate.knowledgeBindingStale
|
|
1476
|
+
? ['The pending strict review is not blocked by a stale Project Knowledge Map.']
|
|
1477
|
+
: []),
|
|
1478
|
+
...(candidate && candidate.claimant === null
|
|
1479
|
+
? ['The rescue Task does not have an active claimed C1 posture.']
|
|
1480
|
+
: []),
|
|
1481
|
+
...(candidate && candidate.lease === null
|
|
1482
|
+
? ['The claimed C1 rescue Task has no bound writer lease.']
|
|
1483
|
+
: []),
|
|
1484
|
+
...(candidate?.lease && candidate.claimant !== candidate.lease.owner
|
|
1485
|
+
? ['The writer lease owner does not match the active C1 claimant.']
|
|
1486
|
+
: []),
|
|
1487
|
+
...(candidate?.lease && !candidate.lease.tokenBound
|
|
1488
|
+
? ['The writer lease token does not match the active C1 binding.']
|
|
1489
|
+
: []),
|
|
1490
|
+
...(unrelatedLeases.length > 0
|
|
1491
|
+
? ['A writer lease exists outside the exact rescue Task.']
|
|
1492
|
+
: []),
|
|
1493
|
+
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
1494
|
+
];
|
|
1495
|
+
const eligible = blockers.length === 0 && candidate !== null && candidate.claimant !== null;
|
|
1496
|
+
return {
|
|
1497
|
+
eligible,
|
|
1498
|
+
mutationFree: true,
|
|
1499
|
+
runnerPackageVersion: PACKAGE_VERSION,
|
|
1500
|
+
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
1501
|
+
declaredProjectPackageVersion: dependency.declared,
|
|
1502
|
+
projectId: context.identity.projectId,
|
|
1503
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
1504
|
+
clean: dirty.length === 0,
|
|
1505
|
+
candidate,
|
|
1506
|
+
blockers,
|
|
1507
|
+
requiredAction: eligible && candidate && candidate.claimant
|
|
1508
|
+
? {
|
|
1509
|
+
action: 'update rescue-review',
|
|
1510
|
+
taskId: candidate.taskId,
|
|
1511
|
+
stepId: candidate.stepId,
|
|
1512
|
+
expectedRevision: candidate.taskRevision,
|
|
1513
|
+
actor: candidate.claimant,
|
|
1514
|
+
requiresBoundWriterToken: true,
|
|
1515
|
+
}
|
|
1516
|
+
: null,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
alpha6C1StrictReviewRescue(repository, taskId, stepId, expectedRevision, actor, writerToken, runner) {
|
|
1520
|
+
const preflight = this.alpha6C1StrictReviewRescuePreflight(repository);
|
|
1521
|
+
if (!preflight.eligible || !preflight.candidate || !preflight.requiredAction) {
|
|
1522
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 C1 strict-review rescue preflight is not eligible.', {
|
|
1523
|
+
blockers: preflight.blockers,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
const candidate = preflight.candidate;
|
|
1527
|
+
if (candidate.taskId !== taskId
|
|
1528
|
+
|| candidate.stepId !== stepId
|
|
1529
|
+
|| candidate.taskRevision !== expectedRevision) {
|
|
1530
|
+
throw new WorkflowError('STATE_CONFLICT', 'Rescue command does not match the exact preflight candidate.', {
|
|
1531
|
+
expected: preflight.requiredAction,
|
|
1532
|
+
actual: { taskId, stepId, expectedRevision, actor },
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
if (candidate.claimant !== actor.trim()) {
|
|
1536
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Rescue actor does not match the active C1 claimant.', {
|
|
1537
|
+
expectedActor: candidate.claimant,
|
|
1538
|
+
actualActor: actor.trim(),
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const lease = this.context(repository).locks.inspect(taskId);
|
|
1542
|
+
if (!lease || lease.token !== writerToken) {
|
|
1543
|
+
throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer token does not own ${taskId}`);
|
|
1544
|
+
}
|
|
1545
|
+
return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, true);
|
|
1546
|
+
}
|
|
1547
|
+
reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
|
|
1548
|
+
return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, false);
|
|
1549
|
+
}
|
|
1550
|
+
reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, allowStaleKnowledgeBinding) {
|
|
893
1551
|
const context = this.context(repository);
|
|
894
|
-
context.
|
|
1552
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
895
1553
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
896
1554
|
assertExpectedRevision(task, expectedRevision);
|
|
1555
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
1556
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1557
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
1558
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
1559
|
+
}
|
|
1560
|
+
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
1561
|
+
if (!allowStaleKnowledgeBinding)
|
|
1562
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1563
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1564
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1565
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
|
|
1566
|
+
taskId: task.id,
|
|
1567
|
+
stepId,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
const step = requireStep(task, stepId);
|
|
1571
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
1572
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires ${stepId} to remain in progress with canonical evidence.`, {
|
|
1573
|
+
taskId: task.id,
|
|
1574
|
+
stepId,
|
|
1575
|
+
stepStatus: step.status,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1579
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
1580
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires a clean Task checkout.', {
|
|
1581
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1585
|
+
if (currentHead !== step.evidence.commitSha) {
|
|
1586
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires HEAD to remain at the recorded completion commit.', {
|
|
1587
|
+
taskId: task.id,
|
|
1588
|
+
stepId,
|
|
1589
|
+
expectedCommit: step.evidence.commitSha,
|
|
1590
|
+
actualCommit: currentHead,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1594
|
+
if (task.status === 'blocked') {
|
|
1595
|
+
if (currentCycle.resolution !== 'unverified') {
|
|
1596
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review retry requires ${stepId} to be blocked by an unverified prior cycle.`, {
|
|
1597
|
+
taskId: task.id,
|
|
1598
|
+
stepId,
|
|
1599
|
+
resolution: currentCycle.resolution,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
|
|
1603
|
+
}
|
|
1604
|
+
else if (currentCycle.resolution !== 'pending') {
|
|
1605
|
+
if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
|
|
1606
|
+
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1607
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1608
|
+
taskId: task.id,
|
|
1609
|
+
stepId,
|
|
1610
|
+
resolution: currentCycle.resolution,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
|
|
1614
|
+
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
1615
|
+
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
1616
|
+
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
1617
|
+
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
1618
|
+
}, planRiskAudit, this.now());
|
|
1619
|
+
}
|
|
1620
|
+
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
1621
|
+
}
|
|
1622
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1623
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer', allowStaleKnowledgeBinding);
|
|
1624
|
+
const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
|
|
1625
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
1626
|
+
review: launch.review,
|
|
1627
|
+
reviewerOutputHash: launch.reviewerOutputHash,
|
|
1628
|
+
reviewedCommit: launch.reviewedCommit,
|
|
1629
|
+
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
1630
|
+
}, this.now());
|
|
1631
|
+
if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
|
|
1632
|
+
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
1633
|
+
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
1634
|
+
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
1635
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review remediation recording requires review and attestation sidecars.', {
|
|
1636
|
+
taskId: task.id,
|
|
1637
|
+
stepId,
|
|
1638
|
+
resolution: recorded.resolution,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
|
|
1642
|
+
completionCommit: reviewRecordedEvent.completionCommit,
|
|
1643
|
+
checksHash: reviewRecordedEvent.checksHash,
|
|
1644
|
+
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
1645
|
+
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
1646
|
+
}, planRiskAudit, this.now());
|
|
1647
|
+
}
|
|
1648
|
+
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
1649
|
+
}
|
|
1650
|
+
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1651
|
+
const context = this.context(repository);
|
|
1652
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1653
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1654
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1655
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
897
1656
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
898
1657
|
if (!['in_progress', 'ready'].includes(task.status)) {
|
|
899
1658
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot be submitted while ${task.status}.`);
|
|
@@ -920,10 +1679,13 @@ export class WorkflowService {
|
|
|
920
1679
|
this.store.publishArtifact(stagedEvidence);
|
|
921
1680
|
return saved;
|
|
922
1681
|
}
|
|
923
|
-
recordReview(repository, taskId, expectedRevision, review) {
|
|
1682
|
+
recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
|
|
924
1683
|
const { identity } = this.context(repository);
|
|
1684
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
925
1685
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
926
1686
|
assertExpectedRevision(task, expectedRevision);
|
|
1687
|
+
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1688
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
927
1689
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
928
1690
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
929
1691
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for review.`);
|
|
@@ -947,7 +1709,11 @@ export class WorkflowService {
|
|
|
947
1709
|
if (recorded.findings.length > 0) {
|
|
948
1710
|
const file = path.join(this.store.taskRoot(task.projectId, task.id), 'findings.jsonl');
|
|
949
1711
|
for (const finding of recorded.findings) {
|
|
950
|
-
appendFileSync(file, `${JSON.stringify({
|
|
1712
|
+
appendFileSync(file, `${JSON.stringify({
|
|
1713
|
+
...finding,
|
|
1714
|
+
reviewer: recorded.reviewer,
|
|
1715
|
+
recordedAt: recorded.recordedAt,
|
|
1716
|
+
})}\n`, {
|
|
951
1717
|
encoding: 'utf8',
|
|
952
1718
|
mode: 0o600,
|
|
953
1719
|
});
|
|
@@ -955,6 +1721,59 @@ export class WorkflowService {
|
|
|
955
1721
|
}
|
|
956
1722
|
return saved;
|
|
957
1723
|
}
|
|
1724
|
+
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
1725
|
+
const { identity } = this.context(repository);
|
|
1726
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1727
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
1728
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1729
|
+
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1730
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1731
|
+
if (task.status !== 'needs_fix' && task.status !== 'blocked') {
|
|
1732
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
1733
|
+
}
|
|
1734
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1735
|
+
if (!planRiskAudit) {
|
|
1736
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
|
|
1737
|
+
taskId,
|
|
1738
|
+
stepId,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
if (!task.planHash) {
|
|
1742
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no current Plan hash for corrective decision recording.`, {
|
|
1743
|
+
taskId,
|
|
1744
|
+
stepId,
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
const step = requireStep(task, stepId);
|
|
1748
|
+
if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1749
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
|
|
1750
|
+
taskId,
|
|
1751
|
+
stepId,
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
if (task.status === 'needs_fix') {
|
|
1755
|
+
if (step.status !== 'failed') {
|
|
1756
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
|
|
1757
|
+
taskId,
|
|
1758
|
+
stepId,
|
|
1759
|
+
stepStatus: step.status,
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1765
|
+
if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
|
|
1766
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
|
|
1767
|
+
taskId,
|
|
1768
|
+
stepId,
|
|
1769
|
+
stepStatus: step.status,
|
|
1770
|
+
resolution: currentCycle.resolution,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
1775
|
+
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
1776
|
+
}
|
|
958
1777
|
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
959
1778
|
const { identity } = this.context(repository);
|
|
960
1779
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
@@ -1059,10 +1878,12 @@ export class WorkflowService {
|
|
|
1059
1878
|
this.store.publishArtifact(stagedEvidence);
|
|
1060
1879
|
return saved;
|
|
1061
1880
|
}
|
|
1062
|
-
mergeTask(repository, taskId, expectedRevision, writerToken) {
|
|
1881
|
+
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1063
1882
|
const context = this.context(repository);
|
|
1064
1883
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1065
1884
|
assertExpectedRevision(task, expectedRevision);
|
|
1885
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1886
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
1066
1887
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
|
|
1067
1888
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for a local merge.`);
|
|
1068
1889
|
}
|
|
@@ -1079,10 +1900,12 @@ export class WorkflowService {
|
|
|
1079
1900
|
context.locks.release(taskId, writerToken);
|
|
1080
1901
|
return saved;
|
|
1081
1902
|
}
|
|
1082
|
-
confirmExternalMerge(repository, taskId, expectedRevision, writerToken) {
|
|
1903
|
+
confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1083
1904
|
const context = this.context(repository);
|
|
1084
1905
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1085
1906
|
assertExpectedRevision(task, expectedRevision);
|
|
1907
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1908
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
1086
1909
|
if (task.status !== 'ready_to_merge' || task.workspaceOwner !== 'external') {
|
|
1087
1910
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting external merge confirmation.`);
|
|
1088
1911
|
}
|
|
@@ -1096,12 +1919,91 @@ export class WorkflowService {
|
|
|
1096
1919
|
}
|
|
1097
1920
|
status(repository) {
|
|
1098
1921
|
const { identity } = this.context(repository);
|
|
1922
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
1923
|
+
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
1924
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
|
|
1099
1925
|
return {
|
|
1100
1926
|
projectId: identity.projectId,
|
|
1101
1927
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
1102
|
-
tasks
|
|
1103
|
-
milestones: this.
|
|
1928
|
+
tasks,
|
|
1929
|
+
milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
|
|
1104
1930
|
delegations: this.store.listDelegations(identity.projectId),
|
|
1931
|
+
adoption,
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
recoverMilestoneTransactions(repository) {
|
|
1935
|
+
const { identity } = this.context(repository);
|
|
1936
|
+
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
1937
|
+
return {
|
|
1938
|
+
projectId: identity.projectId,
|
|
1939
|
+
recoveredMilestoneIds: milestones.map((milestone) => milestone.id),
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
prepareAdoption(repository) {
|
|
1943
|
+
const { identity } = this.context(repository);
|
|
1944
|
+
return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
|
|
1945
|
+
}
|
|
1946
|
+
applyAdoption(repository, actor, confirmationCode) {
|
|
1947
|
+
const context = this.context(repository);
|
|
1948
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1949
|
+
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
1950
|
+
return applyAdoptionPosture(this.store, {
|
|
1951
|
+
projectId: context.identity.projectId,
|
|
1952
|
+
repositoryIdentity: context.identity,
|
|
1953
|
+
packageVersion: PACKAGE_VERSION,
|
|
1954
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1955
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1956
|
+
tasks,
|
|
1957
|
+
milestones,
|
|
1958
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
1959
|
+
blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
|
|
1960
|
+
now: this.now(),
|
|
1961
|
+
}, actor, confirmationCode);
|
|
1962
|
+
}
|
|
1963
|
+
updatePreflight(repository) {
|
|
1964
|
+
const context = this.context(repository);
|
|
1965
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1966
|
+
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
1967
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1968
|
+
const runningSteps = tasks.flatMap((task) => task.steps
|
|
1969
|
+
.filter((step) => step.status === 'in_progress')
|
|
1970
|
+
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
1971
|
+
const leases = context.locks.list();
|
|
1972
|
+
const now = this.now().getTime();
|
|
1973
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1974
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
1975
|
+
const blockers = [
|
|
1976
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1977
|
+
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1978
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1979
|
+
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
1980
|
+
];
|
|
1981
|
+
const adoption = buildAdoptionPreparation({
|
|
1982
|
+
projectId: context.identity.projectId,
|
|
1983
|
+
repositoryIdentity: context.identity,
|
|
1984
|
+
packageVersion: PACKAGE_VERSION,
|
|
1985
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1986
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1987
|
+
tasks,
|
|
1988
|
+
milestones,
|
|
1989
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
1990
|
+
blockers,
|
|
1991
|
+
now: this.now(),
|
|
1992
|
+
}, readCurrentAdoptionPosture(this.store, context.identity.projectId));
|
|
1993
|
+
return {
|
|
1994
|
+
safe: blockers.length === 0,
|
|
1995
|
+
packageVersion: PACKAGE_VERSION,
|
|
1996
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1997
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1998
|
+
projectId: context.identity.projectId,
|
|
1999
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
2000
|
+
clean: dirty.length === 0,
|
|
2001
|
+
changedFiles: dirty,
|
|
2002
|
+
runningSteps,
|
|
2003
|
+
activeLeases,
|
|
2004
|
+
staleLeases,
|
|
2005
|
+
blockers,
|
|
2006
|
+
adoption,
|
|
1105
2007
|
};
|
|
1106
2008
|
}
|
|
1107
2009
|
scanKnowledgeMap(repository) {
|
|
@@ -1177,12 +2079,35 @@ export class WorkflowService {
|
|
|
1177
2079
|
}
|
|
1178
2080
|
next(repository) {
|
|
1179
2081
|
const status = this.status(repository);
|
|
2082
|
+
const adoptionInfo = {
|
|
2083
|
+
status: status.adoption.status,
|
|
2084
|
+
safe: status.adoption.safe,
|
|
2085
|
+
blockers: status.adoption.blockers,
|
|
2086
|
+
baselineDigestHash: status.adoption.baselineDigestHash,
|
|
2087
|
+
sidecarBaselineHash: status.adoption.sidecarBaselineHash,
|
|
2088
|
+
confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
|
|
2089
|
+
confirmationCode: status.adoption.confirmationCode,
|
|
2090
|
+
currentEventType: status.adoption.currentPosture?.eventType ?? null,
|
|
2091
|
+
};
|
|
2092
|
+
if (status.adoption.status !== 'applied') {
|
|
2093
|
+
return {
|
|
2094
|
+
scope: 'project',
|
|
2095
|
+
projectId: status.projectId,
|
|
2096
|
+
status: status.adoption.status,
|
|
2097
|
+
action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2098
|
+
adoption: adoptionInfo,
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
|
|
1180
2102
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
1181
2103
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
1182
2104
|
const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
1183
2105
|
if (activeTask) {
|
|
1184
2106
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
1185
|
-
|
|
2107
|
+
const planRiskAudit = activeTask.planHash
|
|
2108
|
+
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2109
|
+
: null;
|
|
2110
|
+
return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
|
|
1186
2111
|
}
|
|
1187
2112
|
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
1188
2113
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
@@ -1190,21 +2115,37 @@ export class WorkflowService {
|
|
|
1190
2115
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
1191
2116
|
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
|
|
1192
2117
|
: [];
|
|
1193
|
-
|
|
2118
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
|
|
2119
|
+
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
2120
|
+
&& storedKnowledgeMap.status === 'active'
|
|
2121
|
+
&& storedKnowledgeMap.approval
|
|
2122
|
+
&& isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
|
|
2123
|
+
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
|
|
2124
|
+
: [];
|
|
2125
|
+
return withAdoption({
|
|
1194
2126
|
scope: 'project-memory',
|
|
1195
2127
|
status: knowledgeMap.status,
|
|
1196
2128
|
action,
|
|
1197
2129
|
revision: knowledgeMap.revision,
|
|
1198
2130
|
taskId: activeTask.id,
|
|
1199
2131
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1200
|
-
|
|
2132
|
+
...(contextRefreshOptions.length > 0
|
|
2133
|
+
? {
|
|
2134
|
+
contextRefresh: {
|
|
2135
|
+
action: 'task context-refresh',
|
|
2136
|
+
mode: 'delegated-content-only',
|
|
2137
|
+
options: contextRefreshOptions,
|
|
2138
|
+
},
|
|
2139
|
+
}
|
|
2140
|
+
: {}),
|
|
2141
|
+
});
|
|
1201
2142
|
}
|
|
1202
2143
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
1203
2144
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
1204
2145
|
activeTask.planHash &&
|
|
1205
2146
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
1206
2147
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
1207
|
-
return {
|
|
2148
|
+
return withAdoption({
|
|
1208
2149
|
scope: 'task',
|
|
1209
2150
|
id: activeTask.id,
|
|
1210
2151
|
status: activeTask.status,
|
|
@@ -1212,9 +2153,12 @@ export class WorkflowService {
|
|
|
1212
2153
|
revision: activeTask.revision,
|
|
1213
2154
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
1214
2155
|
knowledgeMapHash,
|
|
1215
|
-
};
|
|
2156
|
+
});
|
|
1216
2157
|
}
|
|
1217
|
-
|
|
2158
|
+
const planRiskAudit = activeTask.planHash && activeTask.status !== 'planning'
|
|
2159
|
+
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2160
|
+
: null;
|
|
2161
|
+
return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
|
|
1218
2162
|
}
|
|
1219
2163
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
1220
2164
|
if (activeMilestone) {
|
|
@@ -1232,17 +2176,17 @@ export class WorkflowService {
|
|
|
1232
2176
|
evidence.headCommit !== currentHead ||
|
|
1233
2177
|
resultHash !== activeMilestone.resultHash ||
|
|
1234
2178
|
evidenceHash !== activeMilestone.evidenceHash) {
|
|
1235
|
-
return {
|
|
2179
|
+
return withAdoption({
|
|
1236
2180
|
scope: 'milestone',
|
|
1237
2181
|
id: activeMilestone.id,
|
|
1238
2182
|
status: activeMilestone.status,
|
|
1239
2183
|
action: 'restore clean base, then milestone validate',
|
|
1240
2184
|
revision: activeMilestone.revision,
|
|
1241
2185
|
reason: 'final validation evidence is stale',
|
|
1242
|
-
};
|
|
2186
|
+
});
|
|
1243
2187
|
}
|
|
1244
2188
|
const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
|
|
1245
|
-
return {
|
|
2189
|
+
return withAdoption({
|
|
1246
2190
|
scope: 'milestone',
|
|
1247
2191
|
id: activeMilestone.id,
|
|
1248
2192
|
status: activeMilestone.status,
|
|
@@ -1251,32 +2195,53 @@ export class WorkflowService {
|
|
|
1251
2195
|
: 'await explicit human final acceptance',
|
|
1252
2196
|
revision: activeMilestone.revision,
|
|
1253
2197
|
requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
|
|
2198
|
+
completionPolicy: {
|
|
2199
|
+
resultEvidenceAndHeadMustMatch: true,
|
|
2200
|
+
finalAcceptanceTransition: 'milestone.final_accept',
|
|
2201
|
+
delegatedActorMustRemainDelegate: true,
|
|
2202
|
+
},
|
|
1254
2203
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1255
|
-
};
|
|
2204
|
+
});
|
|
1256
2205
|
}
|
|
1257
2206
|
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
1258
2207
|
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
1259
2208
|
: [];
|
|
1260
|
-
return {
|
|
2209
|
+
return withAdoption({
|
|
1261
2210
|
scope: 'milestone',
|
|
1262
2211
|
id: activeMilestone.id,
|
|
1263
2212
|
status: activeMilestone.status,
|
|
1264
2213
|
action: nextForMilestone(activeMilestone),
|
|
1265
2214
|
revision: activeMilestone.revision,
|
|
2215
|
+
...(activeMilestone.status === 'active'
|
|
2216
|
+
? {
|
|
2217
|
+
completionPolicy: {
|
|
2218
|
+
requiredTasksMustBeMerged: true,
|
|
2219
|
+
validationAction: 'milestone validate',
|
|
2220
|
+
finalAcceptanceTransition: 'milestone.final_accept',
|
|
2221
|
+
delegatedActorMustRemainDelegate: true,
|
|
2222
|
+
requiredTasks: activeMilestone.memberships
|
|
2223
|
+
.filter((membership) => membership.disposition === 'required')
|
|
2224
|
+
.map((membership) => {
|
|
2225
|
+
const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
|
|
2226
|
+
return { taskId: membership.taskId, status: task?.status ?? 'missing' };
|
|
2227
|
+
}),
|
|
2228
|
+
},
|
|
2229
|
+
}
|
|
2230
|
+
: {}),
|
|
1266
2231
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1267
|
-
};
|
|
2232
|
+
});
|
|
1268
2233
|
}
|
|
1269
2234
|
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
1270
2235
|
if (activeDiscovery) {
|
|
1271
|
-
return {
|
|
2236
|
+
return withAdoption({
|
|
1272
2237
|
scope: 'discovery',
|
|
1273
2238
|
id: activeDiscovery.id,
|
|
1274
2239
|
status: activeDiscovery.status,
|
|
1275
2240
|
action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
|
|
1276
2241
|
readiness: assessDiscovery(activeDiscovery),
|
|
1277
|
-
};
|
|
2242
|
+
});
|
|
1278
2243
|
}
|
|
1279
|
-
return { scope: 'repository', status: 'idle', action: 'discovery start' };
|
|
2244
|
+
return withAdoption({ scope: 'repository', status: 'idle', action: 'discovery start' });
|
|
1280
2245
|
}
|
|
1281
2246
|
delegatedAuthorization(projectId, grantId, transition, actor, target) {
|
|
1282
2247
|
if (!grantId) {
|
|
@@ -1288,7 +2253,37 @@ export class WorkflowService {
|
|
|
1288
2253
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
1289
2254
|
return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
|
|
1290
2255
|
}
|
|
1291
|
-
|
|
2256
|
+
assertHandoffGrant(projectId, task, targetActor, grantId) {
|
|
2257
|
+
const grant = this.store.readDelegation(projectId, grantId);
|
|
2258
|
+
const policyHash = hashDelegationPolicy(projectId, {
|
|
2259
|
+
principal: grant.principal,
|
|
2260
|
+
delegate: grant.delegate,
|
|
2261
|
+
scope: grant.scope,
|
|
2262
|
+
transitions: grant.transitions,
|
|
2263
|
+
expiresAt: grant.expiresAt,
|
|
2264
|
+
});
|
|
2265
|
+
const scopeMatches = grant.scope.kind === 'project'
|
|
2266
|
+
|| (grant.scope.kind === 'task' && grant.scope.id === task.id)
|
|
2267
|
+
|| (grant.scope.kind === 'milestone' && grant.scope.id === task.milestoneId);
|
|
2268
|
+
if (grant.status !== 'active'
|
|
2269
|
+
|| grant.policyHash !== policyHash
|
|
2270
|
+
|| Date.parse(grant.expiresAt) <= this.now().getTime()
|
|
2271
|
+
|| grant.delegate !== targetActor.trim()
|
|
2272
|
+
|| !scopeMatches) {
|
|
2273
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} cannot be bound to this C1 handoff.`, {
|
|
2274
|
+
grantId,
|
|
2275
|
+
taskId: task.id,
|
|
2276
|
+
targetActor: targetActor.trim(),
|
|
2277
|
+
grantStatus: grant.status,
|
|
2278
|
+
expectedPolicyHash: policyHash,
|
|
2279
|
+
actualPolicyHash: grant.policyHash,
|
|
2280
|
+
grantDelegate: grant.delegate,
|
|
2281
|
+
grantScope: grant.scope,
|
|
2282
|
+
grantExpiresAt: grant.expiresAt,
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
|
|
1292
2287
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
1293
2288
|
? 'task.execution_authorize'
|
|
1294
2289
|
: task.status === 'awaiting_final_acceptance'
|
|
@@ -1297,10 +2292,89 @@ export class WorkflowService {
|
|
|
1297
2292
|
const delegatedApprovalOptions = transition
|
|
1298
2293
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
1299
2294
|
: [];
|
|
1300
|
-
|
|
1301
|
-
|
|
2295
|
+
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
2296
|
+
const next = nextForTask(task);
|
|
2297
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2298
|
+
const strictStepReviewStep = planRiskAudit
|
|
2299
|
+
? task.steps.find((step) => {
|
|
2300
|
+
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
if (task.status !== 'blocked')
|
|
2304
|
+
return true;
|
|
2305
|
+
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
2306
|
+
}) ?? null
|
|
2307
|
+
: null;
|
|
2308
|
+
const handoffOverlay = handoffPosture.state === 'pending'
|
|
2309
|
+
? {
|
|
2310
|
+
action: 'task claim',
|
|
2311
|
+
blockedAction: next.action,
|
|
2312
|
+
c1Handoff: {
|
|
2313
|
+
state: 'pending',
|
|
2314
|
+
targetActor: handoffPosture.targetActor,
|
|
2315
|
+
eventId: handoffPosture.latestEvent.eventId,
|
|
2316
|
+
},
|
|
2317
|
+
}
|
|
2318
|
+
: handoffPosture.state === 'claimed'
|
|
2319
|
+
? {
|
|
2320
|
+
c1Handoff: {
|
|
2321
|
+
state: 'claimed',
|
|
2322
|
+
claimant: handoffPosture.claimant,
|
|
2323
|
+
writerLeaseTokenBound: handoffPosture.writerLeaseTokenHash !== null,
|
|
2324
|
+
requiredActor: handoffPosture.claimant,
|
|
2325
|
+
},
|
|
2326
|
+
}
|
|
2327
|
+
: {};
|
|
2328
|
+
const result = {
|
|
2329
|
+
...next,
|
|
2330
|
+
...handoffOverlay,
|
|
2331
|
+
...(strictStepReviewStep
|
|
2332
|
+
? {
|
|
2333
|
+
action: 'task step-review',
|
|
2334
|
+
stepId: strictStepReviewStep.id,
|
|
2335
|
+
commitPolicy: {
|
|
2336
|
+
owner: 'workflow-core',
|
|
2337
|
+
manualCommitAllowed: false,
|
|
2338
|
+
completionAction: 'task step-review',
|
|
2339
|
+
},
|
|
2340
|
+
strictStepReview: {
|
|
2341
|
+
reviewRequired: true,
|
|
2342
|
+
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
2343
|
+
},
|
|
2344
|
+
}
|
|
2345
|
+
: {}),
|
|
2346
|
+
...(planRiskAudit
|
|
2347
|
+
? {
|
|
2348
|
+
planRiskAudit: {
|
|
2349
|
+
decision: planRiskAudit.audit.decision,
|
|
2350
|
+
scope: planRiskAudit.scope,
|
|
2351
|
+
postureEventType: planRiskAudit.postureEventType,
|
|
2352
|
+
reviewRequiredStepIds: planRiskAudit.reviewRequiredStepIds,
|
|
2353
|
+
},
|
|
2354
|
+
}
|
|
2355
|
+
: {}),
|
|
2356
|
+
...(task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2
|
|
2357
|
+
? {
|
|
2358
|
+
action: 'task plan-set with independent corrective audit',
|
|
2359
|
+
correctivePlanGate: {
|
|
2360
|
+
failedReviewAttempts,
|
|
2361
|
+
auditFileRequired: true,
|
|
2362
|
+
auditorMustDifferFromFailedReviewer: true,
|
|
2363
|
+
allowedDecision: 'continue-fix',
|
|
2364
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
2365
|
+
},
|
|
2366
|
+
}
|
|
2367
|
+
: {}),
|
|
1302
2368
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1303
2369
|
};
|
|
2370
|
+
if (handoffPosture.state === 'pending') {
|
|
2371
|
+
return {
|
|
2372
|
+
...result,
|
|
2373
|
+
action: 'task claim',
|
|
2374
|
+
blockedAction: next.action,
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
return result;
|
|
1304
2378
|
}
|
|
1305
2379
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
1306
2380
|
return this.store.listDelegations(projectId)
|
|
@@ -1315,6 +2389,21 @@ export class WorkflowService {
|
|
|
1315
2389
|
policyHash: grant.policyHash,
|
|
1316
2390
|
}));
|
|
1317
2391
|
}
|
|
2392
|
+
delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
|
|
2393
|
+
return this.store.listDelegations(projectId)
|
|
2394
|
+
.filter((grant) => delegationCanAuthorize(grant, 'project_memory.approve', knowledgeMap, this.now()))
|
|
2395
|
+
.filter((grant) => delegationCanAuthorize(grant, 'task.execution_authorize', task, this.now()))
|
|
2396
|
+
.map((grant) => ({
|
|
2397
|
+
grantId: grant.id,
|
|
2398
|
+
principal: grant.principal,
|
|
2399
|
+
delegate: grant.delegate,
|
|
2400
|
+
transition: 'task.execution_authorize',
|
|
2401
|
+
transitions: ['project_memory.approve', 'task.execution_authorize'],
|
|
2402
|
+
scope: grant.scope,
|
|
2403
|
+
expiresAt: grant.expiresAt,
|
|
2404
|
+
policyHash: grant.policyHash,
|
|
2405
|
+
}));
|
|
2406
|
+
}
|
|
1318
2407
|
assertDelegationScopeExists(projectId, policy) {
|
|
1319
2408
|
if (policy.scope.kind === 'task') {
|
|
1320
2409
|
this.store.readTask(projectId, policy.scope.id);
|
|
@@ -1352,6 +2441,75 @@ export class WorkflowService {
|
|
|
1352
2441
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone authorization is stale.');
|
|
1353
2442
|
}
|
|
1354
2443
|
}
|
|
2444
|
+
readMilestoneCurrent(projectId, milestoneId) {
|
|
2445
|
+
return readMilestoneWithIntegrity(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
|
|
2446
|
+
}
|
|
2447
|
+
readMilestoneScopeChangeCurrent(projectId, milestoneId) {
|
|
2448
|
+
return readMilestoneForScopeChange(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
|
|
2449
|
+
}
|
|
2450
|
+
listMilestonesCurrent(projectId, posture = this.readCurrentAdoptionPostureStrict(projectId)) {
|
|
2451
|
+
const milestones = this.store.listMilestones(projectId);
|
|
2452
|
+
if (!posture)
|
|
2453
|
+
return milestones;
|
|
2454
|
+
const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
|
|
2455
|
+
return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
|
|
2456
|
+
? milestone
|
|
2457
|
+
: readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
2458
|
+
}
|
|
2459
|
+
prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
|
|
2460
|
+
validateMilestonePlan(plan);
|
|
2461
|
+
return prepareMilestoneScopeChangeCandidate({
|
|
2462
|
+
milestone,
|
|
2463
|
+
plan,
|
|
2464
|
+
readTask: (taskId) => this.store.readTask(projectId, taskId),
|
|
2465
|
+
actor,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
|
|
2469
|
+
const currentLease = context.locks.inspect(taskId);
|
|
2470
|
+
if (!currentLease) {
|
|
2471
|
+
if (writerToken !== null) {
|
|
2472
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
2473
|
+
taskId,
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
if (writerToken === null) {
|
|
2479
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} requires the active writer token while a lease is present.`, {
|
|
2480
|
+
taskId,
|
|
2481
|
+
leaseOwner: currentLease.owner,
|
|
2482
|
+
expiresAt: currentLease.expiresAt,
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
return context.locks.heartbeat(taskId, writerToken);
|
|
2486
|
+
}
|
|
2487
|
+
proveOptionalWriterLeaseToken(context, taskId, writerToken) {
|
|
2488
|
+
const currentLease = context.locks.inspect(taskId);
|
|
2489
|
+
if (!currentLease) {
|
|
2490
|
+
if (writerToken !== null) {
|
|
2491
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
2492
|
+
taskId,
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
if (writerToken === null)
|
|
2498
|
+
return null;
|
|
2499
|
+
return context.locks.heartbeat(taskId, writerToken);
|
|
2500
|
+
}
|
|
2501
|
+
assertTaskMutationAllowedByHandoff(task, actor, writerToken) {
|
|
2502
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
2503
|
+
if (posture.state === 'none')
|
|
2504
|
+
return;
|
|
2505
|
+
if (!actor || !actor.trim()) {
|
|
2506
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has active C1 coordination and requires an explicit actor for mutation.`, {
|
|
2507
|
+
taskId: task.id,
|
|
2508
|
+
posture: posture.state,
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
assertTaskMutationAllowedByC1(this.store, task, { actor: actor.trim(), writerToken });
|
|
2512
|
+
}
|
|
1355
2513
|
readGraphBindingOrNull(projectId, graphKind) {
|
|
1356
2514
|
try {
|
|
1357
2515
|
return this.store.readGraphBinding(projectId, graphKind);
|
|
@@ -1374,6 +2532,93 @@ export class WorkflowService {
|
|
|
1374
2532
|
}
|
|
1375
2533
|
return inspected;
|
|
1376
2534
|
}
|
|
2535
|
+
strictReviewKnowledgeMap(repositoryRoot, task) {
|
|
2536
|
+
const stored = this.store.readKnowledgeMap(task.projectId);
|
|
2537
|
+
const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repositoryRoot, task.projectId, this.now()));
|
|
2538
|
+
if (inspected.status === 'active' && inspected.approval)
|
|
2539
|
+
return inspected;
|
|
2540
|
+
const storedHash = hashKnowledgeMap(stored);
|
|
2541
|
+
if (stored.status === 'active'
|
|
2542
|
+
&& stored.approval
|
|
2543
|
+
&& task.knowledgeMapRevision === stored.revision
|
|
2544
|
+
&& task.knowledgeMapHash === storedHash
|
|
2545
|
+
&& stored.approval.mapHash === storedHash) {
|
|
2546
|
+
return stored;
|
|
2547
|
+
}
|
|
2548
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review has neither a current active Knowledge Map nor its exact historical Task binding.', {
|
|
2549
|
+
taskId: task.id,
|
|
2550
|
+
inspectedStatus: inspected.status,
|
|
2551
|
+
taskKnowledgeMapRevision: task.knowledgeMapRevision,
|
|
2552
|
+
storedKnowledgeMapRevision: stored.revision,
|
|
2553
|
+
taskKnowledgeMapHash: task.knowledgeMapHash,
|
|
2554
|
+
storedKnowledgeMapHash: storedHash,
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
inspectAdoptionPreparation(identity, tasks, milestones) {
|
|
2558
|
+
const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
|
|
2559
|
+
const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
|
|
2560
|
+
if (currentPosture) {
|
|
2561
|
+
assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
|
|
2562
|
+
}
|
|
2563
|
+
return buildAdoptionPreparation({
|
|
2564
|
+
projectId: identity.projectId,
|
|
2565
|
+
repositoryIdentity: identity,
|
|
2566
|
+
packageVersion: PACKAGE_VERSION,
|
|
2567
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2568
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
2569
|
+
tasks,
|
|
2570
|
+
milestones,
|
|
2571
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
|
|
2572
|
+
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
|
|
2573
|
+
now: this.now(),
|
|
2574
|
+
}, currentPosture);
|
|
2575
|
+
}
|
|
2576
|
+
computeAdoptionBoundaryBlockers(identity, tasks, leases) {
|
|
2577
|
+
const dirty = changedFiles(identity.repositoryRoot);
|
|
2578
|
+
const runningSteps = tasks.flatMap((task) => task.steps
|
|
2579
|
+
.filter((step) => step.status === 'in_progress')
|
|
2580
|
+
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
2581
|
+
const now = this.now().getTime();
|
|
2582
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
2583
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2584
|
+
return [
|
|
2585
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
2586
|
+
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
2587
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
2588
|
+
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2589
|
+
];
|
|
2590
|
+
}
|
|
2591
|
+
collectAdoptionSidecarInventory(projectId, tasks, milestones) {
|
|
2592
|
+
const inventory = [];
|
|
2593
|
+
const collect = (scope, ownerId, root) => {
|
|
2594
|
+
if (!existsSync(root))
|
|
2595
|
+
return;
|
|
2596
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
2597
|
+
if (entry.isSymbolicLink()) {
|
|
2598
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory rejects symlink entries: ${path.join(root, entry.name)}`);
|
|
2599
|
+
}
|
|
2600
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
2601
|
+
continue;
|
|
2602
|
+
const filePath = path.join(root, entry.name);
|
|
2603
|
+
const stat = lstatSync(filePath);
|
|
2604
|
+
if (!stat.isFile()) {
|
|
2605
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory requires regular files only: ${filePath}`);
|
|
2606
|
+
}
|
|
2607
|
+
inventory.push({
|
|
2608
|
+
scope,
|
|
2609
|
+
ownerId,
|
|
2610
|
+
sidecarName: entry.name,
|
|
2611
|
+
contentHash: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
collect('project', projectId, this.store.projectRoot(projectId));
|
|
2616
|
+
for (const task of tasks)
|
|
2617
|
+
collect('task', task.id, this.store.taskRoot(projectId, task.id));
|
|
2618
|
+
for (const milestone of milestones)
|
|
2619
|
+
collect('milestone', milestone.id, this.store.milestoneRoot(projectId, milestone.id));
|
|
2620
|
+
return inventory;
|
|
2621
|
+
}
|
|
1377
2622
|
assertKnowledgeMapBinding(repositoryRoot, projectId, revision, mapHash) {
|
|
1378
2623
|
const map = this.requireActiveKnowledgeMap(repositoryRoot, projectId);
|
|
1379
2624
|
const currentHash = hashKnowledgeMap(map);
|
|
@@ -1387,6 +2632,29 @@ export class WorkflowService {
|
|
|
1387
2632
|
}
|
|
1388
2633
|
return map;
|
|
1389
2634
|
}
|
|
2635
|
+
readCurrentAdoptionPostureStrict(projectId) {
|
|
2636
|
+
const posture = readCurrentAdoptionPosture(this.store, projectId);
|
|
2637
|
+
if (!posture)
|
|
2638
|
+
return null;
|
|
2639
|
+
assertAdoptionBaselinePreserved(posture, this.store.listTasks(projectId), this.store.listMilestones(projectId));
|
|
2640
|
+
return posture;
|
|
2641
|
+
}
|
|
2642
|
+
requireExecutionAdoptionPosture(identity, taskId) {
|
|
2643
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
2644
|
+
const milestones = this.store.listMilestones(identity.projectId);
|
|
2645
|
+
const posture = this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2646
|
+
if (posture)
|
|
2647
|
+
return posture;
|
|
2648
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, milestones);
|
|
2649
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption posture is required before execution can proceed.', {
|
|
2650
|
+
taskId,
|
|
2651
|
+
adoptionStatus: adoption.status,
|
|
2652
|
+
requiredAction: adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2653
|
+
blockers: adoption.blockers,
|
|
2654
|
+
confirmationCode: adoption.confirmationCode,
|
|
2655
|
+
confirmationCodeBindingHash: adoption.confirmationCodeBindingHash,
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
1390
2658
|
assertTaskKnowledgeBinding(repositoryRoot, task) {
|
|
1391
2659
|
if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
1392
2660
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has no Project Knowledge Map binding.`);
|
|
@@ -1458,6 +2726,62 @@ function dependenciesComplete(step, completed) {
|
|
|
1458
2726
|
const completedIds = new Set(completed);
|
|
1459
2727
|
return step.dependencies.every((dependency) => completedIds.has(dependency));
|
|
1460
2728
|
}
|
|
2729
|
+
function applyStrictStepReviewResolution(task, stepId, resolution, summary) {
|
|
2730
|
+
if (resolution === 'pending') {
|
|
2731
|
+
throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution is still pending for ${stepId}.`, {
|
|
2732
|
+
taskId: task.id,
|
|
2733
|
+
stepId,
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
const step = requireStep(task, stepId);
|
|
2737
|
+
if (!step.evidence) {
|
|
2738
|
+
throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution requires canonical evidence for ${stepId}.`, {
|
|
2739
|
+
taskId: task.id,
|
|
2740
|
+
stepId,
|
|
2741
|
+
resolution,
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
if (resolution === 'passed_verified') {
|
|
2745
|
+
const completedIds = new Set([
|
|
2746
|
+
...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
|
|
2747
|
+
stepId,
|
|
2748
|
+
]);
|
|
2749
|
+
return {
|
|
2750
|
+
...task,
|
|
2751
|
+
status: 'in_progress',
|
|
2752
|
+
steps: task.steps.map((candidate) => {
|
|
2753
|
+
if (candidate.id === stepId)
|
|
2754
|
+
return { ...candidate, status: 'completed', evidence: step.evidence };
|
|
2755
|
+
if (candidate.status === 'blocked' && dependenciesComplete(candidate, completedIds)) {
|
|
2756
|
+
return { ...candidate, status: 'planned' };
|
|
2757
|
+
}
|
|
2758
|
+
return candidate;
|
|
2759
|
+
}),
|
|
2760
|
+
blockReason: null,
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
if (resolution === 'failed') {
|
|
2764
|
+
return {
|
|
2765
|
+
...task,
|
|
2766
|
+
status: 'needs_fix',
|
|
2767
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
2768
|
+
? { ...candidate, status: 'failed', evidence: null }
|
|
2769
|
+
: candidate),
|
|
2770
|
+
blockReason: summary?.trim() || `Strict Step Review failed for ${stepId}.`,
|
|
2771
|
+
systemCommits: task.systemCommits.includes(step.evidence.commitSha)
|
|
2772
|
+
? task.systemCommits
|
|
2773
|
+
: [...task.systemCommits, step.evidence.commitSha],
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
return {
|
|
2777
|
+
...task,
|
|
2778
|
+
status: 'blocked',
|
|
2779
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
2780
|
+
? { ...candidate, status: 'in_progress', evidence: step.evidence }
|
|
2781
|
+
: candidate),
|
|
2782
|
+
blockReason: summary?.trim() || `Strict Step Review isolation could not be verified for ${stepId}.`,
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
1461
2785
|
function identityRelative(repositoryRoot, value) {
|
|
1462
2786
|
return path.resolve(repositoryRoot, value);
|
|
1463
2787
|
}
|
|
@@ -1511,6 +2835,83 @@ function validateReviewInput(review) {
|
|
|
1511
2835
|
throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
|
|
1512
2836
|
}
|
|
1513
2837
|
}
|
|
2838
|
+
function countFailedReviewAttempts(taskRoot) {
|
|
2839
|
+
const file = path.join(taskRoot, 'findings.jsonl');
|
|
2840
|
+
if (!existsSync(file))
|
|
2841
|
+
return 0;
|
|
2842
|
+
const attempts = new Set();
|
|
2843
|
+
for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
2844
|
+
try {
|
|
2845
|
+
const finding = JSON.parse(line);
|
|
2846
|
+
if (typeof finding.recordedAt === 'string' && finding.recordedAt) {
|
|
2847
|
+
attempts.add(`${finding.recordedAt}\0${typeof finding.reviewer === 'string' ? finding.reviewer : ''}`);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
catch {
|
|
2851
|
+
throw new WorkflowError('STATE_CORRUPT', `Invalid review finding log: ${file}`);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
return attempts.size;
|
|
2855
|
+
}
|
|
2856
|
+
function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
2857
|
+
if (!audit) {
|
|
2858
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
|
|
2859
|
+
failedReviewAttempts,
|
|
2860
|
+
requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
if (!audit.auditor.trim() || !audit.summary.trim()) {
|
|
2864
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
|
|
2865
|
+
}
|
|
2866
|
+
if (audit.auditor.trim() === task.review.reviewer.trim()) {
|
|
2867
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
|
|
2868
|
+
}
|
|
2869
|
+
const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
|
|
2870
|
+
const reviewedFindingIds = new Set(audit.reviewedFindingIds);
|
|
2871
|
+
if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
|
|
2872
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
|
|
2873
|
+
}
|
|
2874
|
+
const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
|
|
2875
|
+
if (missing.length > 0) {
|
|
2876
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
|
|
2877
|
+
missingFindingIds: missing,
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
if (audit.decision !== 'continue-fix') {
|
|
2881
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
2885
|
+
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
2886
|
+
recordedAt: now.toISOString(),
|
|
2887
|
+
taskId: previous.id,
|
|
2888
|
+
taskRevision: previous.revision,
|
|
2889
|
+
failedReviewAttempts,
|
|
2890
|
+
previousPlanHash: previous.planHash,
|
|
2891
|
+
correctivePlanHash: saved.planHash,
|
|
2892
|
+
...audit,
|
|
2893
|
+
})}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
2894
|
+
}
|
|
2895
|
+
function assertContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
2896
|
+
if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
|
|
2897
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh is limited to content-hash drift. Knowledge sources, categories, scopes, authorities, gaps, or conflicts changed.', { requiredAction: 'Run project-memory reconcile, inspect the classification diff, and approve it explicitly.' });
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
function isContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
2901
|
+
const classification = (map) => JSON.stringify({
|
|
2902
|
+
entries: map.entries.map((entry) => ({
|
|
2903
|
+
id: entry.id,
|
|
2904
|
+
category: entry.category,
|
|
2905
|
+
path: entry.path,
|
|
2906
|
+
scope: entry.scope,
|
|
2907
|
+
authority: entry.authority,
|
|
2908
|
+
discoveredBy: entry.discoveredBy,
|
|
2909
|
+
})),
|
|
2910
|
+
gaps: map.gaps,
|
|
2911
|
+
conflicts: map.conflicts,
|
|
2912
|
+
});
|
|
2913
|
+
return classification(approved) === classification(inspected);
|
|
2914
|
+
}
|
|
1514
2915
|
function validateMilestonePlan(plan) {
|
|
1515
2916
|
if (!plan.outcome.trim())
|
|
1516
2917
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');
|
|
@@ -1606,11 +3007,14 @@ function nextForMilestone(milestone) {
|
|
|
1606
3007
|
}
|
|
1607
3008
|
function nextForTask(task) {
|
|
1608
3009
|
const stepsComplete = task.steps.every((step) => step.status === 'completed' || step.status === 'skipped');
|
|
3010
|
+
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
3011
|
+
const failedStep = task.steps.find((step) => step.status === 'failed');
|
|
3012
|
+
const runnableStep = task.steps.find((step) => step.status === 'planned' && step.dependencies.every((dependency) => ['completed', 'skipped'].includes(requireStep(task, dependency).status)));
|
|
1609
3013
|
const actionByStatus = {
|
|
1610
3014
|
planning: 'task plan-set',
|
|
1611
3015
|
awaiting_execution_authorization: 'task authorize',
|
|
1612
3016
|
ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
|
|
1613
|
-
in_progress:
|
|
3017
|
+
in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
|
|
1614
3018
|
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
|
|
1615
3019
|
needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
|
|
1616
3020
|
awaiting_final_acceptance: 'task accept',
|
|
@@ -1620,7 +3024,24 @@ function nextForTask(task) {
|
|
|
1620
3024
|
blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
|
|
1621
3025
|
cancelled: 'none',
|
|
1622
3026
|
};
|
|
1623
|
-
|
|
3027
|
+
const selectedStep = activeStep ?? failedStep ?? runnableStep;
|
|
3028
|
+
return {
|
|
3029
|
+
scope: 'task',
|
|
3030
|
+
id: task.id,
|
|
3031
|
+
status: task.status,
|
|
3032
|
+
action: actionByStatus[task.status],
|
|
3033
|
+
revision: task.revision,
|
|
3034
|
+
...(selectedStep ? { stepId: selectedStep.id } : {}),
|
|
3035
|
+
...(activeStep
|
|
3036
|
+
? {
|
|
3037
|
+
commitPolicy: {
|
|
3038
|
+
owner: 'workflow-core',
|
|
3039
|
+
manualCommitAllowed: false,
|
|
3040
|
+
completionAction: 'task step-complete',
|
|
3041
|
+
},
|
|
3042
|
+
}
|
|
3043
|
+
: {}),
|
|
3044
|
+
};
|
|
1624
3045
|
}
|
|
1625
3046
|
function knowledgeSummary(map) {
|
|
1626
3047
|
return {
|