codex-workflow-v2 2.0.0-beta.12 → 2.0.0-beta.12.10

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.
Files changed (62) hide show
  1. package/README.md +65 -5
  2. package/dist/src/alpha6/downstream-proof.d.ts +68 -0
  3. package/dist/src/alpha6/downstream-proof.js +604 -0
  4. package/dist/src/alpha6/downstream-proof.js.map +1 -0
  5. package/dist/src/alpha6/milestone.d.ts +6 -2
  6. package/dist/src/alpha6/milestone.js +164 -10
  7. package/dist/src/alpha6/milestone.js.map +1 -1
  8. package/dist/src/alpha6/plan-risk.js +2 -2
  9. package/dist/src/alpha6/plan-risk.js.map +1 -1
  10. package/dist/src/alpha6/remediation.d.ts +8 -3
  11. package/dist/src/alpha6/remediation.js +374 -9
  12. package/dist/src/alpha6/remediation.js.map +1 -1
  13. package/dist/src/alpha6/review.d.ts +6 -0
  14. package/dist/src/alpha6/review.js +12 -0
  15. package/dist/src/alpha6/review.js.map +1 -1
  16. package/dist/src/alpha7/corrective-recovery.d.ts +8 -1
  17. package/dist/src/alpha7/corrective-recovery.js +406 -26
  18. package/dist/src/alpha7/corrective-recovery.js.map +1 -1
  19. package/dist/src/cli.js +135 -5
  20. package/dist/src/cli.js.map +1 -1
  21. package/dist/src/contracts.d.ts +154 -3
  22. package/dist/src/dependency-provenance.d.ts +9 -2
  23. package/dist/src/dependency-provenance.js +70 -8
  24. package/dist/src/dependency-provenance.js.map +1 -1
  25. package/dist/src/git.d.ts +1 -1
  26. package/dist/src/git.js +6 -2
  27. package/dist/src/git.js.map +1 -1
  28. package/dist/src/historical-step-provenance.d.ts +20 -0
  29. package/dist/src/historical-step-provenance.js +111 -0
  30. package/dist/src/historical-step-provenance.js.map +1 -0
  31. package/dist/src/state/corrective-replan-executor.js +5 -0
  32. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/dist/src/version.js.map +1 -1
  36. package/dist/src/workflow.d.ts +40 -2
  37. package/dist/src/workflow.js +1440 -65
  38. package/dist/src/workflow.js.map +1 -1
  39. package/docs/autonomy-guardrails.md +91 -1
  40. package/docs/decisions.md +14 -0
  41. package/docs/delegated-approval.md +6 -1
  42. package/docs/development-flow.md +60 -5
  43. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  44. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  45. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  46. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +97 -7
  47. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +68 -9
  48. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +150 -33
  49. package/docs/release.md +51 -8
  50. package/docs/updating-existing-project.md +155 -2
  51. package/docs/validation-report.md +101 -94
  52. package/package.json +7 -2
  53. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  54. package/plugins/codex-workflow-gateway/references/protocol.md +69 -1
  55. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +156 -1
  56. package/schemas/authorization-event.schema.json +8 -1
  57. package/schemas/downstream-proof-invalidation-event.schema.json +71 -0
  58. package/schemas/downstream-proof-replan-recovery-event.schema.json +61 -0
  59. package/schemas/milestone-scope-change-event.schema.json +25 -1
  60. package/schemas/stop-escalate-override-event.schema.json +45 -0
  61. package/schemas/task.schema.json +104 -0
  62. package/scripts/generate-pdf-docs.py +13 -2
@@ -7,8 +7,10 @@ import { createUlid } from '../ulid.js';
7
7
  import { readTaskC1Posture, readTaskHandoffEvents } from '../alpha6/handoff.js';
8
8
  import { readCurrentPlanRiskAudit, readPlanRiskAuditEvents } from '../alpha6/plan-risk.js';
9
9
  import { readMilestoneAutonomyEvents } from './autonomy.js';
10
- import { assertCorrectiveAuditorIndependence, readCorrectiveDecisionEvents, readRemediationEvents, } from '../alpha6/remediation.js';
10
+ import { assertCorrectiveAuditorIndependence, readCorrectiveDecisionEvents, readRemediationEvents, readStopEscalateOverrideEvents, } from '../alpha6/remediation.js';
11
11
  import { assertHash64, canonicalJsonStringify, sha256Hex } from '../alpha6/store-sidecars.js';
12
+ import { gitIsAncestor, runGit } from '../repository.js';
13
+ import { PACKAGE_NAME } from '../version.js';
12
14
  export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
13
15
  const RECOVERY_LOCK = '.corrective-decision-recovery.lock';
14
16
  const KNOWLEDGE_LINE = /^- Knowledge map: revision ([1-9]\d*), hash ([a-f0-9]{64})$/gm;
@@ -63,27 +65,9 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
63
65
  throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective decision already binds the current Plan; recovery is unnecessary.');
64
66
  }
65
67
  assertCorrectiveAuditorIndependence(store, task, stepId, decision.auditor);
66
- const taskRoot = store.taskRoot(task.projectId, task.id);
67
- const planEvidence = comparePlanArtifacts(taskRoot, decision.planHash, task.planHash);
68
- const currentAudit = readCurrentPlanRiskAudit(store, task);
69
- if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
70
- throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
71
- }
72
- const planRiskAudits = readPlanRiskAuditEvents(store, task);
73
- const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(task, decision, planRiskAudits);
74
- const directRebind = requireDirectRebind(task, decision, sourceAudit);
75
- assertRebindMatchesPlans(directRebind, planEvidence);
76
- if (sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
77
- || sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
78
- || reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
79
- || reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash) {
80
- throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
81
- }
82
- const knowledgeMap = store.readKnowledgeMap(task.projectId);
83
- const authorization = requireRefreshAuthorization(task);
84
- const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, directRebind);
68
+ const rebindEvidence = assessContextOnlyPlanRebindEvidence(store, task, stepId, decision);
69
+ const { directRebind, semanticPlanHash, sourceAudit, reboundAudit, planRiskAudits, knowledgeMap, authorization, grant, autonomyContract, } = rebindEvidence;
85
70
  const c1 = requireC1Evidence(store, task, normalizedActor, writerToken);
86
- assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
87
71
  const knowledgeMapApprovalFingerprint = hashEvidence(knowledgeMap.approval);
88
72
  const taskAuthorizationFingerprint = hashEvidence(authorization);
89
73
  const delegationGrantFingerprint = hashEvidence(grant);
@@ -92,7 +76,7 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
92
76
  task,
93
77
  decisionPlanHash: decision.planHash,
94
78
  reboundPlanHash: task.planHash,
95
- semanticPlanHash: planEvidence.semanticPlanHash,
79
+ semanticPlanHash,
96
80
  remediationEvents,
97
81
  correctiveDecisions,
98
82
  planRiskAudits,
@@ -106,7 +90,7 @@ export function assessCorrectiveDecisionRecovery(store, task, stepId, actor, wri
106
90
  task,
107
91
  decision,
108
92
  directRebind,
109
- semanticPlanHash: planEvidence.semanticPlanHash,
93
+ semanticPlanHash,
110
94
  sourceAudit,
111
95
  reboundAudit,
112
96
  knowledgeMapApprovalFingerprint,
@@ -228,6 +212,96 @@ export function assessCorrectiveDecisionRecoveryForNavigation(store, task, stepI
228
212
  const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
229
213
  return assessCorrectiveDecisionRecovery(store, task, stepId, actor, lease?.token ?? null);
230
214
  }
215
+ export function readValidatedStopOverrideContextRebindIfApplicable(store, task, stepId, repositoryRoot) {
216
+ if (!task.planHash)
217
+ return null;
218
+ const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
219
+ const triggeringAttemptCount = remediationEvents.length + 1;
220
+ const staleStops = readCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId
221
+ && event.triggeringAttemptCount === triggeringAttemptCount
222
+ && event.decision === 'stop-escalate'
223
+ && event.planHash !== task.planHash);
224
+ if (staleStops.length === 0)
225
+ return null;
226
+ if (staleStops.length !== 1) {
227
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple stale stop decisions match one guarded retry binding.', {
228
+ taskId: task.id,
229
+ stepId,
230
+ triggeringAttemptCount,
231
+ decisionEventIds: staleStops.map((event) => event.eventId),
232
+ });
233
+ }
234
+ return assessStopOverrideContextRebind(store, task, stepId, staleStops[0], remediationEvents, repositoryRoot);
235
+ }
236
+ function assessStopOverrideContextRebind(store, task, stepId, stopDecision, remediationEvents, repositoryRoot) {
237
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
238
+ if (!step)
239
+ throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
240
+ if (!['planned', 'failed', 'in_progress'].includes(step.status)) {
241
+ throw new WorkflowError('TRANSITION_BLOCKED', `Attempt-four continuation is unavailable while ${stepId} is ${step.status}.`);
242
+ }
243
+ if (remediationEvents.length !== 3
244
+ || remediationEvents[0]?.attemptOrdinal !== 1
245
+ || remediationEvents[0]?.mode !== 'ordinary'
246
+ || remediationEvents[1]?.attemptOrdinal !== 2
247
+ || remediationEvents[1]?.mode !== 'ordinary'
248
+ || remediationEvents[2]?.attemptOrdinal !== 3
249
+ || remediationEvents[2]?.mode !== 'corrective') {
250
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound stop override requires the exact three-attempt beta.11 history.', {
251
+ taskId: task.id,
252
+ stepId,
253
+ attempts: remediationEvents.map((event) => ({ ordinal: event.attemptOrdinal, mode: event.mode })),
254
+ });
255
+ }
256
+ const overrides = readStopEscalateOverrideEvents(store, task).filter((event) => event.stepId === stepId && event.stopDecisionEventId === stopDecision.eventId);
257
+ if (overrides.length !== 1) {
258
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-rebound attempt four requires one exact applied stop override.', {
259
+ taskId: task.id,
260
+ stepId,
261
+ stopDecisionEventId: stopDecision.eventId,
262
+ overrideEventIds: overrides.map((event) => event.eventId),
263
+ });
264
+ }
265
+ const override = overrides[0];
266
+ if (stopDecision.triggeringAttemptCount !== 4
267
+ || stopDecision.planHash !== override.planHash
268
+ || task.planHash === override.planHash
269
+ || override.coveredEventIds.length !== remediationEvents.length
270
+ || override.coveredEventIds.some((eventId, index) => eventId !== remediationEvents[index].eventId)) {
271
+ throw new WorkflowError('STATE_CORRUPT', 'Context-rebound stop override no longer binds the exact source Plan/history.', {
272
+ taskId: task.id,
273
+ stepId,
274
+ overrideEventId: override.eventId,
275
+ });
276
+ }
277
+ assertCorrectiveAuditorIndependence(store, task, stepId, stopDecision.auditor);
278
+ const rebindEvidence = assessAppendOnlyContextPlanRebindEvidence(store, task, stepId, stopDecision);
279
+ const verifiedHeadRecoveries = requireStopOverrideHeadContinuity(repositoryRoot, task, override, rebindEvidence.authorization);
280
+ requireEvidenceBoundAuditTaskRevision(task, rebindEvidence.reboundAudit, verifiedHeadRecoveries, step.status === 'in_progress');
281
+ const stopAt = Date.parse(stopDecision.recordedAt);
282
+ const overrideAt = Date.parse(override.recordedAt);
283
+ const firstRebindAt = Date.parse(rebindEvidence.rebindChain[0].recordedAt);
284
+ const lastRebindAt = Date.parse(rebindEvidence.rebindChain.at(-1).recordedAt);
285
+ if (![stopAt, overrideAt, firstRebindAt, lastRebindAt].every(Number.isFinite)
286
+ || overrideAt < stopAt
287
+ || firstRebindAt < overrideAt
288
+ || lastRebindAt < firstRebindAt) {
289
+ throw new WorkflowError('STATE_CORRUPT', 'Stop override and context-rebind chronology is invalid.', {
290
+ taskId: task.id,
291
+ stepId,
292
+ stopDecisionRecordedAt: stopDecision.recordedAt,
293
+ overrideRecordedAt: override.recordedAt,
294
+ firstRebindRecordedAt: rebindEvidence.rebindChain[0].recordedAt,
295
+ lastRebindRecordedAt: rebindEvidence.rebindChain.at(-1).recordedAt,
296
+ });
297
+ }
298
+ return {
299
+ task,
300
+ override,
301
+ stopDecision,
302
+ semanticPlanHash: rebindEvidence.semanticPlanHash,
303
+ };
304
+ }
231
305
  function recoveryPayload(assessment, actor, now) {
232
306
  const { task, decision, directRebind, sourceAudit, reboundAudit } = assessment;
233
307
  return {
@@ -261,6 +335,142 @@ function recoveryPayload(assessment, actor, now) {
261
335
  function hashEvidence(value) {
262
336
  return sha256Hex(canonicalJsonStringify(value));
263
337
  }
338
+ function assessContextOnlyPlanRebindEvidence(store, task, stepId, sourceDecision, options = {}) {
339
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
340
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
341
+ }
342
+ if (sourceDecision.planHash === task.planHash) {
343
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Context-only Plan rebind validation requires distinct source and current Plan hashes.');
344
+ }
345
+ const taskRoot = store.taskRoot(task.projectId, task.id);
346
+ const planEvidence = comparePlanArtifacts(taskRoot, sourceDecision.planHash, task.planHash);
347
+ const currentAudit = readCurrentPlanRiskAudit(store, task);
348
+ if (!currentAudit || !currentAudit.reviewRequiredStepIds.includes(stepId)) {
349
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
350
+ }
351
+ const planRiskAudits = readPlanRiskAuditEvents(store, task);
352
+ const { sourceAudit, reboundAudit } = requireMechanicalAuditRebound(task, sourceDecision, planRiskAudits, options.allowTaskRevisionAdvance ?? false);
353
+ const directRebind = requireDirectRebind(task, sourceDecision, sourceAudit);
354
+ assertRebindMatchesPlans(directRebind, planEvidence);
355
+ if (sourceAudit.knowledgeMapRevision !== directRebind.fromKnowledgeMapRevision
356
+ || sourceAudit.knowledgeMapHash !== directRebind.fromKnowledgeMapHash
357
+ || reboundAudit.knowledgeMapRevision !== directRebind.toKnowledgeMapRevision
358
+ || reboundAudit.knowledgeMapHash !== directRebind.toKnowledgeMapHash) {
359
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit pair does not bind the direct Knowledge rebind endpoints.');
360
+ }
361
+ const knowledgeMap = store.readKnowledgeMap(task.projectId);
362
+ const authorization = requireRefreshAuthorization(task);
363
+ const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, directRebind);
364
+ assertRefreshChronology(knowledgeMap, authorization, directRebind, reboundAudit);
365
+ return {
366
+ directRebind,
367
+ semanticPlanHash: planEvidence.semanticPlanHash,
368
+ sourceAudit,
369
+ reboundAudit,
370
+ planRiskAudits,
371
+ knowledgeMap,
372
+ authorization,
373
+ grant,
374
+ autonomyContract,
375
+ };
376
+ }
377
+ function assessAppendOnlyContextPlanRebindEvidence(store, task, stepId, sourceDecision) {
378
+ if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
379
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing current Plan or Knowledge bindings.`);
380
+ }
381
+ if (sourceDecision.planHash === task.planHash) {
382
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Append-only Plan rebind validation requires distinct source and current Plan hashes.');
383
+ }
384
+ const planRiskAudits = readPlanRiskAuditEvents(store, task);
385
+ const rebindChain = requireAppendOnlyRebindChain(task, sourceDecision, planRiskAudits);
386
+ const planEvidence = comparePlanRebindChainArtifacts(store.taskRoot(task.projectId, task.id), sourceDecision.planHash, task.planHash, rebindChain);
387
+ const { sourceAudit, reboundAudit, auditChain } = requireMechanicalAuditReboundChain(task, sourceDecision, planRiskAudits, rebindChain);
388
+ if (!reboundAudit.stepClassifications.some((classification) => classification.stepId === stepId && classification.reviewRequired)) {
389
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit.`);
390
+ }
391
+ for (let index = 0; index < rebindChain.length; index += 1) {
392
+ const rebind = rebindChain[index];
393
+ const fromBinding = planEvidence.bindings.get(rebind.previousPlanHash);
394
+ const toBinding = planEvidence.bindings.get(rebind.reboundPlanHash);
395
+ if (!fromBinding || !toBinding) {
396
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind references an unverified Plan artifact.', {
397
+ previousPlanHash: rebind.previousPlanHash,
398
+ reboundPlanHash: rebind.reboundPlanHash,
399
+ });
400
+ }
401
+ assertRebindMatchesPlans(rebind, { decisionBinding: fromBinding, reboundBinding: toBinding });
402
+ const fromAudit = auditChain[index];
403
+ const toAudit = auditChain[index + 1];
404
+ if (fromAudit.knowledgeMapRevision !== rebind.fromKnowledgeMapRevision
405
+ || fromAudit.knowledgeMapHash !== rebind.fromKnowledgeMapHash
406
+ || toAudit.knowledgeMapRevision !== rebind.toKnowledgeMapRevision
407
+ || toAudit.knowledgeMapHash !== rebind.toKnowledgeMapHash) {
408
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit chain does not bind every Knowledge rebind endpoint.', {
409
+ rebindIndex: index,
410
+ });
411
+ }
412
+ }
413
+ const knowledgeMap = store.readKnowledgeMap(task.projectId);
414
+ const authorization = requireRefreshAuthorization(task);
415
+ const finalRebind = rebindChain.at(-1);
416
+ const { grant, autonomyContract } = requireRefreshAuthority(store, task, knowledgeMap, authorization, finalRebind);
417
+ assertRefreshChronology(knowledgeMap, authorization, finalRebind, reboundAudit);
418
+ return {
419
+ rebindChain,
420
+ semanticPlanHash: planEvidence.semanticPlanHash,
421
+ sourceAudit,
422
+ reboundAudit,
423
+ planRiskAudits,
424
+ knowledgeMap,
425
+ authorization,
426
+ grant,
427
+ autonomyContract,
428
+ };
429
+ }
430
+ function comparePlanRebindChainArtifacts(taskRoot, sourcePlanHash, currentPlanHash, rebindChain) {
431
+ const planHashes = [sourcePlanHash, ...rebindChain.map((rebind) => rebind.reboundPlanHash)];
432
+ if (planHashes.at(-1) !== currentPlanHash || new Set(planHashes).size !== planHashes.length) {
433
+ throw new WorkflowError('STATE_CORRUPT', 'Knowledge rebind chain does not form one acyclic source-to-current Plan sequence.');
434
+ }
435
+ const contents = planHashes.map((planHash) => readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash));
436
+ const bindings = new Map();
437
+ let semanticPlanHash = null;
438
+ for (let index = 0; index < contents.length; index += 1) {
439
+ const planHash = planHashes[index];
440
+ bindings.set(planHash, normalizePlanKnowledgeBinding(contents[index], `Plan ${planHash}`).binding);
441
+ if (index === 0)
442
+ continue;
443
+ const adjacentSemanticHash = assertKnowledgeBindingOnlyPlanChange(contents[index - 1], contents[index]);
444
+ if (semanticPlanHash !== null && semanticPlanHash !== adjacentSemanticHash) {
445
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain does not preserve one semantic Plan identity.');
446
+ }
447
+ semanticPlanHash = adjacentSemanticHash;
448
+ }
449
+ if (!semanticPlanHash) {
450
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Plan rebind chain has no comparable Plan hop.');
451
+ }
452
+ return { semanticPlanHash, bindings };
453
+ }
454
+ function readVerifiedPlanArtifact(taskRoot, planHash, currentPlanHash) {
455
+ assertHash64(planHash, 'Plan hash');
456
+ const file = planHash === currentPlanHash
457
+ ? path.join(taskRoot, 'plan.md')
458
+ : path.join(taskRoot, '.versions', `plan.md.${planHash}`);
459
+ if (!existsSync(file)) {
460
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact required by the append-only rebind chain is missing.', {
461
+ planHash,
462
+ file,
463
+ });
464
+ }
465
+ const content = readFileSync(file, 'utf8');
466
+ if (sha256Hex(content) !== planHash) {
467
+ throw new WorkflowError('STATE_CORRUPT', 'Plan artifact content does not match its append-only rebind hash.', {
468
+ planHash,
469
+ file,
470
+ });
471
+ }
472
+ return content;
473
+ }
264
474
  function comparePlanArtifacts(taskRoot, decisionPlanHash, reboundPlanHash) {
265
475
  assertHash64(decisionPlanHash, 'Corrective decision Plan hash');
266
476
  assertHash64(reboundPlanHash, 'Current Plan hash');
@@ -298,7 +508,7 @@ function normalizePlanKnowledgeBinding(content, label) {
298
508
  binding: { revision: Number(match[1]), hash: match[2] },
299
509
  };
300
510
  }
301
- function requireMechanicalAuditRebound(task, decision, events) {
511
+ function requireMechanicalAuditRebound(task, decision, events, allowTaskRevisionAdvance = false) {
302
512
  const sources = events.filter((event) => event.planHash === decision.planHash);
303
513
  const rebounds = events.filter((event) => event.planHash === task.planHash);
304
514
  if (sources.length !== 1 || rebounds.length !== 1) {
@@ -328,10 +538,13 @@ function requireMechanicalAuditRebound(task, decision, events) {
328
538
  if (sourceAudit.briefHash !== task.briefHash || reboundAudit.briefHash !== task.briefHash) {
329
539
  throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebound does not preserve the current Brief binding.');
330
540
  }
331
- if (reboundAudit.taskRevision + 1 !== task.revision) {
332
- throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit must be exactly one Task revision before current authorization.', {
541
+ const authorizedTaskRevision = reboundAudit.taskRevision + 1;
542
+ if ((!allowTaskRevisionAdvance && authorizedTaskRevision !== task.revision)
543
+ || (allowTaskRevisionAdvance && authorizedTaskRevision > task.revision)) {
544
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit does not bind the required Task authorization boundary.', {
333
545
  reboundAuditTaskRevision: reboundAudit.taskRevision,
334
546
  currentTaskRevision: task.revision,
547
+ allowTaskRevisionAdvance,
335
548
  });
336
549
  }
337
550
  return { sourceAudit, reboundAudit };
@@ -355,6 +568,173 @@ function requireDirectRebind(task, decision, sourceAudit) {
355
568
  }
356
569
  return selected;
357
570
  }
571
+ function requireAppendOnlyRebindChain(task, decision, planRiskAudits) {
572
+ for (let index = 0; index < task.knowledgeRebinds.length; index += 1) {
573
+ const recordedAt = Date.parse(task.knowledgeRebinds[index].recordedAt);
574
+ if (!Number.isFinite(recordedAt)
575
+ || (index > 0 && recordedAt <= Date.parse(task.knowledgeRebinds[index - 1].recordedAt))) {
576
+ throw new WorkflowError('STATE_CORRUPT', 'Task Knowledge rebind timestamps must be finite and strictly increasing.');
577
+ }
578
+ }
579
+ const sourceAudits = planRiskAudits.filter((event) => event.planHash === decision.planHash);
580
+ if (sourceAudits.length !== 1) {
581
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one exact source Plan Risk Audit.', {
582
+ sourceAuditIds: sourceAudits.map((event) => event.eventId),
583
+ });
584
+ }
585
+ const boundary = Math.max(Date.parse(decision.recordedAt), Date.parse(sourceAudits[0].recordedAt));
586
+ if (!Number.isFinite(boundary)) {
587
+ throw new WorkflowError('STATE_CORRUPT', 'Stop decision or source Plan Risk Audit timestamp is invalid.');
588
+ }
589
+ const chain = task.knowledgeRebinds.filter((record) => Date.parse(record.recordedAt) > boundary);
590
+ if (chain.length < 1) {
591
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Stop-override continuation requires at least one Knowledge rebind after its source authority.');
592
+ }
593
+ if (task.knowledgeRebinds.at(-1) !== chain.at(-1)) {
594
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only Knowledge rebind chain does not terminate at the current Task boundary.');
595
+ }
596
+ let expectedPlanHash = decision.planHash;
597
+ for (const record of chain) {
598
+ if (record.previousPlanHash !== expectedPlanHash || record.reboundPlanHash === record.previousPlanHash) {
599
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance is not one contiguous source-to-current Plan chain.', {
600
+ expectedPreviousPlanHash: expectedPlanHash,
601
+ actualPreviousPlanHash: record.previousPlanHash,
602
+ reboundPlanHash: record.reboundPlanHash,
603
+ });
604
+ }
605
+ expectedPlanHash = record.reboundPlanHash;
606
+ }
607
+ if (expectedPlanHash !== task.planHash) {
608
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge rebind provenance does not terminate at the current Task Plan.', {
609
+ expectedCurrentPlanHash: task.planHash,
610
+ actualTerminalPlanHash: expectedPlanHash,
611
+ });
612
+ }
613
+ return chain;
614
+ }
615
+ function requireMechanicalAuditReboundChain(task, decision, events, rebindChain) {
616
+ const planHashes = [decision.planHash, ...rebindChain.map((record) => record.reboundPlanHash)];
617
+ const auditChain = planHashes.map((planHash) => {
618
+ const matches = events.filter((event) => event.planHash === planHash);
619
+ if (matches.length !== 1) {
620
+ throw new WorkflowError('STATE_CORRUPT', 'Append-only recovery requires one Plan Risk Audit for every Plan in the rebind chain.', {
621
+ planHash,
622
+ auditEventIds: matches.map((event) => event.eventId),
623
+ });
624
+ }
625
+ return matches[0];
626
+ });
627
+ const sourceIndex = events.indexOf(auditChain[0]);
628
+ for (let index = 0; index < auditChain.length; index += 1) {
629
+ if (events[sourceIndex + index] !== auditChain[index]) {
630
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind evidence is not one contiguous append-only chain.');
631
+ }
632
+ if (index > 0)
633
+ assertMechanicalAuditSuccessor(task, auditChain[index - 1], auditChain[index]);
634
+ }
635
+ if (sourceIndex < 0 || sourceIndex + auditChain.length !== events.length) {
636
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit rebind chain does not end at the current append-only boundary.');
637
+ }
638
+ const reboundAudit = auditChain.at(-1);
639
+ return { sourceAudit: auditChain[0], reboundAudit, auditChain };
640
+ }
641
+ function assertMechanicalAuditSuccessor(task, sourceAudit, reboundAudit) {
642
+ const expectedSummary = `${sourceAudit.summary} Rebound automatically after content-only Task context refresh.`;
643
+ if (reboundAudit.previousEventHash !== sourceAudit.eventHash
644
+ || reboundAudit.taskId !== sourceAudit.taskId
645
+ || reboundAudit.briefHash !== sourceAudit.briefHash
646
+ || reboundAudit.briefHash !== task.briefHash
647
+ || reboundAudit.planner !== sourceAudit.planner
648
+ || reboundAudit.auditor !== sourceAudit.auditor
649
+ || reboundAudit.decision !== sourceAudit.decision
650
+ || reboundAudit.splitRationale !== sourceAudit.splitRationale
651
+ || reboundAudit.summary !== expectedSummary
652
+ || canonicalJsonStringify(reboundAudit.stepClassifications) !== canonicalJsonStringify(sourceAudit.stepClassifications)) {
653
+ throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit successor is not a mechanical content-refresh rebound.');
654
+ }
655
+ }
656
+ function requireStopOverrideHeadContinuity(repositoryRoot, task, override, authorization) {
657
+ const authorizedHead = authorization.headCommit;
658
+ if (!authorizedHead) {
659
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution authorization does not bind a Git HEAD.');
660
+ }
661
+ if (authorizedHead === override.headCommit)
662
+ return [];
663
+ if (!gitIsAncestor(repositoryRoot, override.headCommit, authorizedHead)) {
664
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Current execution HEAD is not an append-only descendant of the stop-override HEAD.', {
665
+ overrideHeadCommit: override.headCommit,
666
+ authorizedHeadCommit: authorizedHead,
667
+ });
668
+ }
669
+ const output = runGit(repositoryRoot, ['rev-list', '--reverse', `${override.headCommit}..${authorizedHead}`]);
670
+ const commits = output ? output.split('\n') : [];
671
+ if (commits.length < 1) {
672
+ throw new WorkflowError('STATE_CORRUPT', 'Git reports distinct stop-override and authorization HEADs without an intervening commit.');
673
+ }
674
+ let expectedParent = override.headCommit;
675
+ let previousRecoveryIndex = -1;
676
+ let previousRecoveryAt = Number.NEGATIVE_INFINITY;
677
+ const verifiedRecoveries = [];
678
+ for (const commit of commits) {
679
+ const matches = (task.dependencyProvenanceRecoveries ?? []).filter((record) => record.commitSha === commit);
680
+ if (matches.length !== 1) {
681
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Git HEAD advanced through a commit without one exact dependency-provenance recovery.', {
682
+ commit,
683
+ recoveryCount: matches.length,
684
+ });
685
+ }
686
+ const recovery = matches[0];
687
+ const recoveryIndex = (task.dependencyProvenanceRecoveries ?? []).indexOf(recovery);
688
+ const recoveryAt = Date.parse(recovery.recordedAt);
689
+ const parents = runGit(repositoryRoot, ['show', '-s', '--format=%P', commit]).split(/\s+/).filter(Boolean);
690
+ const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
691
+ const files = filesOutput ? filesOutput.split('\n').sort() : [];
692
+ if (parents.length !== 1
693
+ || parents[0] !== expectedParent
694
+ || recovery.parentCommitSha !== expectedParent
695
+ || recoveryIndex <= previousRecoveryIndex
696
+ || !Number.isFinite(recoveryAt)
697
+ || recoveryAt <= previousRecoveryAt
698
+ || recovery.packageName !== PACKAGE_NAME
699
+ || canonicalJsonStringify(recovery.files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
700
+ || canonicalJsonStringify(files) !== canonicalJsonStringify(['package-lock.json', 'package.json'])
701
+ || !task.systemCommits.includes(commit)) {
702
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Dependency-only Git continuity from stop override to current authorization is invalid.', {
703
+ commit,
704
+ expectedParent,
705
+ recordedParent: recovery.parentCommitSha,
706
+ parents,
707
+ files,
708
+ });
709
+ }
710
+ verifiedRecoveries.push(recovery);
711
+ previousRecoveryIndex = recoveryIndex;
712
+ previousRecoveryAt = recoveryAt;
713
+ expectedParent = commit;
714
+ }
715
+ if (expectedParent !== authorizedHead) {
716
+ throw new WorkflowError('STATE_CORRUPT', 'Dependency-provenance Git chain does not terminate at the authorized HEAD.');
717
+ }
718
+ return verifiedRecoveries;
719
+ }
720
+ function requireEvidenceBoundAuditTaskRevision(task, reboundAudit, verifiedHeadRecoveries, allowTaskRevisionAdvance) {
721
+ const auditAt = Date.parse(reboundAudit.recordedAt);
722
+ if (!Number.isFinite(auditAt)) {
723
+ throw new WorkflowError('STATE_CORRUPT', 'Current rebound Plan Risk Audit timestamp is invalid.');
724
+ }
725
+ const postAuditRecoveries = verifiedHeadRecoveries.filter((recovery) => Date.parse(recovery.recordedAt) > auditAt);
726
+ const evidenceBoundTaskRevision = reboundAudit.taskRevision + 1 + postAuditRecoveries.length;
727
+ if ((!allowTaskRevisionAdvance && evidenceBoundTaskRevision !== task.revision)
728
+ || (allowTaskRevisionAdvance && evidenceBoundTaskRevision > task.revision)) {
729
+ throw new WorkflowError('STATE_CORRUPT', 'Mechanical rebound audit chain does not bind the evidence-accounted Task authorization boundary.', {
730
+ reboundAuditTaskRevision: reboundAudit.taskRevision,
731
+ currentTaskRevision: task.revision,
732
+ postAuditDependencyRecoveryCommits: postAuditRecoveries.map((recovery) => recovery.commitSha),
733
+ evidenceBoundTaskRevision,
734
+ allowTaskRevisionAdvance,
735
+ });
736
+ }
737
+ }
358
738
  function assertRebindMatchesPlans(rebind, plans) {
359
739
  if (rebind.fromKnowledgeMapRevision !== plans.decisionBinding.revision
360
740
  || rebind.fromKnowledgeMapHash !== plans.decisionBinding.hash