codex-workflow-v2 2.0.0-alpha.4 → 2.0.0-alpha.7
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 +40 -0
- package/dist/src/alpha6/handoff.js +981 -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 +52 -0
- package/dist/src/alpha6/milestone.js +1103 -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 +908 -0
- package/dist/src/alpha6/plan-risk.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +21 -0
- package/dist/src/alpha6/remediation.js +754 -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/alpha7/autonomy.d.ts +61 -0
- package/dist/src/alpha7/autonomy.js +256 -0
- package/dist/src/alpha7/autonomy.js.map +1 -0
- package/dist/src/cli.js +109 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +268 -0
- package/dist/src/git.js +2 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- 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/workflow.d.ts +113 -8
- package/dist/src/workflow.js +1635 -78
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +164 -0
- package/docs/decisions.md +39 -0
- package/docs/delegated-approval.md +37 -5
- package/docs/development-flow.md +45 -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 +9 -0
- package/docs/updating-existing-project.md +63 -5
- package/docs/validation-report.md +52 -34
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +182 -6
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +35 -1
- package/references/git-policy.md +5 -2
- package/references/state-machine.md +43 -0
- package/references/validation-and-review.md +28 -1
- package/roles/delivery-coordinator.md +21 -1
- package/roles/independent-reviewer.md +3 -0
- package/roles/technical-planner.md +10 -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-autonomy-event.schema.json +45 -0
- package/schemas/milestone-scope-change-event.schema.json +70 -0
- package/schemas/milestone-transaction-journal.schema.json +95 -0
- package/schemas/plan-risk-audit-event.schema.json +107 -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,26 @@
|
|
|
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,
|
|
6
|
+
import { assertDelegationCanAuthorize, 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
10
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
11
|
+
import { inspectLocalDependency } from './diagnostics.js';
|
|
12
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, 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, defaultCorrectiveAuditorActor, 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';
|
|
18
|
+
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
11
19
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
20
|
+
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
12
21
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
13
22
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
23
|
+
import { launchStrictStepReviewer } from './reviewer.js';
|
|
14
24
|
import { WriterLockManager } from './state/lock.js';
|
|
15
25
|
import { FileStateStore } from './state/store.js';
|
|
16
26
|
import { createEntityId } from './ulid.js';
|
|
@@ -24,6 +34,18 @@ export class WorkflowService {
|
|
|
24
34
|
}
|
|
25
35
|
context(repository) {
|
|
26
36
|
const identity = resolveRepositoryIdentity(repository);
|
|
37
|
+
const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
|
|
38
|
+
const projectAlreadyRegistered = existsSync(projectFile);
|
|
39
|
+
if (!projectAlreadyRegistered) {
|
|
40
|
+
initializeProjectRegistrationAdoption(this.store, {
|
|
41
|
+
projectId: identity.projectId,
|
|
42
|
+
repositoryIdentity: identity,
|
|
43
|
+
packageVersion: PACKAGE_VERSION,
|
|
44
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
45
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
46
|
+
now: this.now(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
27
49
|
this.store.registerProject(identity);
|
|
28
50
|
return {
|
|
29
51
|
identity,
|
|
@@ -162,7 +184,12 @@ export class WorkflowService {
|
|
|
162
184
|
if (!title.trim())
|
|
163
185
|
throw new WorkflowError('INVALID_ARGUMENT', 'Task title is required.');
|
|
164
186
|
if (milestoneId) {
|
|
165
|
-
|
|
187
|
+
// A Milestone is assembled from independently materialized Task discoveries. During
|
|
188
|
+
// that bounded assembly window the reverse-membership set is intentionally incomplete;
|
|
189
|
+
// execution remains unavailable until one complete initial Milestone Plan classifies
|
|
190
|
+
// every linked Task. The scope-change reader preserves every other integrity check while
|
|
191
|
+
// allowing exactly that repairable reverse gap.
|
|
192
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
166
193
|
if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
|
|
167
194
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
168
195
|
}
|
|
@@ -271,17 +298,29 @@ export class WorkflowService {
|
|
|
271
298
|
}
|
|
272
299
|
setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
|
|
273
300
|
const { identity } = this.context(repository);
|
|
274
|
-
const milestone = this.
|
|
301
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
275
302
|
assertExpectedRevision(milestone, expectedRevision);
|
|
276
303
|
if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
|
|
277
304
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Milestone Plan while ${milestone.status}.`);
|
|
278
305
|
}
|
|
306
|
+
if (milestone.status !== 'planning'
|
|
307
|
+
|| milestone.planHash !== null
|
|
308
|
+
|| milestone.membershipRevision !== 0
|
|
309
|
+
|| milestone.taskIds.length > 0
|
|
310
|
+
|| milestone.memberships.length > 0) {
|
|
311
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan is no longer in the initial empty planning posture. Use explicit scope-change prepare/apply.', {
|
|
312
|
+
milestoneId,
|
|
313
|
+
revision: milestone.revision,
|
|
314
|
+
requiredAction: 'prepareMilestoneScopeChange -> applyMilestoneScopeChange',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
279
317
|
validateMilestonePlan(plan);
|
|
280
|
-
const
|
|
318
|
+
const normalized = normalizeMilestonePlan(plan);
|
|
319
|
+
const taskIds = normalized.taskIds;
|
|
281
320
|
if (new Set(taskIds).size !== taskIds.length) {
|
|
282
321
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone memberships must have unique Task IDs.');
|
|
283
322
|
}
|
|
284
|
-
for (const membership of
|
|
323
|
+
for (const membership of normalized.memberships) {
|
|
285
324
|
const task = this.store.readTask(identity.projectId, membership.taskId);
|
|
286
325
|
if (task.milestoneId !== milestoneId) {
|
|
287
326
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} is not linked to Milestone ${milestoneId}.`);
|
|
@@ -296,22 +335,18 @@ export class WorkflowService {
|
|
|
296
335
|
throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
|
|
297
336
|
}
|
|
298
337
|
}
|
|
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
338
|
const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
312
339
|
? { ...authorization, decision: 'superseded' }
|
|
313
340
|
: authorization);
|
|
314
|
-
const
|
|
341
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
342
|
+
const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
|
|
343
|
+
outcome: normalized.outcome,
|
|
344
|
+
successSignal: normalized.successSignal,
|
|
345
|
+
acceptance: normalized.acceptance,
|
|
346
|
+
memberships: normalized.memberships,
|
|
347
|
+
checks: normalized.checks,
|
|
348
|
+
}, null, 2));
|
|
349
|
+
const candidate = {
|
|
315
350
|
...milestone,
|
|
316
351
|
status: 'awaiting_execution_authorization',
|
|
317
352
|
outcome: normalized.outcome,
|
|
@@ -327,13 +362,113 @@ export class WorkflowService {
|
|
|
327
362
|
evidenceHash: null,
|
|
328
363
|
acceptedHead: null,
|
|
329
364
|
cancellationReason: null,
|
|
330
|
-
}
|
|
365
|
+
};
|
|
366
|
+
assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
|
|
367
|
+
const saved = this.store.writeMilestone(candidate, expectedRevision);
|
|
331
368
|
this.store.publishArtifact(stagedPlan);
|
|
332
369
|
return saved;
|
|
333
370
|
}
|
|
371
|
+
prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
|
|
372
|
+
const { identity } = this.context(repository);
|
|
373
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
374
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
375
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
376
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
377
|
+
return {
|
|
378
|
+
milestoneId: prepared.milestoneId,
|
|
379
|
+
revision: prepared.expectedRevision,
|
|
380
|
+
actor: prepared.actor,
|
|
381
|
+
candidatePlanHash: prepared.candidatePlanHash,
|
|
382
|
+
candidateMembershipRevision: prepared.candidateMembershipRevision,
|
|
383
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
384
|
+
confirmationCode: prepared.confirmationCode,
|
|
385
|
+
after: prepared.after,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
389
|
+
const { identity } = this.context(repository);
|
|
390
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
391
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
392
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
393
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
394
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
395
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
|
|
396
|
+
milestoneId,
|
|
397
|
+
revision: milestone.revision,
|
|
398
|
+
actor: prepared.actor,
|
|
399
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return applyMilestoneScopeChangeTransaction({
|
|
403
|
+
store: this.store,
|
|
404
|
+
milestone,
|
|
405
|
+
expectedRevision,
|
|
406
|
+
prepared,
|
|
407
|
+
now: this.now(),
|
|
408
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
412
|
+
const { identity } = this.context(repository);
|
|
413
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
414
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
415
|
+
return prepareMilestoneAutonomyContract({
|
|
416
|
+
projectId: identity.projectId,
|
|
417
|
+
milestone,
|
|
418
|
+
principal,
|
|
419
|
+
delegate,
|
|
420
|
+
expiresAt,
|
|
421
|
+
now: this.now(),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
425
|
+
const { identity } = this.context(repository);
|
|
426
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
427
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
428
|
+
return applyMilestoneAutonomyContract({
|
|
429
|
+
store: this.store,
|
|
430
|
+
projectId: identity.projectId,
|
|
431
|
+
milestone,
|
|
432
|
+
principal,
|
|
433
|
+
delegate,
|
|
434
|
+
expiresAt,
|
|
435
|
+
confirmationCode,
|
|
436
|
+
now: this.now(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
440
|
+
const { identity } = this.context(repository);
|
|
441
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
442
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
443
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
444
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
445
|
+
store: this.store,
|
|
446
|
+
projectId: identity.projectId,
|
|
447
|
+
milestone,
|
|
448
|
+
actor,
|
|
449
|
+
now: this.now(),
|
|
450
|
+
});
|
|
451
|
+
validateMilestonePlan(plan);
|
|
452
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
453
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
454
|
+
milestone,
|
|
455
|
+
plan,
|
|
456
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
457
|
+
actor,
|
|
458
|
+
autonomyContractEventHash: autonomy.contract.eventHash,
|
|
459
|
+
});
|
|
460
|
+
return applyMilestoneScopeChangeTransaction({
|
|
461
|
+
store: this.store,
|
|
462
|
+
milestone,
|
|
463
|
+
expectedRevision,
|
|
464
|
+
prepared,
|
|
465
|
+
now: this.now(),
|
|
466
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
334
469
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
335
470
|
const { identity } = this.context(repository);
|
|
336
|
-
const milestone = this.
|
|
471
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
337
472
|
assertExpectedRevision(milestone, expectedRevision);
|
|
338
473
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
339
474
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
@@ -362,7 +497,7 @@ export class WorkflowService {
|
|
|
362
497
|
}
|
|
363
498
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
364
499
|
const { identity } = this.context(repository);
|
|
365
|
-
const milestone = this.
|
|
500
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
366
501
|
assertExpectedRevision(milestone, expectedRevision);
|
|
367
502
|
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
368
503
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
@@ -425,7 +560,7 @@ export class WorkflowService {
|
|
|
425
560
|
}
|
|
426
561
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
427
562
|
const { identity } = this.context(repository);
|
|
428
|
-
const milestone = this.
|
|
563
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
429
564
|
assertExpectedRevision(milestone, expectedRevision);
|
|
430
565
|
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
431
566
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
|
|
@@ -480,23 +615,93 @@ export class WorkflowService {
|
|
|
480
615
|
if (!reason.trim())
|
|
481
616
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
482
617
|
const { identity } = this.context(repository);
|
|
483
|
-
const milestone = this.
|
|
618
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
484
619
|
assertExpectedRevision(milestone, expectedRevision);
|
|
485
620
|
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
486
621
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
487
622
|
}
|
|
488
623
|
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
489
624
|
}
|
|
490
|
-
setTaskPlan(repository, taskId, expectedRevision, plan) {
|
|
625
|
+
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
491
626
|
validatePlan(plan);
|
|
492
627
|
const { identity } = this.context(repository);
|
|
493
628
|
const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
|
|
494
629
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
495
630
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
496
631
|
assertExpectedRevision(task, expectedRevision);
|
|
497
|
-
if (!['planning', 'awaiting_execution_authorization', 'needs_fix'].includes(task.status)) {
|
|
632
|
+
if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
498
633
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
499
634
|
}
|
|
635
|
+
const currentPlanCorrectiveDecisions = task.planHash
|
|
636
|
+
? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
|
|
637
|
+
: [];
|
|
638
|
+
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
639
|
+
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
|
|
640
|
+
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
641
|
+
const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
|
|
642
|
+
if (currentHardBlockingDecisions.length > 0) {
|
|
643
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
|
|
644
|
+
taskId: task.id,
|
|
645
|
+
requiredAction: 'Escalate or split the work according to the recorded corrective decision.',
|
|
646
|
+
decisions: currentHardBlockingDecisions.map((event) => ({
|
|
647
|
+
eventId: event.eventId,
|
|
648
|
+
stepId: event.stepId,
|
|
649
|
+
decision: event.decision,
|
|
650
|
+
})),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (task.status === 'blocked' && currentReplanDecisions.length === 0) {
|
|
654
|
+
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.`, {
|
|
655
|
+
taskId: task.id,
|
|
656
|
+
status: task.status,
|
|
657
|
+
requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (currentContinueFixDecisions.length > 0) {
|
|
661
|
+
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.`, {
|
|
662
|
+
taskId: task.id,
|
|
663
|
+
requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
|
|
664
|
+
decisions: currentContinueFixDecisions.map((event) => ({
|
|
665
|
+
eventId: event.eventId,
|
|
666
|
+
stepId: event.stepId,
|
|
667
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
668
|
+
})),
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
672
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
673
|
+
: null;
|
|
674
|
+
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
675
|
+
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
676
|
+
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
677
|
+
}
|
|
678
|
+
const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
|
|
679
|
+
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
680
|
+
: null;
|
|
681
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
|
|
682
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
|
|
683
|
+
taskId: task.id,
|
|
684
|
+
stepId: guardedRemediationGate.stepId,
|
|
685
|
+
attemptCount: guardedRemediationGate.attemptCount,
|
|
686
|
+
requiredSidecar: 'corrective-decisions.jsonl',
|
|
687
|
+
allowedDecision: 'continue-fix',
|
|
688
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (currentReplanDecisions.length > 0 && correctiveAudit) {
|
|
692
|
+
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.`, {
|
|
693
|
+
taskId: task.id,
|
|
694
|
+
decision: correctiveAudit.decision,
|
|
695
|
+
requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
|
|
699
|
+
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.`, {
|
|
700
|
+
taskId: task.id,
|
|
701
|
+
stepId: guardedRemediationGate.stepId,
|
|
702
|
+
decision: correctiveAudit?.decision ?? null,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
500
705
|
for (const requirement of task.requirements) {
|
|
501
706
|
if (!plan.requirements.includes(requirement)) {
|
|
502
707
|
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
|
|
@@ -525,7 +730,24 @@ export class WorkflowService {
|
|
|
525
730
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
526
731
|
throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
|
|
527
732
|
}
|
|
528
|
-
|
|
733
|
+
if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
|
|
734
|
+
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.`, {
|
|
735
|
+
taskId: task.id,
|
|
736
|
+
requiredStepIds: currentReplanRequiredStepIds,
|
|
737
|
+
plannedStepIds: plannedSteps.map((step) => step.id),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
|
|
741
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
|
|
742
|
+
taskId: task.id,
|
|
743
|
+
stepId: guardedRemediationGate.stepId,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
747
|
+
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
748
|
+
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
749
|
+
}
|
|
750
|
+
const candidate = {
|
|
529
751
|
...task,
|
|
530
752
|
status: 'awaiting_execution_authorization',
|
|
531
753
|
planHash: stagedPlan.hash,
|
|
@@ -545,8 +767,16 @@ export class WorkflowService {
|
|
|
545
767
|
resultHash: null,
|
|
546
768
|
evidenceHash: null,
|
|
547
769
|
blockReason: null,
|
|
548
|
-
}
|
|
770
|
+
};
|
|
771
|
+
if (planRiskAudit)
|
|
772
|
+
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
773
|
+
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
549
774
|
this.store.publishArtifact(stagedPlan);
|
|
775
|
+
if (planRiskAudit)
|
|
776
|
+
appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
|
|
777
|
+
if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
|
|
778
|
+
appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
|
|
779
|
+
}
|
|
550
780
|
return saved;
|
|
551
781
|
}
|
|
552
782
|
rebindTaskKnowledge(repository, taskId, expectedRevision) {
|
|
@@ -630,8 +860,62 @@ export class WorkflowService {
|
|
|
630
860
|
this.store.publishArtifact(stagedPlan);
|
|
631
861
|
return saved;
|
|
632
862
|
}
|
|
863
|
+
refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
|
|
864
|
+
if (!delegationGrantId) {
|
|
865
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
|
|
866
|
+
}
|
|
867
|
+
const { identity } = this.context(repository);
|
|
868
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
869
|
+
const currentMap = this.store.readKnowledgeMap(identity.projectId);
|
|
870
|
+
assertExpectedRevision(task, expectedTaskRevision);
|
|
871
|
+
assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
|
|
872
|
+
if (!taskCanRebindKnowledge(task)) {
|
|
873
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
|
|
874
|
+
}
|
|
875
|
+
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
876
|
+
if (activeStep) {
|
|
877
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
|
|
878
|
+
}
|
|
879
|
+
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
880
|
+
const transitions = [];
|
|
881
|
+
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
882
|
+
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
883
|
+
// leave the composite operation half-applied.
|
|
884
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
885
|
+
let knowledgeMap = currentMap;
|
|
886
|
+
if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
|
|
887
|
+
if (currentMap.status !== 'active' || !currentMap.approval || inspectedMap.status !== 'stale') {
|
|
888
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
889
|
+
}
|
|
890
|
+
assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
|
|
891
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
|
|
892
|
+
knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
|
|
893
|
+
transitions.push('project-memory reconcile');
|
|
894
|
+
knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
|
|
895
|
+
transitions.push('project-memory approve');
|
|
896
|
+
}
|
|
897
|
+
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
898
|
+
if (task.knowledgeMapRevision === knowledgeMap.revision
|
|
899
|
+
&& task.knowledgeMapHash === knowledgeMapHash) {
|
|
900
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
901
|
+
}
|
|
902
|
+
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
903
|
+
if (currentPlanRiskAudit) {
|
|
904
|
+
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
905
|
+
}
|
|
906
|
+
transitions.push('task knowledge-rebind');
|
|
907
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, 'Delegated content-only context refresh.', delegationGrantId);
|
|
908
|
+
transitions.push('task execution authorize');
|
|
909
|
+
return {
|
|
910
|
+
mode: 'delegated-content-only',
|
|
911
|
+
transitions,
|
|
912
|
+
knowledgeMap,
|
|
913
|
+
task: refreshedTask,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
633
916
|
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
634
917
|
const { identity } = this.context(repository);
|
|
918
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
635
919
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
636
920
|
assertExpectedRevision(task, expectedRevision);
|
|
637
921
|
if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
|
|
@@ -641,6 +925,7 @@ export class WorkflowService {
|
|
|
641
925
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
642
926
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
643
927
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
928
|
+
readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
|
|
644
929
|
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
645
930
|
const event = {
|
|
646
931
|
kind: 'execution',
|
|
@@ -658,15 +943,164 @@ export class WorkflowService {
|
|
|
658
943
|
};
|
|
659
944
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
660
945
|
}
|
|
946
|
+
recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
|
|
947
|
+
const { identity } = this.context(repository);
|
|
948
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
949
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
950
|
+
assertExpectedRevision(task, expectedRevision);
|
|
951
|
+
this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
952
|
+
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
953
|
+
}
|
|
954
|
+
showTaskHandoff(repository, taskId) {
|
|
955
|
+
const { identity } = this.context(repository);
|
|
956
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
957
|
+
return {
|
|
958
|
+
task,
|
|
959
|
+
posture: readTaskC1Posture(this.store, task),
|
|
960
|
+
claimTokenAvailable: false,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
|
|
964
|
+
const context = this.context(repository);
|
|
965
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
966
|
+
assertExpectedRevision(task, expectedRevision);
|
|
967
|
+
const normalizedActor = actor.trim();
|
|
968
|
+
if (!normalizedActor) {
|
|
969
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
|
|
970
|
+
}
|
|
971
|
+
if (grantId)
|
|
972
|
+
this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
|
|
973
|
+
const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
|
|
974
|
+
const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
|
|
975
|
+
const milestoneDisplayNumber = milestone
|
|
976
|
+
? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
|
|
977
|
+
: null;
|
|
978
|
+
const taskDisplayNumber = milestone
|
|
979
|
+
? milestone.memberships.findIndex((membership) => membership.taskId === task.id) + 1
|
|
980
|
+
: null;
|
|
981
|
+
const currentLease = context.locks.inspect(taskId);
|
|
982
|
+
let lease;
|
|
983
|
+
let acquiredHere = false;
|
|
984
|
+
if (currentLease) {
|
|
985
|
+
if (currentLease.owner !== normalizedActor) {
|
|
986
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} active writer lease belongs to a different actor.`, {
|
|
987
|
+
taskId,
|
|
988
|
+
leaseOwner: currentLease.owner,
|
|
989
|
+
actor: normalizedActor,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
if (writerToken === null) {
|
|
993
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} handoff requires the active writer token while a lease is present.`, {
|
|
994
|
+
taskId,
|
|
995
|
+
leaseOwner: currentLease.owner,
|
|
996
|
+
expiresAt: currentLease.expiresAt,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
if (writerToken !== null) {
|
|
1003
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
1004
|
+
taskId,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
acquiredHere = true;
|
|
1008
|
+
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1009
|
+
}
|
|
1010
|
+
const releaseLease = () => context.locks.release(taskId, lease.token);
|
|
1011
|
+
try {
|
|
1012
|
+
const prepared = appendTaskHandoff(this.store, task, {
|
|
1013
|
+
actor: normalizedActor,
|
|
1014
|
+
targetActor,
|
|
1015
|
+
reason,
|
|
1016
|
+
writerToken: lease.token,
|
|
1017
|
+
milestoneDisplayNumber,
|
|
1018
|
+
taskDisplayNumber,
|
|
1019
|
+
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1020
|
+
grantId,
|
|
1021
|
+
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
1022
|
+
}, this.now());
|
|
1023
|
+
releaseLease();
|
|
1024
|
+
return { task, event: prepared.event, bundle: prepared.bundle };
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (acquiredHere)
|
|
1028
|
+
releaseLease();
|
|
1029
|
+
throw error;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
|
|
1033
|
+
const context = this.context(repository);
|
|
1034
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1035
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1036
|
+
const normalizedActor = actor.trim();
|
|
1037
|
+
if (!normalizedActor) {
|
|
1038
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
1039
|
+
}
|
|
1040
|
+
const pendingHandoff = readTaskC1Posture(this.store, task);
|
|
1041
|
+
if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
|
|
1042
|
+
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
1043
|
+
}
|
|
1044
|
+
const existingLease = context.locks.inspect(taskId);
|
|
1045
|
+
let lease;
|
|
1046
|
+
let acquiredHere = false;
|
|
1047
|
+
if (existingLease) {
|
|
1048
|
+
if (existingLease.owner !== normalizedActor) {
|
|
1049
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} existing writer lease belongs to a different actor.`, {
|
|
1050
|
+
taskId,
|
|
1051
|
+
leaseOwner: existingLease.owner,
|
|
1052
|
+
actor: normalizedActor,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (writerToken === null) {
|
|
1056
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claim requires the existing writer token.`, {
|
|
1057
|
+
taskId,
|
|
1058
|
+
leaseOwner: existingLease.owner,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
if (writerToken !== null) {
|
|
1065
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
1066
|
+
taskId,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
acquiredHere = true;
|
|
1070
|
+
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1074
|
+
return { task, event, lease };
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
if (acquiredHere)
|
|
1078
|
+
context.locks.release(taskId, lease.token);
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
|
|
1083
|
+
const context = this.context(repository);
|
|
1084
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1085
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1086
|
+
const lease = task.status === 'merged' || task.status === 'cancelled'
|
|
1087
|
+
? context.locks.inspect(taskId)
|
|
1088
|
+
: this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
|
|
1089
|
+
const event = appendTaskHandback(this.store, task, actor, reason, lease?.token ?? writerToken, limitations, recommendedNext, this.now());
|
|
1090
|
+
if (lease)
|
|
1091
|
+
context.locks.release(taskId, lease.token);
|
|
1092
|
+
return { task, event };
|
|
1093
|
+
}
|
|
661
1094
|
startTask(repository, taskId, expectedRevision, workspaceOwner) {
|
|
662
1095
|
const { identity } = this.context(repository);
|
|
663
1096
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
664
1097
|
assertExpectedRevision(task, expectedRevision);
|
|
1098
|
+
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
665
1099
|
if (task.status !== 'ready')
|
|
666
1100
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
|
|
667
1101
|
this.assertExecutionAuthorizationFresh(task);
|
|
668
1102
|
if (task.milestoneId) {
|
|
669
|
-
const milestone = this.
|
|
1103
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
|
|
670
1104
|
const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
|
|
671
1105
|
if (milestone.status !== 'active' || membership?.disposition !== 'required') {
|
|
672
1106
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
|
|
@@ -675,6 +1109,15 @@ export class WorkflowService {
|
|
|
675
1109
|
disposition: membership?.disposition ?? null,
|
|
676
1110
|
});
|
|
677
1111
|
}
|
|
1112
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1113
|
+
if (handoffPosture.state !== 'claimed') {
|
|
1114
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
|
|
1115
|
+
taskId: task.id,
|
|
1116
|
+
milestoneId: milestone.id,
|
|
1117
|
+
c1Posture: handoffPosture.state,
|
|
1118
|
+
requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
678
1121
|
}
|
|
679
1122
|
if (!isClean(identity.repositoryRoot)) {
|
|
680
1123
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
@@ -700,20 +1143,48 @@ export class WorkflowService {
|
|
|
700
1143
|
}
|
|
701
1144
|
return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
|
|
702
1145
|
}
|
|
703
|
-
runStep(repository, taskId, stepId, expectedRevision,
|
|
1146
|
+
runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
704
1147
|
const context = this.context(repository);
|
|
705
1148
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
706
1149
|
assertExpectedRevision(task, expectedRevision);
|
|
1150
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
707
1151
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
708
1152
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
709
1153
|
}
|
|
710
1154
|
this.assertExecutionAuthorizationFresh(task);
|
|
711
1155
|
const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1156
|
+
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1157
|
+
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1158
|
+
if (currentPlanBlockingDecisions.length > 0) {
|
|
1159
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run any Step while the current Plan has active blocking corrective decisions.`, {
|
|
1160
|
+
taskId,
|
|
1161
|
+
decisions: currentPlanBlockingDecisions.map((event) => ({
|
|
1162
|
+
eventId: event.eventId,
|
|
1163
|
+
stepId: event.stepId,
|
|
1164
|
+
decision: event.decision,
|
|
1165
|
+
planHash: event.planHash,
|
|
1166
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
1167
|
+
})),
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
712
1170
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
713
1171
|
const step = requireStep(task, stepId);
|
|
1172
|
+
const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
714
1173
|
if (!['planned', 'failed'].includes(step.status)) {
|
|
715
1174
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
|
|
716
1175
|
}
|
|
1176
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1177
|
+
if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
|
|
1178
|
+
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.`, {
|
|
1179
|
+
taskId,
|
|
1180
|
+
stepId,
|
|
1181
|
+
remediationAttemptCount: remediationHistoryForStep.length,
|
|
1182
|
+
currentAuditPresent: Boolean(planRiskAudit),
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1186
|
+
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
|
|
1187
|
+
}
|
|
717
1188
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
718
1189
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
719
1190
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -732,18 +1203,41 @@ export class WorkflowService {
|
|
|
732
1203
|
incompleteDependencies,
|
|
733
1204
|
});
|
|
734
1205
|
}
|
|
1206
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1207
|
+
if (handoffPosture.state === 'pending') {
|
|
1208
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, null);
|
|
1209
|
+
}
|
|
735
1210
|
const existing = context.locks.inspect(taskId);
|
|
736
1211
|
let acquiredHere = false;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1212
|
+
let lease;
|
|
1213
|
+
if (handoffPosture.state === 'claimed') {
|
|
1214
|
+
if (!existing) {
|
|
1215
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires an active writer lease before task run.`, {
|
|
1216
|
+
taskId,
|
|
1217
|
+
claimant: handoffPosture.claimant,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
if (!writerToken) {
|
|
1221
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires the bound writer token for task run.`, {
|
|
1222
|
+
taskId,
|
|
1223
|
+
claimant: handoffPosture.claimant,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
lease = context.locks.heartbeat(taskId, writerToken);
|
|
1227
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, lease.token);
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
lease = existing
|
|
1231
|
+
? writerToken
|
|
1232
|
+
? context.locks.heartbeat(taskId, writerToken)
|
|
1233
|
+
: (() => {
|
|
1234
|
+
throw new WorkflowError('LOCKED', `${taskId} already has a writer.`, { owner: existing.owner });
|
|
1235
|
+
})()
|
|
740
1236
|
: (() => {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
return context.locks.acquire(taskId, owner.trim() || 'worker');
|
|
746
|
-
})();
|
|
1237
|
+
acquiredHere = true;
|
|
1238
|
+
return context.locks.acquire(taskId, actor.trim() || 'worker');
|
|
1239
|
+
})();
|
|
1240
|
+
}
|
|
747
1241
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
|
|
748
1242
|
let saved;
|
|
749
1243
|
try {
|
|
@@ -761,7 +1255,7 @@ export class WorkflowService {
|
|
|
761
1255
|
protocolVersion: PROTOCOL_VERSION,
|
|
762
1256
|
packageVersion: PACKAGE_VERSION,
|
|
763
1257
|
role: 'worker',
|
|
764
|
-
objective: step.objective
|
|
1258
|
+
objective: `${step.objective} Leave all Step changes uncommitted; Workflow core creates the atomic commit through task step-complete.`,
|
|
765
1259
|
inputs: [
|
|
766
1260
|
path.join(this.store.taskRoot(task.projectId, task.id), 'brief.md'),
|
|
767
1261
|
path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
|
|
@@ -769,7 +1263,10 @@ export class WorkflowService {
|
|
|
769
1263
|
allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
|
|
770
1264
|
allowedWrites: step.allowedWrites,
|
|
771
1265
|
commands: step.checks,
|
|
772
|
-
forbiddenScope:
|
|
1266
|
+
forbiddenScope: [
|
|
1267
|
+
...step.forbiddenScope,
|
|
1268
|
+
'Manual Git staging, commits, amendments, resets, rebases, or other history mutations. Workflow core owns the Step commit.',
|
|
1269
|
+
],
|
|
773
1270
|
resultSchema: {
|
|
774
1271
|
type: 'object',
|
|
775
1272
|
required: ['summary', 'changedFiles', 'checks'],
|
|
@@ -783,6 +1280,7 @@ export class WorkflowService {
|
|
|
783
1280
|
'Required change falls outside allowedWrites.',
|
|
784
1281
|
'Implementation requires changing Brief, Plan, or acceptance.',
|
|
785
1282
|
'A configured check cannot be run or interpreted safely.',
|
|
1283
|
+
'Do not run git add or git commit. Return uncommitted changes to the coordinator, which must invoke task step-complete.',
|
|
786
1284
|
],
|
|
787
1285
|
contextBudget: 'medium',
|
|
788
1286
|
taskId,
|
|
@@ -797,10 +1295,22 @@ export class WorkflowService {
|
|
|
797
1295
|
taskContext(repository, taskId, role) {
|
|
798
1296
|
const { identity } = this.context(repository);
|
|
799
1297
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1298
|
+
const historicalStrictReview = role === 'independent-reviewer'
|
|
1299
|
+
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
800
1300
|
const knowledgeMap = task.knowledgeMapRevision === null
|
|
801
1301
|
? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
|
|
802
|
-
:
|
|
1302
|
+
: historicalStrictReview
|
|
1303
|
+
? this.strictReviewKnowledgeMap(identity.repositoryRoot, task)
|
|
1304
|
+
: this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
803
1305
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1306
|
+
const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
|
|
1307
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
1308
|
+
: null;
|
|
1309
|
+
const planRiskReviewScope = planRiskAudit
|
|
1310
|
+
? planRiskAudit.reviewRequiredStepIds.length > 0
|
|
1311
|
+
? ` Current Plan Risk Audit marks ${planRiskAudit.reviewRequiredStepIds.join(', ')} for strict review.`
|
|
1312
|
+
: ' Current Plan Risk Audit marks no Steps for strict review.'
|
|
1313
|
+
: '';
|
|
804
1314
|
const common = {
|
|
805
1315
|
protocolVersion: PROTOCOL_VERSION,
|
|
806
1316
|
packageVersion: PACKAGE_VERSION,
|
|
@@ -822,17 +1332,26 @@ export class WorkflowService {
|
|
|
822
1332
|
if (role === 'technical-planner') {
|
|
823
1333
|
return {
|
|
824
1334
|
...common,
|
|
825
|
-
objective: `Produce an execution-ready Plan for ${task.title}
|
|
1335
|
+
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 and executable proof obligations in the original Plan.${planRiskReviewScope}`,
|
|
826
1336
|
resultSchema: {
|
|
827
1337
|
type: 'object',
|
|
828
1338
|
required: ['objective', 'requirements', 'acceptance', 'risks', 'steps'],
|
|
829
1339
|
},
|
|
1340
|
+
stopConditions: [
|
|
1341
|
+
...common.stopConditions,
|
|
1342
|
+
'A migration is planned without a populated forward-upgrade check.',
|
|
1343
|
+
'Concurrency or cursor/state ownership is planned without deterministic race checks.',
|
|
1344
|
+
'Restart, replay, or crash recovery is claimed without restart/crash-window evidence.',
|
|
1345
|
+
'An external provider is used without failure-injection and hidden-default verification.',
|
|
1346
|
+
'A guarded failure mode lacks a hostile counterexample, expected safe result, or exact executable evidence command.',
|
|
1347
|
+
'The safe risk slice crosses persistence, recovery orchestration, and executable composition and should be split.',
|
|
1348
|
+
],
|
|
830
1349
|
};
|
|
831
1350
|
}
|
|
832
1351
|
if (role === 'independent-reviewer') {
|
|
833
1352
|
return {
|
|
834
1353
|
...common,
|
|
835
|
-
objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files
|
|
1354
|
+
objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.${planRiskReviewScope}`,
|
|
836
1355
|
inputs: [...common.inputs, ...(task.evidenceHash ? [path.join(root, 'evidence.json')] : [])],
|
|
837
1356
|
resultSchema: {
|
|
838
1357
|
type: 'object',
|
|
@@ -842,28 +1361,65 @@ export class WorkflowService {
|
|
|
842
1361
|
...common.stopConditions,
|
|
843
1362
|
'The checkout is dirty or the evidence does not identify the reviewed commit.',
|
|
844
1363
|
'Read-only reviewer isolation cannot be established; return unverified.',
|
|
1364
|
+
'High-risk behavior lacks populated-upgrade, race, restart/replay, crash-window, or provider-failure evidence required by the Plan.',
|
|
845
1365
|
],
|
|
846
1366
|
};
|
|
847
1367
|
}
|
|
848
1368
|
return {
|
|
849
1369
|
...common,
|
|
850
|
-
objective: `Coordinate the next valid state transition for ${task.title}
|
|
1370
|
+
objective: `Coordinate the next valid state transition for ${task.title}.${planRiskReviewScope}`,
|
|
851
1371
|
resultSchema: { type: 'object', required: ['recommendedAction', 'reason'] },
|
|
852
1372
|
};
|
|
853
1373
|
}
|
|
854
|
-
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes) {
|
|
1374
|
+
completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
|
|
855
1375
|
const context = this.context(repository);
|
|
856
|
-
context.locks.heartbeat(taskId, writerToken);
|
|
1376
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1377
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
857
1378
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
858
1379
|
assertExpectedRevision(task, expectedRevision);
|
|
1380
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
859
1381
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
860
1382
|
const step = requireStep(task, stepId);
|
|
861
1383
|
if (task.status !== 'in_progress' || step.status !== 'in_progress') {
|
|
862
1384
|
throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
|
|
863
1385
|
}
|
|
1386
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1387
|
+
const reviewRequired = planRiskAudit
|
|
1388
|
+
? isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
|
|
1389
|
+
: false;
|
|
1390
|
+
if (reviewRequired && step.evidence) {
|
|
1391
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1392
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
1393
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires a clean Task checkout.', {
|
|
1394
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1398
|
+
if (currentHead !== step.evidence.commitSha) {
|
|
1399
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires HEAD to remain at the recorded completion commit.', {
|
|
1400
|
+
stepId,
|
|
1401
|
+
expectedCommit: step.evidence.commitSha,
|
|
1402
|
+
actualCommit: currentHead,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1406
|
+
if (!currentCycle.pendingEvent && !currentCycle.latestMatchingEvent) {
|
|
1407
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1408
|
+
}
|
|
1409
|
+
return task;
|
|
1410
|
+
}
|
|
1411
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
864
1412
|
const checks = runChecks(context.identity.repositoryRoot, step.checks);
|
|
865
1413
|
const failed = checks.filter((check) => !check.passed);
|
|
866
1414
|
if (failed.length > 0) {
|
|
1415
|
+
if (reviewRequired && planRiskAudit) {
|
|
1416
|
+
appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
|
|
1417
|
+
completionCommit: headCommit(context.identity.repositoryRoot),
|
|
1418
|
+
checksHash: sha256Hex(canonicalJsonStringify(checks)),
|
|
1419
|
+
reviewEventHash: null,
|
|
1420
|
+
reviewerAttestationHash: null,
|
|
1421
|
+
}, planRiskAudit, this.now());
|
|
1422
|
+
}
|
|
867
1423
|
const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
|
|
868
1424
|
return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
|
|
869
1425
|
}
|
|
@@ -875,6 +1431,18 @@ export class WorkflowService {
|
|
|
875
1431
|
checks,
|
|
876
1432
|
notes,
|
|
877
1433
|
};
|
|
1434
|
+
if (reviewRequired) {
|
|
1435
|
+
const saved = this.store.writeTask({
|
|
1436
|
+
...task,
|
|
1437
|
+
status: 'in_progress',
|
|
1438
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
1439
|
+
? { ...candidate, status: 'in_progress', evidence }
|
|
1440
|
+
: candidate),
|
|
1441
|
+
blockReason: null,
|
|
1442
|
+
}, expectedRevision);
|
|
1443
|
+
ensurePendingStrictStepReview(this.store, saved, stepId, this.now());
|
|
1444
|
+
return saved;
|
|
1445
|
+
}
|
|
878
1446
|
const completedIds = new Set([
|
|
879
1447
|
...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
|
|
880
1448
|
stepId,
|
|
@@ -889,11 +1457,117 @@ export class WorkflowService {
|
|
|
889
1457
|
});
|
|
890
1458
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
891
1459
|
}
|
|
892
|
-
|
|
1460
|
+
reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
|
|
893
1461
|
const context = this.context(repository);
|
|
894
|
-
context.
|
|
1462
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
895
1463
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
896
1464
|
assertExpectedRevision(task, expectedRevision);
|
|
1465
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
1466
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1467
|
+
if (task.status !== 'in_progress' && task.status !== 'blocked') {
|
|
1468
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
1469
|
+
}
|
|
1470
|
+
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
1471
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1472
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1473
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
|
|
1474
|
+
taskId: task.id,
|
|
1475
|
+
stepId,
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
const step = requireStep(task, stepId);
|
|
1479
|
+
if (step.status !== 'in_progress' || !step.evidence) {
|
|
1480
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires ${stepId} to remain in progress with canonical evidence.`, {
|
|
1481
|
+
taskId: task.id,
|
|
1482
|
+
stepId,
|
|
1483
|
+
stepStatus: step.status,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
// A strict review is bound to the immutable completion commit, Task Plan, and the
|
|
1487
|
+
// Knowledge Map revision that authorized the Step. Project knowledge commonly becomes
|
|
1488
|
+
// stale because the completion commit itself changes a classified source. Requiring the
|
|
1489
|
+
// *current* map here creates a deadlock: review cannot run before rebind, while rebind is
|
|
1490
|
+
// intentionally forbidden until the in-progress Step becomes terminal. The pending
|
|
1491
|
+
// strict-review binding below validates the historical Task context; a current-map rebind
|
|
1492
|
+
// remains mandatory after the review finalizes the Step and before another Step can run.
|
|
1493
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1494
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
1495
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires a clean Task checkout.', {
|
|
1496
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1500
|
+
if (currentHead !== step.evidence.commitSha) {
|
|
1501
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires HEAD to remain at the recorded completion commit.', {
|
|
1502
|
+
taskId: task.id,
|
|
1503
|
+
stepId,
|
|
1504
|
+
expectedCommit: step.evidence.commitSha,
|
|
1505
|
+
actualCommit: currentHead,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1509
|
+
if (task.status === 'blocked') {
|
|
1510
|
+
if (currentCycle.resolution !== 'unverified') {
|
|
1511
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review retry requires ${stepId} to be blocked by an unverified prior cycle.`, {
|
|
1512
|
+
taskId: task.id,
|
|
1513
|
+
stepId,
|
|
1514
|
+
resolution: currentCycle.resolution,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
|
|
1518
|
+
}
|
|
1519
|
+
else if (currentCycle.resolution !== 'pending') {
|
|
1520
|
+
if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
|
|
1521
|
+
if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
|
|
1522
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
|
|
1523
|
+
taskId: task.id,
|
|
1524
|
+
stepId,
|
|
1525
|
+
resolution: currentCycle.resolution,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
|
|
1529
|
+
completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
|
|
1530
|
+
checksHash: currentCycle.reviewRecordedEvent.checksHash,
|
|
1531
|
+
reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
|
|
1532
|
+
reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
|
|
1533
|
+
}, planRiskAudit, this.now());
|
|
1534
|
+
}
|
|
1535
|
+
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
1536
|
+
}
|
|
1537
|
+
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1538
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
1539
|
+
const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
|
|
1540
|
+
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
1541
|
+
review: launch.review,
|
|
1542
|
+
reviewerOutputHash: launch.reviewerOutputHash,
|
|
1543
|
+
reviewedCommit: launch.reviewedCommit,
|
|
1544
|
+
isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
|
|
1545
|
+
}, this.now());
|
|
1546
|
+
if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
|
|
1547
|
+
const reviewRecordedEvent = recorded.reviewRecordedEvent;
|
|
1548
|
+
const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
|
|
1549
|
+
if (!reviewRecordedEvent || !reviewerAttestationEvent) {
|
|
1550
|
+
throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review remediation recording requires review and attestation sidecars.', {
|
|
1551
|
+
taskId: task.id,
|
|
1552
|
+
stepId,
|
|
1553
|
+
resolution: recorded.resolution,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
|
|
1557
|
+
completionCommit: reviewRecordedEvent.completionCommit,
|
|
1558
|
+
checksHash: reviewRecordedEvent.checksHash,
|
|
1559
|
+
reviewEventHash: reviewRecordedEvent.eventHash,
|
|
1560
|
+
reviewerAttestationHash: reviewerAttestationEvent.eventHash,
|
|
1561
|
+
}, planRiskAudit, this.now());
|
|
1562
|
+
}
|
|
1563
|
+
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
|
|
1564
|
+
}
|
|
1565
|
+
submitTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1566
|
+
const context = this.context(repository);
|
|
1567
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1568
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1569
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1570
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
897
1571
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
898
1572
|
if (!['in_progress', 'ready'].includes(task.status)) {
|
|
899
1573
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot be submitted while ${task.status}.`);
|
|
@@ -920,10 +1594,13 @@ export class WorkflowService {
|
|
|
920
1594
|
this.store.publishArtifact(stagedEvidence);
|
|
921
1595
|
return saved;
|
|
922
1596
|
}
|
|
923
|
-
recordReview(repository, taskId, expectedRevision, review) {
|
|
1597
|
+
recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
|
|
924
1598
|
const { identity } = this.context(repository);
|
|
1599
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
925
1600
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
926
1601
|
assertExpectedRevision(task, expectedRevision);
|
|
1602
|
+
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1603
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
927
1604
|
const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
|
|
928
1605
|
if (task.status !== 'validating' && !retryingUnverified) {
|
|
929
1606
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for review.`);
|
|
@@ -947,7 +1624,11 @@ export class WorkflowService {
|
|
|
947
1624
|
if (recorded.findings.length > 0) {
|
|
948
1625
|
const file = path.join(this.store.taskRoot(task.projectId, task.id), 'findings.jsonl');
|
|
949
1626
|
for (const finding of recorded.findings) {
|
|
950
|
-
appendFileSync(file, `${JSON.stringify({
|
|
1627
|
+
appendFileSync(file, `${JSON.stringify({
|
|
1628
|
+
...finding,
|
|
1629
|
+
reviewer: recorded.reviewer,
|
|
1630
|
+
recordedAt: recorded.recordedAt,
|
|
1631
|
+
})}\n`, {
|
|
951
1632
|
encoding: 'utf8',
|
|
952
1633
|
mode: 0o600,
|
|
953
1634
|
});
|
|
@@ -955,6 +1636,59 @@ export class WorkflowService {
|
|
|
955
1636
|
}
|
|
956
1637
|
return saved;
|
|
957
1638
|
}
|
|
1639
|
+
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
1640
|
+
const { identity } = this.context(repository);
|
|
1641
|
+
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
1642
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
1643
|
+
assertExpectedRevision(task, expectedRevision);
|
|
1644
|
+
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1645
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1646
|
+
if (task.status !== 'needs_fix' && task.status !== 'blocked') {
|
|
1647
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
1648
|
+
}
|
|
1649
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1650
|
+
if (!planRiskAudit) {
|
|
1651
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
|
|
1652
|
+
taskId,
|
|
1653
|
+
stepId,
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (!task.planHash) {
|
|
1657
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no current Plan hash for corrective decision recording.`, {
|
|
1658
|
+
taskId,
|
|
1659
|
+
stepId,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
const step = requireStep(task, stepId);
|
|
1663
|
+
if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1664
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
|
|
1665
|
+
taskId,
|
|
1666
|
+
stepId,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
if (task.status === 'needs_fix') {
|
|
1670
|
+
if (step.status !== 'failed') {
|
|
1671
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
|
|
1672
|
+
taskId,
|
|
1673
|
+
stepId,
|
|
1674
|
+
stepStatus: step.status,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
|
|
1680
|
+
if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
|
|
1681
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
|
|
1682
|
+
taskId,
|
|
1683
|
+
stepId,
|
|
1684
|
+
stepStatus: step.status,
|
|
1685
|
+
resolution: currentCycle.resolution,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
1690
|
+
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
1691
|
+
}
|
|
958
1692
|
setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
|
|
959
1693
|
const { identity } = this.context(repository);
|
|
960
1694
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
@@ -1059,10 +1793,12 @@ export class WorkflowService {
|
|
|
1059
1793
|
this.store.publishArtifact(stagedEvidence);
|
|
1060
1794
|
return saved;
|
|
1061
1795
|
}
|
|
1062
|
-
mergeTask(repository, taskId, expectedRevision, writerToken) {
|
|
1796
|
+
mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1063
1797
|
const context = this.context(repository);
|
|
1064
1798
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1065
1799
|
assertExpectedRevision(task, expectedRevision);
|
|
1800
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1801
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
1066
1802
|
if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
|
|
1067
1803
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for a local merge.`);
|
|
1068
1804
|
}
|
|
@@ -1079,10 +1815,12 @@ export class WorkflowService {
|
|
|
1079
1815
|
context.locks.release(taskId, writerToken);
|
|
1080
1816
|
return saved;
|
|
1081
1817
|
}
|
|
1082
|
-
confirmExternalMerge(repository, taskId, expectedRevision, writerToken) {
|
|
1818
|
+
confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
|
|
1083
1819
|
const context = this.context(repository);
|
|
1084
1820
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
1085
1821
|
assertExpectedRevision(task, expectedRevision);
|
|
1822
|
+
const lease = context.locks.heartbeat(taskId, writerToken);
|
|
1823
|
+
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
|
|
1086
1824
|
if (task.status !== 'ready_to_merge' || task.workspaceOwner !== 'external') {
|
|
1087
1825
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting external merge confirmation.`);
|
|
1088
1826
|
}
|
|
@@ -1096,12 +1834,194 @@ export class WorkflowService {
|
|
|
1096
1834
|
}
|
|
1097
1835
|
status(repository) {
|
|
1098
1836
|
const { identity } = this.context(repository);
|
|
1837
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
1838
|
+
const rawMilestones = this.store.listMilestones(identity.projectId);
|
|
1839
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
|
|
1099
1840
|
return {
|
|
1100
1841
|
projectId: identity.projectId,
|
|
1101
1842
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
1102
|
-
tasks
|
|
1103
|
-
milestones: this.
|
|
1843
|
+
tasks,
|
|
1844
|
+
milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
|
|
1104
1845
|
delegations: this.store.listDelegations(identity.projectId),
|
|
1846
|
+
adoption,
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
recoverMilestoneTransactions(repository) {
|
|
1850
|
+
const { identity } = this.context(repository);
|
|
1851
|
+
const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
|
|
1852
|
+
return {
|
|
1853
|
+
projectId: identity.projectId,
|
|
1854
|
+
recoveredMilestoneIds: milestones.map((milestone) => milestone.id),
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
prepareAdoption(repository) {
|
|
1858
|
+
const { identity } = this.context(repository);
|
|
1859
|
+
return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
|
|
1860
|
+
}
|
|
1861
|
+
applyAdoption(repository, actor, confirmationCode) {
|
|
1862
|
+
const context = this.context(repository);
|
|
1863
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1864
|
+
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
1865
|
+
return applyAdoptionPosture(this.store, {
|
|
1866
|
+
projectId: context.identity.projectId,
|
|
1867
|
+
repositoryIdentity: context.identity,
|
|
1868
|
+
packageVersion: PACKAGE_VERSION,
|
|
1869
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1870
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1871
|
+
tasks,
|
|
1872
|
+
milestones,
|
|
1873
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
1874
|
+
blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
|
|
1875
|
+
now: this.now(),
|
|
1876
|
+
}, actor, confirmationCode);
|
|
1877
|
+
}
|
|
1878
|
+
updatePreflight(repository) {
|
|
1879
|
+
const context = this.context(repository);
|
|
1880
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1881
|
+
const milestones = context.store.listMilestones(context.identity.projectId);
|
|
1882
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1883
|
+
const runningSteps = tasks.flatMap((task) => task.steps
|
|
1884
|
+
.filter((step) => step.status === 'in_progress')
|
|
1885
|
+
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
1886
|
+
const leases = context.locks.list();
|
|
1887
|
+
const now = this.now().getTime();
|
|
1888
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1889
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
1890
|
+
const blockers = [
|
|
1891
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1892
|
+
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
1893
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1894
|
+
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
1895
|
+
];
|
|
1896
|
+
const adoption = buildAdoptionPreparation({
|
|
1897
|
+
projectId: context.identity.projectId,
|
|
1898
|
+
repositoryIdentity: context.identity,
|
|
1899
|
+
packageVersion: PACKAGE_VERSION,
|
|
1900
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1901
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1902
|
+
tasks,
|
|
1903
|
+
milestones,
|
|
1904
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
|
|
1905
|
+
blockers,
|
|
1906
|
+
now: this.now(),
|
|
1907
|
+
}, readCurrentAdoptionPosture(this.store, context.identity.projectId));
|
|
1908
|
+
return {
|
|
1909
|
+
safe: blockers.length === 0,
|
|
1910
|
+
packageVersion: PACKAGE_VERSION,
|
|
1911
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1912
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
1913
|
+
projectId: context.identity.projectId,
|
|
1914
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
1915
|
+
clean: dirty.length === 0,
|
|
1916
|
+
changedFiles: dirty,
|
|
1917
|
+
runningSteps,
|
|
1918
|
+
activeLeases,
|
|
1919
|
+
staleLeases,
|
|
1920
|
+
blockers,
|
|
1921
|
+
adoption,
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
alpha6StrictReviewRescuePreflight(repository) {
|
|
1925
|
+
const context = this.context(repository);
|
|
1926
|
+
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
1927
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1928
|
+
const leases = context.locks.list();
|
|
1929
|
+
const now = this.now().getTime();
|
|
1930
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1931
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
1932
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1933
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1934
|
+
const pendingCandidates = [];
|
|
1935
|
+
const malformedCandidates = [];
|
|
1936
|
+
for (const task of tasks) {
|
|
1937
|
+
if (task.status !== 'in_progress')
|
|
1938
|
+
continue;
|
|
1939
|
+
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
1940
|
+
if (!planRiskAudit)
|
|
1941
|
+
continue;
|
|
1942
|
+
for (const step of task.steps) {
|
|
1943
|
+
if (step.status !== 'in_progress' || !step.evidence)
|
|
1944
|
+
continue;
|
|
1945
|
+
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
1946
|
+
continue;
|
|
1947
|
+
try {
|
|
1948
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
1949
|
+
if (cycle.resolution !== 'pending')
|
|
1950
|
+
continue;
|
|
1951
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1952
|
+
let knowledgeBindingStale = false;
|
|
1953
|
+
try {
|
|
1954
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
1955
|
+
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
1956
|
+
knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
1957
|
+
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
1958
|
+
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
knowledgeBindingStale = true;
|
|
1962
|
+
}
|
|
1963
|
+
pendingCandidates.push({
|
|
1964
|
+
taskId: task.id,
|
|
1965
|
+
taskRevision: task.revision,
|
|
1966
|
+
stepId: step.id,
|
|
1967
|
+
completionCommit: step.evidence.commitSha,
|
|
1968
|
+
currentHead,
|
|
1969
|
+
knowledgeBindingStale,
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
catch (error) {
|
|
1973
|
+
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
|
|
1978
|
+
const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
|
|
1979
|
+
const blockers = [
|
|
1980
|
+
...(dependency.declared !== '2.0.0-alpha.6'
|
|
1981
|
+
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
1982
|
+
: []),
|
|
1983
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1984
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1985
|
+
...(pendingCandidates.length !== 1
|
|
1986
|
+
? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
|
|
1987
|
+
: []),
|
|
1988
|
+
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
1989
|
+
? ['HEAD does not match the pending strict review completion commit.']
|
|
1990
|
+
: []),
|
|
1991
|
+
...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
|
|
1992
|
+
? ['A stale writer lease exists outside the rescue Task.']
|
|
1993
|
+
: []),
|
|
1994
|
+
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
1995
|
+
];
|
|
1996
|
+
const eligible = blockers.length === 0 && candidate !== null;
|
|
1997
|
+
const requiredActions = eligible
|
|
1998
|
+
? [
|
|
1999
|
+
...staleLeases.map((lease) => ({
|
|
2000
|
+
action: 'locks repair',
|
|
2001
|
+
entityId: lease.entityId,
|
|
2002
|
+
})),
|
|
2003
|
+
{
|
|
2004
|
+
action: 'task step-review',
|
|
2005
|
+
taskId: candidate.taskId,
|
|
2006
|
+
stepId: candidate.stepId,
|
|
2007
|
+
expectedRevision: candidate.taskRevision,
|
|
2008
|
+
},
|
|
2009
|
+
]
|
|
2010
|
+
: [];
|
|
2011
|
+
return {
|
|
2012
|
+
eligible,
|
|
2013
|
+
mutationFree: true,
|
|
2014
|
+
runnerPackageVersion: PACKAGE_VERSION,
|
|
2015
|
+
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
2016
|
+
declaredProjectPackageVersion: dependency.declared,
|
|
2017
|
+
projectId: context.identity.projectId,
|
|
2018
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
2019
|
+
clean: dirty.length === 0,
|
|
2020
|
+
activeLeases,
|
|
2021
|
+
staleLeases,
|
|
2022
|
+
candidate,
|
|
2023
|
+
blockers,
|
|
2024
|
+
requiredActions,
|
|
1105
2025
|
};
|
|
1106
2026
|
}
|
|
1107
2027
|
scanKnowledgeMap(repository) {
|
|
@@ -1128,11 +2048,14 @@ export class WorkflowService {
|
|
|
1128
2048
|
return inspectKnowledgeMap(current, scanned);
|
|
1129
2049
|
}
|
|
1130
2050
|
approveKnowledgeMap(repository, expectedRevision, selections, actor, delegationGrantId = null) {
|
|
2051
|
+
return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
|
|
2052
|
+
}
|
|
2053
|
+
approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
|
|
1131
2054
|
const { identity } = this.context(repository);
|
|
1132
2055
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
1133
2056
|
assertExpectedRevision(current, expectedRevision);
|
|
1134
2057
|
const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
1135
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected);
|
|
2058
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected, autonomyTaskId);
|
|
1136
2059
|
const approved = applyKnowledgeSelections(inspected, selections, actor, this.now());
|
|
1137
2060
|
if (!approved.approval) {
|
|
1138
2061
|
throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval was not recorded.');
|
|
@@ -1177,12 +2100,44 @@ export class WorkflowService {
|
|
|
1177
2100
|
}
|
|
1178
2101
|
next(repository) {
|
|
1179
2102
|
const status = this.status(repository);
|
|
2103
|
+
const adoptionInfo = {
|
|
2104
|
+
status: status.adoption.status,
|
|
2105
|
+
safe: status.adoption.safe,
|
|
2106
|
+
blockers: status.adoption.blockers,
|
|
2107
|
+
baselineDigestHash: status.adoption.baselineDigestHash,
|
|
2108
|
+
sidecarBaselineHash: status.adoption.sidecarBaselineHash,
|
|
2109
|
+
confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
|
|
2110
|
+
confirmationCode: status.adoption.confirmationCode,
|
|
2111
|
+
currentEventType: status.adoption.currentPosture?.eventType ?? null,
|
|
2112
|
+
};
|
|
2113
|
+
if (status.adoption.status !== 'applied') {
|
|
2114
|
+
return {
|
|
2115
|
+
scope: 'project',
|
|
2116
|
+
projectId: status.projectId,
|
|
2117
|
+
status: status.adoption.status,
|
|
2118
|
+
action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2119
|
+
adoption: adoptionInfo,
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
|
|
1180
2123
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
1181
2124
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
1182
2125
|
const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
1183
2126
|
if (activeTask) {
|
|
2127
|
+
const activePlanRiskAudit = activeTask.planHash
|
|
2128
|
+
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2129
|
+
: null;
|
|
2130
|
+
const taskNext = activeTask.status === 'planning'
|
|
2131
|
+
? null
|
|
2132
|
+
: this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2133
|
+
// Pending strict review is completion work for the already-recorded commit. It must
|
|
2134
|
+
// outrank Project Knowledge refresh/rebind; otherwise the active Step makes the rebind
|
|
2135
|
+
// returned by `next` impossible to execute.
|
|
2136
|
+
if (taskNext?.action === 'task step-review') {
|
|
2137
|
+
return withAdoption(taskNext);
|
|
2138
|
+
}
|
|
1184
2139
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
1185
|
-
return
|
|
2140
|
+
return withAdoption(taskNext);
|
|
1186
2141
|
}
|
|
1187
2142
|
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
1188
2143
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
@@ -1190,21 +2145,37 @@ export class WorkflowService {
|
|
|
1190
2145
|
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
1191
2146
|
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
|
|
1192
2147
|
: [];
|
|
1193
|
-
|
|
2148
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
|
|
2149
|
+
const contextRefreshOptions = action === 'project-memory reconcile'
|
|
2150
|
+
&& storedKnowledgeMap.status === 'active'
|
|
2151
|
+
&& storedKnowledgeMap.approval
|
|
2152
|
+
&& isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
|
|
2153
|
+
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
|
|
2154
|
+
: [];
|
|
2155
|
+
return withAdoption({
|
|
1194
2156
|
scope: 'project-memory',
|
|
1195
2157
|
status: knowledgeMap.status,
|
|
1196
2158
|
action,
|
|
1197
2159
|
revision: knowledgeMap.revision,
|
|
1198
2160
|
taskId: activeTask.id,
|
|
1199
2161
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1200
|
-
|
|
2162
|
+
...(contextRefreshOptions.length > 0
|
|
2163
|
+
? {
|
|
2164
|
+
contextRefresh: {
|
|
2165
|
+
action: 'task context-refresh',
|
|
2166
|
+
mode: 'delegated-content-only',
|
|
2167
|
+
options: contextRefreshOptions,
|
|
2168
|
+
},
|
|
2169
|
+
}
|
|
2170
|
+
: {}),
|
|
2171
|
+
});
|
|
1201
2172
|
}
|
|
1202
2173
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
1203
2174
|
if (taskCanRebindKnowledge(activeTask) &&
|
|
1204
2175
|
activeTask.planHash &&
|
|
1205
2176
|
(activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
|
|
1206
2177
|
activeTask.knowledgeMapHash !== knowledgeMapHash)) {
|
|
1207
|
-
return {
|
|
2178
|
+
return withAdoption({
|
|
1208
2179
|
scope: 'task',
|
|
1209
2180
|
id: activeTask.id,
|
|
1210
2181
|
status: activeTask.status,
|
|
@@ -1212,9 +2183,9 @@ export class WorkflowService {
|
|
|
1212
2183
|
revision: activeTask.revision,
|
|
1213
2184
|
knowledgeMapRevision: knowledgeMap.revision,
|
|
1214
2185
|
knowledgeMapHash,
|
|
1215
|
-
};
|
|
2186
|
+
});
|
|
1216
2187
|
}
|
|
1217
|
-
return this.nextForTaskWithDelegation(status.projectId, activeTask);
|
|
2188
|
+
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
1218
2189
|
}
|
|
1219
2190
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
1220
2191
|
if (activeMilestone) {
|
|
@@ -1232,17 +2203,17 @@ export class WorkflowService {
|
|
|
1232
2203
|
evidence.headCommit !== currentHead ||
|
|
1233
2204
|
resultHash !== activeMilestone.resultHash ||
|
|
1234
2205
|
evidenceHash !== activeMilestone.evidenceHash) {
|
|
1235
|
-
return {
|
|
2206
|
+
return withAdoption({
|
|
1236
2207
|
scope: 'milestone',
|
|
1237
2208
|
id: activeMilestone.id,
|
|
1238
2209
|
status: activeMilestone.status,
|
|
1239
2210
|
action: 'restore clean base, then milestone validate',
|
|
1240
2211
|
revision: activeMilestone.revision,
|
|
1241
2212
|
reason: 'final validation evidence is stale',
|
|
1242
|
-
};
|
|
2213
|
+
});
|
|
1243
2214
|
}
|
|
1244
2215
|
const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
|
|
1245
|
-
return {
|
|
2216
|
+
return withAdoption({
|
|
1246
2217
|
scope: 'milestone',
|
|
1247
2218
|
id: activeMilestone.id,
|
|
1248
2219
|
status: activeMilestone.status,
|
|
@@ -1251,34 +2222,55 @@ export class WorkflowService {
|
|
|
1251
2222
|
: 'await explicit human final acceptance',
|
|
1252
2223
|
revision: activeMilestone.revision,
|
|
1253
2224
|
requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
|
|
2225
|
+
completionPolicy: {
|
|
2226
|
+
resultEvidenceAndHeadMustMatch: true,
|
|
2227
|
+
finalAcceptanceTransition: 'milestone.final_accept',
|
|
2228
|
+
delegatedActorMustRemainDelegate: true,
|
|
2229
|
+
},
|
|
1254
2230
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1255
|
-
};
|
|
2231
|
+
});
|
|
1256
2232
|
}
|
|
1257
2233
|
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
1258
2234
|
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
1259
2235
|
: [];
|
|
1260
|
-
return {
|
|
2236
|
+
return withAdoption({
|
|
1261
2237
|
scope: 'milestone',
|
|
1262
2238
|
id: activeMilestone.id,
|
|
1263
2239
|
status: activeMilestone.status,
|
|
1264
2240
|
action: nextForMilestone(activeMilestone),
|
|
1265
2241
|
revision: activeMilestone.revision,
|
|
2242
|
+
...(activeMilestone.status === 'active'
|
|
2243
|
+
? {
|
|
2244
|
+
completionPolicy: {
|
|
2245
|
+
requiredTasksMustBeMerged: true,
|
|
2246
|
+
validationAction: 'milestone validate',
|
|
2247
|
+
finalAcceptanceTransition: 'milestone.final_accept',
|
|
2248
|
+
delegatedActorMustRemainDelegate: true,
|
|
2249
|
+
requiredTasks: activeMilestone.memberships
|
|
2250
|
+
.filter((membership) => membership.disposition === 'required')
|
|
2251
|
+
.map((membership) => {
|
|
2252
|
+
const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
|
|
2253
|
+
return { taskId: membership.taskId, status: task?.status ?? 'missing' };
|
|
2254
|
+
}),
|
|
2255
|
+
},
|
|
2256
|
+
}
|
|
2257
|
+
: {}),
|
|
1266
2258
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1267
|
-
};
|
|
2259
|
+
});
|
|
1268
2260
|
}
|
|
1269
2261
|
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
1270
2262
|
if (activeDiscovery) {
|
|
1271
|
-
return {
|
|
2263
|
+
return withAdoption({
|
|
1272
2264
|
scope: 'discovery',
|
|
1273
2265
|
id: activeDiscovery.id,
|
|
1274
2266
|
status: activeDiscovery.status,
|
|
1275
2267
|
action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
|
|
1276
2268
|
readiness: assessDiscovery(activeDiscovery),
|
|
1277
|
-
};
|
|
2269
|
+
});
|
|
1278
2270
|
}
|
|
1279
|
-
return { scope: 'repository', status: 'idle', action: 'discovery start' };
|
|
2271
|
+
return withAdoption({ scope: 'repository', status: 'idle', action: 'discovery start' });
|
|
1280
2272
|
}
|
|
1281
|
-
delegatedAuthorization(projectId, grantId, transition, actor, target) {
|
|
2273
|
+
delegatedAuthorization(projectId, grantId, transition, actor, target, autonomyTaskId = null) {
|
|
1282
2274
|
if (!grantId) {
|
|
1283
2275
|
if (actor.trim().startsWith('agent:')) {
|
|
1284
2276
|
throw new WorkflowError('TRANSITION_BLOCKED', 'An agent actor requires an explicit delegated approval grant.');
|
|
@@ -1286,9 +2278,102 @@ export class WorkflowService {
|
|
|
1286
2278
|
return null;
|
|
1287
2279
|
}
|
|
1288
2280
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
2281
|
+
if (grant.scope.kind === 'milestone') {
|
|
2282
|
+
const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
|
|
2283
|
+
.find((event) => event.delegationGrantId === grant.id) ?? null;
|
|
2284
|
+
if (autonomyContract) {
|
|
2285
|
+
const contextTask = target.kind === 'knowledge-map' && autonomyTaskId
|
|
2286
|
+
? this.store.readTask(projectId, autonomyTaskId)
|
|
2287
|
+
: null;
|
|
2288
|
+
const milestone = target.kind === 'milestone'
|
|
2289
|
+
? target
|
|
2290
|
+
: target.kind === 'task' && target.milestoneId === grant.scope.id
|
|
2291
|
+
? this.store.readMilestone(projectId, grant.scope.id)
|
|
2292
|
+
: contextTask?.milestoneId === grant.scope.id
|
|
2293
|
+
? this.store.readMilestone(projectId, grant.scope.id)
|
|
2294
|
+
: null;
|
|
2295
|
+
if (!milestone) {
|
|
2296
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant cannot authorize this target.');
|
|
2297
|
+
}
|
|
2298
|
+
const activeAutonomy = requireActiveMilestoneAutonomy({
|
|
2299
|
+
store: this.store,
|
|
2300
|
+
projectId,
|
|
2301
|
+
milestone,
|
|
2302
|
+
actor,
|
|
2303
|
+
now: this.now(),
|
|
2304
|
+
});
|
|
2305
|
+
if (activeAutonomy.grant.id !== grant.id) {
|
|
2306
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
|
|
2307
|
+
}
|
|
2308
|
+
if (target.kind === 'knowledge-map') {
|
|
2309
|
+
if (transition !== 'project_memory.approve'
|
|
2310
|
+
|| !contextTask
|
|
2311
|
+
|| !taskCanRebindKnowledge(contextTask)) {
|
|
2312
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy may approve Project Memory only inside a content-only Task context refresh.');
|
|
2313
|
+
}
|
|
2314
|
+
if (!grant.transitions.includes(transition)) {
|
|
2315
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not allow ${transition}.`);
|
|
2316
|
+
}
|
|
2317
|
+
return {
|
|
2318
|
+
grantId: grant.id,
|
|
2319
|
+
policyHash: grant.policyHash,
|
|
2320
|
+
principal: grant.principal,
|
|
2321
|
+
delegate: grant.delegate,
|
|
2322
|
+
transition,
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
1289
2327
|
return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
|
|
1290
2328
|
}
|
|
1291
|
-
|
|
2329
|
+
assertHandoffGrant(projectId, task, targetActor, grantId) {
|
|
2330
|
+
const grant = this.store.readDelegation(projectId, grantId);
|
|
2331
|
+
const policyHash = hashDelegationPolicy(projectId, {
|
|
2332
|
+
principal: grant.principal,
|
|
2333
|
+
delegate: grant.delegate,
|
|
2334
|
+
scope: grant.scope,
|
|
2335
|
+
transitions: grant.transitions,
|
|
2336
|
+
expiresAt: grant.expiresAt,
|
|
2337
|
+
});
|
|
2338
|
+
const scopeMatches = grant.scope.kind === 'project'
|
|
2339
|
+
|| (grant.scope.kind === 'task' && grant.scope.id === task.id)
|
|
2340
|
+
|| (grant.scope.kind === 'milestone' && grant.scope.id === task.milestoneId);
|
|
2341
|
+
if (grant.status !== 'active'
|
|
2342
|
+
|| grant.policyHash !== policyHash
|
|
2343
|
+
|| Date.parse(grant.expiresAt) <= this.now().getTime()
|
|
2344
|
+
|| grant.delegate !== targetActor.trim()
|
|
2345
|
+
|| !scopeMatches) {
|
|
2346
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} cannot be bound to this C1 handoff.`, {
|
|
2347
|
+
grantId,
|
|
2348
|
+
taskId: task.id,
|
|
2349
|
+
targetActor: targetActor.trim(),
|
|
2350
|
+
grantStatus: grant.status,
|
|
2351
|
+
expectedPolicyHash: policyHash,
|
|
2352
|
+
actualPolicyHash: grant.policyHash,
|
|
2353
|
+
grantDelegate: grant.delegate,
|
|
2354
|
+
grantScope: grant.scope,
|
|
2355
|
+
grantExpiresAt: grant.expiresAt,
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
if (grant.scope.kind === 'milestone') {
|
|
2359
|
+
const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
|
|
2360
|
+
.find((event) => event.delegationGrantId === grant.id) ?? null;
|
|
2361
|
+
if (autonomyContract) {
|
|
2362
|
+
const milestone = this.store.readMilestone(projectId, grant.scope.id);
|
|
2363
|
+
const activeAutonomy = requireActiveMilestoneAutonomy({
|
|
2364
|
+
store: this.store,
|
|
2365
|
+
projectId,
|
|
2366
|
+
milestone,
|
|
2367
|
+
actor: targetActor,
|
|
2368
|
+
now: this.now(),
|
|
2369
|
+
});
|
|
2370
|
+
if (activeAutonomy.grant.id !== grant.id) {
|
|
2371
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
|
|
1292
2377
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
1293
2378
|
? 'task.execution_authorize'
|
|
1294
2379
|
: task.status === 'awaiting_final_acceptance'
|
|
@@ -1297,14 +2382,130 @@ export class WorkflowService {
|
|
|
1297
2382
|
const delegatedApprovalOptions = transition
|
|
1298
2383
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
1299
2384
|
: [];
|
|
1300
|
-
|
|
1301
|
-
|
|
2385
|
+
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
2386
|
+
const guardedCorrectiveGate = task.status === 'needs_fix' && planRiskAudit
|
|
2387
|
+
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
|
|
2388
|
+
: null;
|
|
2389
|
+
const next = nextForTask(task);
|
|
2390
|
+
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2391
|
+
const strictStepReviewStep = planRiskAudit
|
|
2392
|
+
? task.steps.find((step) => {
|
|
2393
|
+
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
if (task.status !== 'blocked')
|
|
2397
|
+
return true;
|
|
2398
|
+
return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
|
|
2399
|
+
}) ?? null
|
|
2400
|
+
: null;
|
|
2401
|
+
const handoffOverlay = handoffPosture.state === 'pending'
|
|
2402
|
+
? {
|
|
2403
|
+
action: 'task claim',
|
|
2404
|
+
blockedAction: next.action,
|
|
2405
|
+
c1Handoff: {
|
|
2406
|
+
state: 'pending',
|
|
2407
|
+
targetActor: handoffPosture.targetActor,
|
|
2408
|
+
eventId: handoffPosture.latestEvent.eventId,
|
|
2409
|
+
},
|
|
2410
|
+
}
|
|
2411
|
+
: handoffPosture.state === 'claimed'
|
|
2412
|
+
? {
|
|
2413
|
+
c1Handoff: {
|
|
2414
|
+
state: 'claimed',
|
|
2415
|
+
claimant: handoffPosture.claimant,
|
|
2416
|
+
writerLeaseTokenBound: handoffPosture.writerLeaseTokenHash !== null,
|
|
2417
|
+
requiredActor: handoffPosture.claimant,
|
|
2418
|
+
},
|
|
2419
|
+
}
|
|
2420
|
+
: task.milestoneId && next.action === 'task start'
|
|
2421
|
+
? {
|
|
2422
|
+
action: 'task handoff-prepare',
|
|
2423
|
+
blockedAction: next.action,
|
|
2424
|
+
c1Handoff: {
|
|
2425
|
+
state: 'required',
|
|
2426
|
+
recommendedTargetActor: defaultTaskWorkerActor(task.id),
|
|
2427
|
+
targetActorDerived: true,
|
|
2428
|
+
},
|
|
2429
|
+
}
|
|
2430
|
+
: {};
|
|
2431
|
+
const result = {
|
|
2432
|
+
...next,
|
|
2433
|
+
...handoffOverlay,
|
|
2434
|
+
...(strictStepReviewStep
|
|
2435
|
+
? {
|
|
2436
|
+
action: 'task step-review',
|
|
2437
|
+
stepId: strictStepReviewStep.id,
|
|
2438
|
+
commitPolicy: {
|
|
2439
|
+
owner: 'workflow-core',
|
|
2440
|
+
manualCommitAllowed: false,
|
|
2441
|
+
completionAction: 'task step-review',
|
|
2442
|
+
},
|
|
2443
|
+
strictStepReview: {
|
|
2444
|
+
reviewRequired: true,
|
|
2445
|
+
completionCommit: strictStepReviewStep.evidence.commitSha,
|
|
2446
|
+
},
|
|
2447
|
+
}
|
|
2448
|
+
: {}),
|
|
2449
|
+
...(planRiskAudit
|
|
2450
|
+
? {
|
|
2451
|
+
planRiskAudit: {
|
|
2452
|
+
decision: planRiskAudit.audit.decision,
|
|
2453
|
+
scope: planRiskAudit.scope,
|
|
2454
|
+
postureEventType: planRiskAudit.postureEventType,
|
|
2455
|
+
reviewRequiredStepIds: planRiskAudit.reviewRequiredStepIds,
|
|
2456
|
+
},
|
|
2457
|
+
}
|
|
2458
|
+
: {}),
|
|
2459
|
+
...(task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2
|
|
2460
|
+
? {
|
|
2461
|
+
action: 'task plan-set with independent corrective audit',
|
|
2462
|
+
correctivePlanGate: {
|
|
2463
|
+
failedReviewAttempts,
|
|
2464
|
+
auditFileRequired: true,
|
|
2465
|
+
auditorMustDifferFromFailedReviewer: true,
|
|
2466
|
+
allowedDecision: 'continue-fix',
|
|
2467
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
2468
|
+
},
|
|
2469
|
+
}
|
|
2470
|
+
: {}),
|
|
2471
|
+
...(guardedCorrectiveGate
|
|
2472
|
+
? {
|
|
2473
|
+
action: 'task corrective-decision',
|
|
2474
|
+
stepId: guardedCorrectiveGate.stepId,
|
|
2475
|
+
correctiveDecisionGate: {
|
|
2476
|
+
attemptCount: guardedCorrectiveGate.attemptCount,
|
|
2477
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
2478
|
+
auditorDerived: true,
|
|
2479
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
2480
|
+
allowedDecision: 'continue-fix',
|
|
2481
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
2482
|
+
humanApprovalRequired: false,
|
|
2483
|
+
},
|
|
2484
|
+
}
|
|
2485
|
+
: {}),
|
|
1302
2486
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1303
2487
|
};
|
|
2488
|
+
if (handoffPosture.state === 'pending') {
|
|
2489
|
+
return {
|
|
2490
|
+
...result,
|
|
2491
|
+
action: 'task claim',
|
|
2492
|
+
blockedAction: next.action,
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
return result;
|
|
1304
2496
|
}
|
|
1305
2497
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
1306
2498
|
return this.store.listDelegations(projectId)
|
|
1307
|
-
.filter((grant) =>
|
|
2499
|
+
.filter((grant) => {
|
|
2500
|
+
try {
|
|
2501
|
+
return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
|
|
2502
|
+
}
|
|
2503
|
+
catch (error) {
|
|
2504
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
2505
|
+
return false;
|
|
2506
|
+
throw error;
|
|
2507
|
+
}
|
|
2508
|
+
})
|
|
1308
2509
|
.map((grant) => ({
|
|
1309
2510
|
grantId: grant.id,
|
|
1310
2511
|
principal: grant.principal,
|
|
@@ -1315,6 +2516,30 @@ export class WorkflowService {
|
|
|
1315
2516
|
policyHash: grant.policyHash,
|
|
1316
2517
|
}));
|
|
1317
2518
|
}
|
|
2519
|
+
delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
|
|
2520
|
+
return this.store.listDelegations(projectId)
|
|
2521
|
+
.filter((grant) => {
|
|
2522
|
+
try {
|
|
2523
|
+
return Boolean(this.delegatedAuthorization(projectId, grant.id, 'project_memory.approve', grant.delegate, knowledgeMap, task.id)
|
|
2524
|
+
&& this.delegatedAuthorization(projectId, grant.id, 'task.execution_authorize', grant.delegate, task));
|
|
2525
|
+
}
|
|
2526
|
+
catch (error) {
|
|
2527
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
2528
|
+
return false;
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
})
|
|
2532
|
+
.map((grant) => ({
|
|
2533
|
+
grantId: grant.id,
|
|
2534
|
+
principal: grant.principal,
|
|
2535
|
+
delegate: grant.delegate,
|
|
2536
|
+
transition: 'task.execution_authorize',
|
|
2537
|
+
transitions: ['project_memory.approve', 'task.execution_authorize'],
|
|
2538
|
+
scope: grant.scope,
|
|
2539
|
+
expiresAt: grant.expiresAt,
|
|
2540
|
+
policyHash: grant.policyHash,
|
|
2541
|
+
}));
|
|
2542
|
+
}
|
|
1318
2543
|
assertDelegationScopeExists(projectId, policy) {
|
|
1319
2544
|
if (policy.scope.kind === 'task') {
|
|
1320
2545
|
this.store.readTask(projectId, policy.scope.id);
|
|
@@ -1352,6 +2577,75 @@ export class WorkflowService {
|
|
|
1352
2577
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone authorization is stale.');
|
|
1353
2578
|
}
|
|
1354
2579
|
}
|
|
2580
|
+
readMilestoneCurrent(projectId, milestoneId) {
|
|
2581
|
+
return readMilestoneWithIntegrity(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
|
|
2582
|
+
}
|
|
2583
|
+
readMilestoneScopeChangeCurrent(projectId, milestoneId) {
|
|
2584
|
+
return readMilestoneForScopeChange(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
|
|
2585
|
+
}
|
|
2586
|
+
listMilestonesCurrent(projectId, posture = this.readCurrentAdoptionPostureStrict(projectId)) {
|
|
2587
|
+
const milestones = this.store.listMilestones(projectId);
|
|
2588
|
+
if (!posture)
|
|
2589
|
+
return milestones;
|
|
2590
|
+
const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
|
|
2591
|
+
return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
|
|
2592
|
+
? milestone
|
|
2593
|
+
: readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
|
|
2594
|
+
}
|
|
2595
|
+
prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
|
|
2596
|
+
validateMilestonePlan(plan);
|
|
2597
|
+
return prepareMilestoneScopeChangeCandidate({
|
|
2598
|
+
milestone,
|
|
2599
|
+
plan,
|
|
2600
|
+
readTask: (taskId) => this.store.readTask(projectId, taskId),
|
|
2601
|
+
actor,
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
|
|
2605
|
+
const currentLease = context.locks.inspect(taskId);
|
|
2606
|
+
if (!currentLease) {
|
|
2607
|
+
if (writerToken !== null) {
|
|
2608
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
2609
|
+
taskId,
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
if (writerToken === null) {
|
|
2615
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} requires the active writer token while a lease is present.`, {
|
|
2616
|
+
taskId,
|
|
2617
|
+
leaseOwner: currentLease.owner,
|
|
2618
|
+
expiresAt: currentLease.expiresAt,
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
return context.locks.heartbeat(taskId, writerToken);
|
|
2622
|
+
}
|
|
2623
|
+
proveOptionalWriterLeaseToken(context, taskId, writerToken) {
|
|
2624
|
+
const currentLease = context.locks.inspect(taskId);
|
|
2625
|
+
if (!currentLease) {
|
|
2626
|
+
if (writerToken !== null) {
|
|
2627
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
|
|
2628
|
+
taskId,
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
return null;
|
|
2632
|
+
}
|
|
2633
|
+
if (writerToken === null)
|
|
2634
|
+
return null;
|
|
2635
|
+
return context.locks.heartbeat(taskId, writerToken);
|
|
2636
|
+
}
|
|
2637
|
+
assertTaskMutationAllowedByHandoff(task, actor, writerToken) {
|
|
2638
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
2639
|
+
if (posture.state === 'none')
|
|
2640
|
+
return;
|
|
2641
|
+
if (!actor || !actor.trim()) {
|
|
2642
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has active C1 coordination and requires an explicit actor for mutation.`, {
|
|
2643
|
+
taskId: task.id,
|
|
2644
|
+
posture: posture.state,
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
assertTaskMutationAllowedByC1(this.store, task, { actor: actor.trim(), writerToken });
|
|
2648
|
+
}
|
|
1355
2649
|
readGraphBindingOrNull(projectId, graphKind) {
|
|
1356
2650
|
try {
|
|
1357
2651
|
return this.store.readGraphBinding(projectId, graphKind);
|
|
@@ -1374,6 +2668,93 @@ export class WorkflowService {
|
|
|
1374
2668
|
}
|
|
1375
2669
|
return inspected;
|
|
1376
2670
|
}
|
|
2671
|
+
strictReviewKnowledgeMap(repositoryRoot, task) {
|
|
2672
|
+
const stored = this.store.readKnowledgeMap(task.projectId);
|
|
2673
|
+
const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repositoryRoot, task.projectId, this.now()));
|
|
2674
|
+
if (inspected.status === 'active' && inspected.approval)
|
|
2675
|
+
return inspected;
|
|
2676
|
+
const storedHash = hashKnowledgeMap(stored);
|
|
2677
|
+
if (stored.status === 'active'
|
|
2678
|
+
&& stored.approval
|
|
2679
|
+
&& task.knowledgeMapRevision === stored.revision
|
|
2680
|
+
&& task.knowledgeMapHash === storedHash
|
|
2681
|
+
&& stored.approval.mapHash === storedHash) {
|
|
2682
|
+
return stored;
|
|
2683
|
+
}
|
|
2684
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review has neither a current active Knowledge Map nor its exact historical Task binding.', {
|
|
2685
|
+
taskId: task.id,
|
|
2686
|
+
inspectedStatus: inspected.status,
|
|
2687
|
+
taskKnowledgeMapRevision: task.knowledgeMapRevision,
|
|
2688
|
+
storedKnowledgeMapRevision: stored.revision,
|
|
2689
|
+
taskKnowledgeMapHash: task.knowledgeMapHash,
|
|
2690
|
+
storedKnowledgeMapHash: storedHash,
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
inspectAdoptionPreparation(identity, tasks, milestones) {
|
|
2694
|
+
const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
|
|
2695
|
+
const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
|
|
2696
|
+
if (currentPosture) {
|
|
2697
|
+
assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
|
|
2698
|
+
}
|
|
2699
|
+
return buildAdoptionPreparation({
|
|
2700
|
+
projectId: identity.projectId,
|
|
2701
|
+
repositoryIdentity: identity,
|
|
2702
|
+
packageVersion: PACKAGE_VERSION,
|
|
2703
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2704
|
+
stateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
2705
|
+
tasks,
|
|
2706
|
+
milestones,
|
|
2707
|
+
sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
|
|
2708
|
+
blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
|
|
2709
|
+
now: this.now(),
|
|
2710
|
+
}, currentPosture);
|
|
2711
|
+
}
|
|
2712
|
+
computeAdoptionBoundaryBlockers(identity, tasks, leases) {
|
|
2713
|
+
const dirty = changedFiles(identity.repositoryRoot);
|
|
2714
|
+
const runningSteps = tasks.flatMap((task) => task.steps
|
|
2715
|
+
.filter((step) => step.status === 'in_progress')
|
|
2716
|
+
.map((step) => ({ taskId: task.id, stepId: step.id })));
|
|
2717
|
+
const now = this.now().getTime();
|
|
2718
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
2719
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
2720
|
+
return [
|
|
2721
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
2722
|
+
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
2723
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
2724
|
+
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
2725
|
+
];
|
|
2726
|
+
}
|
|
2727
|
+
collectAdoptionSidecarInventory(projectId, tasks, milestones) {
|
|
2728
|
+
const inventory = [];
|
|
2729
|
+
const collect = (scope, ownerId, root) => {
|
|
2730
|
+
if (!existsSync(root))
|
|
2731
|
+
return;
|
|
2732
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
2733
|
+
if (entry.isSymbolicLink()) {
|
|
2734
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory rejects symlink entries: ${path.join(root, entry.name)}`);
|
|
2735
|
+
}
|
|
2736
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
2737
|
+
continue;
|
|
2738
|
+
const filePath = path.join(root, entry.name);
|
|
2739
|
+
const stat = lstatSync(filePath);
|
|
2740
|
+
if (!stat.isFile()) {
|
|
2741
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory requires regular files only: ${filePath}`);
|
|
2742
|
+
}
|
|
2743
|
+
inventory.push({
|
|
2744
|
+
scope,
|
|
2745
|
+
ownerId,
|
|
2746
|
+
sidecarName: entry.name,
|
|
2747
|
+
contentHash: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
collect('project', projectId, this.store.projectRoot(projectId));
|
|
2752
|
+
for (const task of tasks)
|
|
2753
|
+
collect('task', task.id, this.store.taskRoot(projectId, task.id));
|
|
2754
|
+
for (const milestone of milestones)
|
|
2755
|
+
collect('milestone', milestone.id, this.store.milestoneRoot(projectId, milestone.id));
|
|
2756
|
+
return inventory;
|
|
2757
|
+
}
|
|
1377
2758
|
assertKnowledgeMapBinding(repositoryRoot, projectId, revision, mapHash) {
|
|
1378
2759
|
const map = this.requireActiveKnowledgeMap(repositoryRoot, projectId);
|
|
1379
2760
|
const currentHash = hashKnowledgeMap(map);
|
|
@@ -1387,6 +2768,29 @@ export class WorkflowService {
|
|
|
1387
2768
|
}
|
|
1388
2769
|
return map;
|
|
1389
2770
|
}
|
|
2771
|
+
readCurrentAdoptionPostureStrict(projectId) {
|
|
2772
|
+
const posture = readCurrentAdoptionPosture(this.store, projectId);
|
|
2773
|
+
if (!posture)
|
|
2774
|
+
return null;
|
|
2775
|
+
assertAdoptionBaselinePreserved(posture, this.store.listTasks(projectId), this.store.listMilestones(projectId));
|
|
2776
|
+
return posture;
|
|
2777
|
+
}
|
|
2778
|
+
requireExecutionAdoptionPosture(identity, taskId) {
|
|
2779
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
2780
|
+
const milestones = this.store.listMilestones(identity.projectId);
|
|
2781
|
+
const posture = this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2782
|
+
if (posture)
|
|
2783
|
+
return posture;
|
|
2784
|
+
const adoption = this.inspectAdoptionPreparation(identity, tasks, milestones);
|
|
2785
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption posture is required before execution can proceed.', {
|
|
2786
|
+
taskId,
|
|
2787
|
+
adoptionStatus: adoption.status,
|
|
2788
|
+
requiredAction: adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
|
|
2789
|
+
blockers: adoption.blockers,
|
|
2790
|
+
confirmationCode: adoption.confirmationCode,
|
|
2791
|
+
confirmationCodeBindingHash: adoption.confirmationCodeBindingHash,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
1390
2794
|
assertTaskKnowledgeBinding(repositoryRoot, task) {
|
|
1391
2795
|
if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
1392
2796
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has no Project Knowledge Map binding.`);
|
|
@@ -1458,6 +2862,62 @@ function dependenciesComplete(step, completed) {
|
|
|
1458
2862
|
const completedIds = new Set(completed);
|
|
1459
2863
|
return step.dependencies.every((dependency) => completedIds.has(dependency));
|
|
1460
2864
|
}
|
|
2865
|
+
function applyStrictStepReviewResolution(task, stepId, resolution, summary) {
|
|
2866
|
+
if (resolution === 'pending') {
|
|
2867
|
+
throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution is still pending for ${stepId}.`, {
|
|
2868
|
+
taskId: task.id,
|
|
2869
|
+
stepId,
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
const step = requireStep(task, stepId);
|
|
2873
|
+
if (!step.evidence) {
|
|
2874
|
+
throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution requires canonical evidence for ${stepId}.`, {
|
|
2875
|
+
taskId: task.id,
|
|
2876
|
+
stepId,
|
|
2877
|
+
resolution,
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
if (resolution === 'passed_verified') {
|
|
2881
|
+
const completedIds = new Set([
|
|
2882
|
+
...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
|
|
2883
|
+
stepId,
|
|
2884
|
+
]);
|
|
2885
|
+
return {
|
|
2886
|
+
...task,
|
|
2887
|
+
status: 'in_progress',
|
|
2888
|
+
steps: task.steps.map((candidate) => {
|
|
2889
|
+
if (candidate.id === stepId)
|
|
2890
|
+
return { ...candidate, status: 'completed', evidence: step.evidence };
|
|
2891
|
+
if (candidate.status === 'blocked' && dependenciesComplete(candidate, completedIds)) {
|
|
2892
|
+
return { ...candidate, status: 'planned' };
|
|
2893
|
+
}
|
|
2894
|
+
return candidate;
|
|
2895
|
+
}),
|
|
2896
|
+
blockReason: null,
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
if (resolution === 'failed') {
|
|
2900
|
+
return {
|
|
2901
|
+
...task,
|
|
2902
|
+
status: 'needs_fix',
|
|
2903
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
2904
|
+
? { ...candidate, status: 'failed', evidence: null }
|
|
2905
|
+
: candidate),
|
|
2906
|
+
blockReason: summary?.trim() || `Strict Step Review failed for ${stepId}.`,
|
|
2907
|
+
systemCommits: task.systemCommits.includes(step.evidence.commitSha)
|
|
2908
|
+
? task.systemCommits
|
|
2909
|
+
: [...task.systemCommits, step.evidence.commitSha],
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
return {
|
|
2913
|
+
...task,
|
|
2914
|
+
status: 'blocked',
|
|
2915
|
+
steps: task.steps.map((candidate) => candidate.id === stepId
|
|
2916
|
+
? { ...candidate, status: 'in_progress', evidence: step.evidence }
|
|
2917
|
+
: candidate),
|
|
2918
|
+
blockReason: summary?.trim() || `Strict Step Review isolation could not be verified for ${stepId}.`,
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
1461
2921
|
function identityRelative(repositoryRoot, value) {
|
|
1462
2922
|
return path.resolve(repositoryRoot, value);
|
|
1463
2923
|
}
|
|
@@ -1511,6 +2971,83 @@ function validateReviewInput(review) {
|
|
|
1511
2971
|
throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
|
|
1512
2972
|
}
|
|
1513
2973
|
}
|
|
2974
|
+
function countFailedReviewAttempts(taskRoot) {
|
|
2975
|
+
const file = path.join(taskRoot, 'findings.jsonl');
|
|
2976
|
+
if (!existsSync(file))
|
|
2977
|
+
return 0;
|
|
2978
|
+
const attempts = new Set();
|
|
2979
|
+
for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
2980
|
+
try {
|
|
2981
|
+
const finding = JSON.parse(line);
|
|
2982
|
+
if (typeof finding.recordedAt === 'string' && finding.recordedAt) {
|
|
2983
|
+
attempts.add(`${finding.recordedAt}\0${typeof finding.reviewer === 'string' ? finding.reviewer : ''}`);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
catch {
|
|
2987
|
+
throw new WorkflowError('STATE_CORRUPT', `Invalid review finding log: ${file}`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
return attempts.size;
|
|
2991
|
+
}
|
|
2992
|
+
function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
2993
|
+
if (!audit) {
|
|
2994
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
|
|
2995
|
+
failedReviewAttempts,
|
|
2996
|
+
requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (!audit.auditor.trim() || !audit.summary.trim()) {
|
|
3000
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
|
|
3001
|
+
}
|
|
3002
|
+
if (audit.auditor.trim() === task.review.reviewer.trim()) {
|
|
3003
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
|
|
3004
|
+
}
|
|
3005
|
+
const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
|
|
3006
|
+
const reviewedFindingIds = new Set(audit.reviewedFindingIds);
|
|
3007
|
+
if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
|
|
3008
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
|
|
3009
|
+
}
|
|
3010
|
+
const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
|
|
3011
|
+
if (missing.length > 0) {
|
|
3012
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
|
|
3013
|
+
missingFindingIds: missing,
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
if (audit.decision !== 'continue-fix') {
|
|
3017
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
3021
|
+
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
3022
|
+
recordedAt: now.toISOString(),
|
|
3023
|
+
taskId: previous.id,
|
|
3024
|
+
taskRevision: previous.revision,
|
|
3025
|
+
failedReviewAttempts,
|
|
3026
|
+
previousPlanHash: previous.planHash,
|
|
3027
|
+
correctivePlanHash: saved.planHash,
|
|
3028
|
+
...audit,
|
|
3029
|
+
})}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
3030
|
+
}
|
|
3031
|
+
function assertContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
3032
|
+
if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
|
|
3033
|
+
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.' });
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
function isContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
3037
|
+
const classification = (map) => JSON.stringify({
|
|
3038
|
+
entries: map.entries.map((entry) => ({
|
|
3039
|
+
id: entry.id,
|
|
3040
|
+
category: entry.category,
|
|
3041
|
+
path: entry.path,
|
|
3042
|
+
scope: entry.scope,
|
|
3043
|
+
authority: entry.authority,
|
|
3044
|
+
discoveredBy: entry.discoveredBy,
|
|
3045
|
+
})),
|
|
3046
|
+
gaps: map.gaps,
|
|
3047
|
+
conflicts: map.conflicts,
|
|
3048
|
+
});
|
|
3049
|
+
return classification(approved) === classification(inspected);
|
|
3050
|
+
}
|
|
1514
3051
|
function validateMilestonePlan(plan) {
|
|
1515
3052
|
if (!plan.outcome.trim())
|
|
1516
3053
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');
|
|
@@ -1606,11 +3143,14 @@ function nextForMilestone(milestone) {
|
|
|
1606
3143
|
}
|
|
1607
3144
|
function nextForTask(task) {
|
|
1608
3145
|
const stepsComplete = task.steps.every((step) => step.status === 'completed' || step.status === 'skipped');
|
|
3146
|
+
const activeStep = task.steps.find((step) => step.status === 'in_progress');
|
|
3147
|
+
const failedStep = task.steps.find((step) => step.status === 'failed');
|
|
3148
|
+
const runnableStep = task.steps.find((step) => step.status === 'planned' && step.dependencies.every((dependency) => ['completed', 'skipped'].includes(requireStep(task, dependency).status)));
|
|
1609
3149
|
const actionByStatus = {
|
|
1610
3150
|
planning: 'task plan-set',
|
|
1611
3151
|
awaiting_execution_authorization: 'task authorize',
|
|
1612
3152
|
ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
|
|
1613
|
-
in_progress:
|
|
3153
|
+
in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
|
|
1614
3154
|
validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
|
|
1615
3155
|
needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
|
|
1616
3156
|
awaiting_final_acceptance: 'task accept',
|
|
@@ -1620,7 +3160,24 @@ function nextForTask(task) {
|
|
|
1620
3160
|
blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
|
|
1621
3161
|
cancelled: 'none',
|
|
1622
3162
|
};
|
|
1623
|
-
|
|
3163
|
+
const selectedStep = activeStep ?? failedStep ?? runnableStep;
|
|
3164
|
+
return {
|
|
3165
|
+
scope: 'task',
|
|
3166
|
+
id: task.id,
|
|
3167
|
+
status: task.status,
|
|
3168
|
+
action: actionByStatus[task.status],
|
|
3169
|
+
revision: task.revision,
|
|
3170
|
+
...(selectedStep ? { stepId: selectedStep.id } : {}),
|
|
3171
|
+
...(activeStep
|
|
3172
|
+
? {
|
|
3173
|
+
commitPolicy: {
|
|
3174
|
+
owner: 'workflow-core',
|
|
3175
|
+
manualCommitAllowed: false,
|
|
3176
|
+
completionAction: 'task step-complete',
|
|
3177
|
+
},
|
|
3178
|
+
}
|
|
3179
|
+
: {}),
|
|
3180
|
+
};
|
|
1624
3181
|
}
|
|
1625
3182
|
function knowledgeSummary(map) {
|
|
1626
3183
|
return {
|