codex-workflow-v2 2.0.0-beta.13.11 → 2.0.0-beta.13.13

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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +21 -9
  3. package/dist/src/alpha6/component-owner.d.ts +15 -1
  4. package/dist/src/alpha6/component-owner.js +44 -2
  5. package/dist/src/alpha6/component-owner.js.map +1 -1
  6. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  7. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  8. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  9. package/dist/src/alpha6/plan-integrity-update.d.ts +6 -0
  10. package/dist/src/alpha6/plan-integrity-update.js +17 -0
  11. package/dist/src/alpha6/plan-integrity-update.js.map +1 -0
  12. package/dist/src/alpha6/plan-integrity.js +7 -5
  13. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  14. package/dist/src/alpha6/remediation.d.ts +5 -0
  15. package/dist/src/alpha6/remediation.js +150 -33
  16. package/dist/src/alpha6/remediation.js.map +1 -1
  17. package/dist/src/contracts.d.ts +7 -0
  18. package/dist/src/navigation-actions.d.ts +9 -0
  19. package/dist/src/navigation-actions.js +72 -0
  20. package/dist/src/navigation-actions.js.map +1 -0
  21. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  22. package/dist/src/state/corrective-replan-executor.js +37 -1
  23. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  24. package/dist/src/version.d.ts +1 -1
  25. package/dist/src/version.js +1 -1
  26. package/dist/src/workflow.d.ts +3 -0
  27. package/dist/src/workflow.js +66 -9
  28. package/dist/src/workflow.js.map +1 -1
  29. package/docs/lifecycle-capture.md +34 -0
  30. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  31. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  32. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  33. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +37 -1
  34. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +36 -2
  35. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +38 -2
  36. package/docs/release.md +57 -0
  37. package/docs/stable-release-defect-register.md +26 -1
  38. package/docs/updating-existing-project.md +45 -0
  39. package/package.json +1 -1
  40. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +17 -0
  41. package/schemas/corrective-decision-event.schema.json +3 -1
@@ -1,6 +1,6 @@
1
1
  import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
3
+ import { assertKnowledgeBindingOnlyPlanChange, normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from '../domain/plan-semantics.js';
4
4
  import { pathAllowed } from '../domain/validation.js';
5
5
  import { WorkflowError } from '../errors.js';
6
6
  import { hashCanonical } from '../lifecycle/fingerprint.js';
@@ -10,7 +10,8 @@ import { readPlanRiskAuditEvents } from './plan-risk.js';
10
10
  import { assertHash64, canonicalJsonStringify, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
11
11
  import { readTaskC1Posture } from './handoff.js';
12
12
  import { buildCurrentStrictStepReviewCycle, latestPassedVerifiedStepReviewEvent, readReviewerAttestations, readStepReviewEvents, } from './review.js';
13
- import { semanticBoundaryHash } from './component-owner.js';
13
+ import { correctiveReplanBoundaryMatchesStep, readCompletedCorrectiveReplanHistory } from '../state/corrective-replan-executor.js';
14
+ import { semanticBoundaryHash, validLiteralTestInvocationEvidence } from './component-owner.js';
14
15
  import { reviewerCauseFingerprint } from './remediation-cause.js';
15
16
  export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
16
17
  export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
@@ -83,7 +84,7 @@ function validateRemediationHistory(store, task) {
83
84
  const overriddenStopDecisionEventIds = validateStopEscalateOverrideHistory(task, remediationEvents, correctiveDecisionEvents, readRawStopEscalateOverrideEvents(store, task));
84
85
  validateRemediationAttemptOrdinals(remediationEvents, task);
85
86
  validateRemediationReviewReferences(store, task, remediationEvents);
86
- validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
87
+ validateCorrectiveDecisionCoverage(store, task, correctiveDecisionEvents, remediationEvents);
87
88
  const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
88
89
  validateHistoricalRemediationModesForAllSteps(store, task, remediationEvents, correctiveDecisionEvents, recoveredEventIds, overriddenStopDecisionEventIds);
89
90
  return {
@@ -139,7 +140,7 @@ export function readStopEscalateOverrideEvents(store, task) {
139
140
  const decisions = readRawCorrectiveDecisionEvents(store, task);
140
141
  const overrides = readRawStopEscalateOverrideEvents(store, task);
141
142
  validateRemediationAttemptOrdinals(remediations, task);
142
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
143
+ validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
143
144
  validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
144
145
  return overrides;
145
146
  }
@@ -148,7 +149,7 @@ export function stopEscalateOverrideDecisionEventIds(store, task) {
148
149
  const decisions = readRawCorrectiveDecisionEvents(store, task);
149
150
  const overrides = readRawStopEscalateOverrideEvents(store, task);
150
151
  validateRemediationAttemptOrdinals(remediations, task);
151
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
152
+ validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
152
153
  return validateStopEscalateOverrideHistory(task, remediations, decisions, overrides);
153
154
  }
154
155
  export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now, reboundStopOverrideDecisionEventId = null, causeBindings = []) {
@@ -185,7 +186,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
185
186
  }
186
187
  const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
187
188
  const cycle = selectCurrentPlanRemediationCycle(store, task, priorEvents, correctiveDecisions);
188
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
189
+ const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
189
190
  recoveredDecisionEventId,
190
191
  reboundStopOverrideDecisionEventId,
191
192
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
@@ -206,7 +207,7 @@ export function prepareRemediationEvent(store, task, stepId, failureKind, failur
206
207
  return { event: reused, postimage: null };
207
208
  }
208
209
  const cycle = selectCurrentPlanRemediationCycle(store, task, existing, correctiveDecisions);
209
- const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
210
+ const gate = deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, cycle.remediationEvents, cycle.correctiveDecisions, {
210
211
  recoveredDecisionEventId,
211
212
  reboundStopOverrideDecisionEventId,
212
213
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
@@ -290,7 +291,7 @@ export function assessRemediationModeRecovery(store, task, stepId, actor, writer
290
291
  const decisions = readRawCorrectiveDecisionEvents(store, task);
291
292
  validateRemediationAttemptOrdinals(remediations, task);
292
293
  validateRemediationReviewReferences(store, task, remediations);
293
- validateCorrectiveDecisionCoverage(task, decisions, remediations);
294
+ validateCorrectiveDecisionCoverage(store, task, decisions, remediations);
294
295
  const malformed = remediations.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
295
296
  if (malformed.length !== 1 || malformed[0].stepId !== stepId || malformed[0].attemptOrdinal !== 3) {
296
297
  throw new WorkflowError('STATE_CORRUPT', 'State does not contain the one exact alpha.7.2 remediation mode omission.', {
@@ -766,7 +767,11 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
766
767
  if (!auditor.trim()) {
767
768
  throw new WorkflowError('INVALID_ARGUMENT', 'Plan-integrity recovery requires a derived auditor identity.');
768
769
  }
769
- const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
770
+ const allRemediationEvents = readRemediationEvents(store, task).filter(event => event.stepId === stepId);
771
+ const support = evidence.kind === 'check-support-anchor-outside-allowed-writes' || evidence.kind === 'failed-check-component-owner-outside-allowed-writes';
772
+ const remediationEvents = support ? selectPlanIntegrityRemediations(store, task, allRemediationEvents, readCorrectiveDecisionEvents(store, task).filter(event => event.stepId === stepId)) : allRemediationEvents;
773
+ if (!remediationEvents)
774
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Mechanical recovery lacks a verified current Plan cycle.');
770
775
  const expectedBindings = evidence.kind === 'missing-npm-script-outside-allowed-writes'
771
776
  ? [{
772
777
  eventId: evidence.remediationEventId,
@@ -777,8 +782,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
777
782
  if (remediationEvents.length !== expectedBindings.length
778
783
  || remediationEvents.some((event, index) => {
779
784
  const binding = expectedBindings[index];
780
- return event.attemptOrdinal !== index + 1
781
- || event.failureKind !== 'checks-failed'
785
+ return event.failureKind !== 'checks-failed'
782
786
  || event.eventId !== binding?.eventId
783
787
  || event.eventHash !== binding.eventHash
784
788
  || event.attemptOrdinal !== binding.attemptOrdinal;
@@ -789,7 +793,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
789
793
  remediationEventIds: remediationEvents.map((event) => event.eventId),
790
794
  });
791
795
  }
792
- const triggeringAttemptCount = remediationEvents.length + 1;
796
+ const triggeringAttemptCount = allRemediationEvents.length + 1;
793
797
  const recoveryDecision = evidence.kind === 'check-support-anchor-outside-allowed-writes'
794
798
  || evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
795
799
  ? 'continue-fix'
@@ -826,7 +830,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
826
830
  decision: recoveryDecision,
827
831
  auditor: auditor.trim(),
828
832
  coveredFindingIds: [findingId],
829
- coveredEventIds: remediationEvents.map((event) => event.eventId),
833
+ coveredEventIds: allRemediationEvents.map((event) => event.eventId),
830
834
  planHash: task.planHash,
831
835
  recoveryBasis: 'plan-integrity',
832
836
  planIntegrityEvidence: evidence,
@@ -836,7 +840,7 @@ export function appendPlanIntegrityRecoveryDecision(store, task, stepId, auditor
836
840
  }
837
841
  export function assertGuardedRemediationAttemptAllowed(store, task, stepId, planRiskAudit, recoveredDecisionEventId = null, reboundStopOverrideDecisionEventId = null) {
838
842
  const history = readOpenGuardedRemediationHistory(store, task, stepId);
839
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
843
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, history.remediationEvents, history.correctiveDecisions, {
840
844
  enforceRuntimeGate: true,
841
845
  recoveredDecisionEventId,
842
846
  reboundStopOverrideDecisionEventId,
@@ -866,7 +870,7 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
866
870
  && deriveCurrentCauseDecision(remediationEvents, history.priorRemediationEvents) !== 'root-cause-replan')
867
871
  return null;
868
872
  if (step.status === 'failed' || step.status === 'planned') {
869
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
873
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
870
874
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
871
875
  priorRemediationEvents: history.priorRemediationEvents,
872
876
  nextAttemptOrdinal: history.nextAttemptOrdinal,
@@ -877,7 +881,7 @@ export function readGuardedRemediationPostureForStep(store, task, stepId, planRi
877
881
  const currentCycle = buildCurrentStrictStepReviewCycle(store, task, stepId);
878
882
  if (currentCycle.resolution !== 'unverified')
879
883
  return null;
880
- return deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
884
+ return deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, history.correctiveDecisions, {
881
885
  overriddenStopDecisionEventIds: stopEscalateOverrideDecisionEventIds(store, task),
882
886
  priorRemediationEvents: history.priorRemediationEvents,
883
887
  nextAttemptOrdinal: history.nextAttemptOrdinal,
@@ -898,7 +902,7 @@ function readOpenGuardedRemediationHistory(store, task, stepId) {
898
902
  nextAttemptOrdinal: allStepRemediations.length + 1,
899
903
  };
900
904
  }
901
- function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions) {
905
+ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, correctiveDecisions, canonicalPlanHash = task.planHash) {
902
906
  const unchanged = () => ({
903
907
  remediationEvents: [...remediationEvents],
904
908
  priorRemediationEvents: [],
@@ -916,13 +920,13 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
916
920
  const taskRoot = store.taskRoot(task.projectId, task.id);
917
921
  let currentSemanticHash;
918
922
  try {
919
- currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash, task.planHash), 'current').semanticHash;
923
+ currentSemanticHash = normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(taskRoot, task.planHash, canonicalPlanHash), 'current').semanticHash;
920
924
  }
921
925
  catch {
922
926
  return unchanged();
923
927
  }
924
928
  const reviews = readStepReviewEvents(store, task);
925
- const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot, task.planHash, event, reviews, audits));
929
+ const bindings = remediationEvents.map((event) => resolveRemediationPlanCycleBinding(taskRoot, canonicalPlanHash, event, reviews, audits));
926
930
  if (bindings.some((binding) => binding === null))
927
931
  return unchanged();
928
932
  let cycleStart = remediationEvents.length;
@@ -939,6 +943,71 @@ function selectCurrentPlanRemediationCycle(store, task, remediationEvents, corre
939
943
  correctiveDecisions: [...correctiveDecisions],
940
944
  };
941
945
  }
946
+ /** Scope mechanical support evidence only; global corrective authority is never discarded. */
947
+ export function selectPlanIntegrityRemediations(store, task, events, decisions, canonicalPlanHash = task.planHash) {
948
+ if (events.length === 0)
949
+ return null;
950
+ const stepId = events[0].stepId;
951
+ if (events.some(event => event.stepId !== stepId) || decisions.some(event => event.stepId !== stepId))
952
+ return null;
953
+ const cycle = selectCurrentPlanRemediationCycle(store, task, events, decisions, canonicalPlanHash);
954
+ if (cycle.priorRemediationEvents.length === 0)
955
+ return decisions.length === 0 ? [...events] : null;
956
+ if (!task.planHash || cycle.remediationEvents.length === 0
957
+ || decisions.some(d => d.decision === 'stop-escalate' || d.decision === 'split-required'))
958
+ return null;
959
+ const root = store.taskRoot(task.projectId, task.id);
960
+ const history = readCompletedCorrectiveReplanHistory(store, task);
961
+ const semantic = (hash) => {
962
+ const saved = history.flatMap(boundary => [boundary.beforePlan, boundary.afterPlan]).find(bytes => sha256Hex(bytes) === hash);
963
+ return normalizePlanKnowledgeBinding(saved ?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash), 'cycle').semanticHash;
964
+ };
965
+ const currentSemantic = semantic(task.planHash);
966
+ const audits = readPlanRiskAuditEvents(store, task), reviews = readStepReviewEvents(store, task);
967
+ const priorBindings = cycle.priorRemediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
968
+ const currentBindings = cycle.remediationEvents.map(event => resolveRemediationPlanCycleBinding(root, canonicalPlanHash, event, reviews, audits));
969
+ if (priorBindings.some(b => !b) || currentBindings.some(b => !b))
970
+ return null;
971
+ const boundaries = history.filter(boundary => correctiveReplanBoundaryMatchesStep(boundary.before, stepId)
972
+ && boundary.after.planHash && boundary.before.planHash
973
+ && semantic(boundary.after.planHash) === currentSemantic && semantic(boundary.before.planHash) !== currentSemantic
974
+ && boundary.after.revision <= task.revision
975
+ && priorBindings.every(b => b.sourceTaskRevision < boundary.after.revision)
976
+ && currentBindings.every(b => b.sourceTaskRevision > boundary.after.revision));
977
+ if (boundaries.length !== 1)
978
+ return null;
979
+ const boundary = boundaries[0];
980
+ let cursor = task.planHash;
981
+ const visited = new Set();
982
+ const planBytes = (hash) => history.flatMap(b => [b.beforePlan, b.afterPlan]).find(bytes => sha256Hex(bytes) === hash)
983
+ ?? readVerifiedPlanArtifact(root, hash, canonicalPlanHash);
984
+ while (cursor !== boundary.after.planHash) {
985
+ if (visited.has(cursor))
986
+ return null;
987
+ visited.add(cursor);
988
+ const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
989
+ if (links.length !== 1 || !links[0].previousPlanHash)
990
+ return null;
991
+ const link = links[0];
992
+ assertKnowledgeBindingOnlyPlanChange(planBytes(link.previousPlanHash), planBytes(cursor));
993
+ if (!planBytes(link.previousPlanHash).includes(`- Knowledge map: revision ${link.fromKnowledgeMapRevision}, hash ${link.fromKnowledgeMapHash}\n`)
994
+ || !planBytes(cursor).includes(`- Knowledge map: revision ${link.toKnowledgeMapRevision}, hash ${link.toKnowledgeMapHash}\n`))
995
+ return null;
996
+ cursor = link.previousPlanHash;
997
+ }
998
+ if (!task.authorizations.some(a => a.kind === 'execution' && ['approved', 'superseded'].includes(a.decision)
999
+ && a.planHash === task.planHash && a.briefHash === task.briefHash
1000
+ && a.recordedAt >= boundary.admittedAt))
1001
+ return null;
1002
+ if (decisions.length === 0 || decisions.some(d => d.recordedAt > boundary.admittedAt
1003
+ || semantic(d.planHash) === currentSemantic))
1004
+ return null;
1005
+ const last = decisions.at(-1);
1006
+ if (last.decision !== 'replan-required' || last.triggeringAttemptCount !== cycle.priorRemediationEvents.length + 1
1007
+ || semantic(last.planHash) !== semantic(boundary.before.planHash))
1008
+ return null;
1009
+ return cycle.remediationEvents;
1010
+ }
942
1011
  function resolveRemediationPlanCycleBinding(taskRoot, currentPlanHash, event, reviews, audits) {
943
1012
  let planHash = null;
944
1013
  let sourceTaskRevision = null;
@@ -994,7 +1063,7 @@ export function findFailedGuardedStepRequiringCorrectiveDecision(store, task, pl
994
1063
  ? posture
995
1064
  : null;
996
1065
  }
997
- function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
1066
+ function deriveAttemptGateFromHistory(store, task, stepId, planRiskAudit, remediationEvents, correctiveDecisions, options = {}) {
998
1067
  const attemptCount = remediationEvents.length + (options.priorRemediationEvents?.length ?? 0);
999
1068
  const nextAttemptOrdinal = options.nextAttemptOrdinal ?? attemptCount + 1;
1000
1069
  const guardedCategory = requireGuardedCategory(planRiskAudit, stepId);
@@ -1002,8 +1071,9 @@ function deriveAttemptGateFromHistory(task, stepId, planRiskAudit, remediationEv
1002
1071
  const currentCorrectiveDecision = findCurrentCorrectiveDecision(task, stepId, nextAttemptOrdinal, correctiveDecisions);
1003
1072
  const planIntegrityDecision = currentCorrectiveDecision;
1004
1073
  if (planIntegrityDecision?.recoveryBasis === 'plan-integrity'
1005
- && (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required')) {
1006
- if (planIntegrityDecision.planHash !== task.planHash) {
1074
+ && (planIntegrityDecision.planHash === task.planHash || planIntegrityDecision.decision === 'replan-required'
1075
+ || isKnowledgeReboundSupportDecision(store, task, planIntegrityDecision))) {
1076
+ if (planIntegrityDecision.planHash !== task.planHash && planIntegrityDecision.decision === 'replan-required') {
1007
1077
  return {
1008
1078
  stepId,
1009
1079
  guardedCategory,
@@ -1638,9 +1708,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1638
1708
  const validateCurrentPlanBinding = event.planHash === task.planHash;
1639
1709
  if (event.decision !== 'continue-fix'
1640
1710
  || evidence.remediationEvents.length === 0
1641
- || event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
1711
+ || event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
1642
1712
  || evidence.remediationEvents.some((binding, index) => {
1643
- if (!binding.eventId || binding.attemptOrdinal !== index + 1)
1713
+ if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
1644
1714
  return true;
1645
1715
  assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
1646
1716
  return false;
@@ -1681,9 +1751,9 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1681
1751
  assertHash64(evidence.failedCheckCauseFingerprint, `Plan-integrity failedCheckCauseFingerprint at ${file}:${line}`);
1682
1752
  if (event.decision !== 'continue-fix'
1683
1753
  || evidence.remediationEvents.length === 0
1684
- || event.triggeringAttemptCount !== evidence.remediationEvents.length + 1
1754
+ || event.triggeringAttemptCount !== evidence.remediationEvents.at(-1).attemptOrdinal + 1
1685
1755
  || evidence.remediationEvents.some((binding, index) => {
1686
- if (!binding.eventId || binding.attemptOrdinal !== index + 1)
1756
+ if (!binding.eventId || binding.attemptOrdinal !== evidence.remediationEvents[0].attemptOrdinal + index)
1687
1757
  return true;
1688
1758
  assertHash64(binding.eventHash, `Plan-integrity remediation event hash at ${file}:${line}`);
1689
1759
  return false;
@@ -1706,9 +1776,10 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1706
1776
  || !conflict.componentRoot.trim()
1707
1777
  || !(conflict.ownerPath === conflict.componentRoot
1708
1778
  || conflict.ownerPath.startsWith(`${conflict.componentRoot}/`))
1709
- || importChain.length < 2
1779
+ || (conflict.testInvocation
1780
+ ? !validLiteralTestInvocationEvidence(conflict) || importChain.length !== 1
1781
+ : importChain.length < 2 || importChain.at(-1) !== conflict.ownerPath)
1710
1782
  || importChain[0] !== conflict.runnerPath
1711
- || importChain.at(-1) !== conflict.ownerPath
1712
1783
  || !evidence.scopeExtensions.includes(conflict.ownerPath);
1713
1784
  })) {
1714
1785
  throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity v4 evidence has invalid component-owner bindings.', {
@@ -1744,13 +1815,54 @@ function validatePlanIntegrityDecision(event, task, file, line) {
1744
1815
  throw new WorkflowError('STATE_CORRUPT', 'Plan-integrity changedFiles must be normalized.', { file, line });
1745
1816
  }
1746
1817
  }
1818
+ /** A validated support decision survives only a hash-verified Knowledge-only Plan chain.
1819
+ * This preserves scope evidence, never execution authorization or a semantic replan. */
1820
+ export function isKnowledgeReboundSupportDecision(store, task, event) {
1821
+ const evidence = event.planIntegrityEvidence;
1822
+ if (!task.planHash || event.planHash === task.planHash || event.taskId !== task.id
1823
+ || event.recoveryBasis !== 'plan-integrity' || event.decision !== 'continue-fix'
1824
+ || (evidence?.kind !== 'check-support-anchor-outside-allowed-writes'
1825
+ && evidence?.kind !== 'failed-check-component-owner-outside-allowed-writes'))
1826
+ return false;
1827
+ const step = task.steps.find(s => s.id === event.stepId);
1828
+ if (!step || evidence.stepDefinitionHash !== hashCanonical({
1829
+ id: step.id, title: step.title, objective: step.objective, requirements: step.requirements,
1830
+ dependencies: step.dependencies, expectedOutputs: step.expectedOutputs,
1831
+ allowedWrites: step.allowedWrites, forbiddenScope: step.forbiddenScope, checks: step.checks,
1832
+ }) || evidence.scopeExtensions.some(p => pathAllowed(p, step.allowedWrites) || pathAllowed(p, step.forbiddenScope))
1833
+ || (evidence.kind === 'failed-check-component-owner-outside-allowed-writes'
1834
+ && evidence.semanticBoundaryHash !== semanticBoundaryHash(task)))
1835
+ return false;
1836
+ const root = store.taskRoot(task.projectId, task.id);
1837
+ let cursor = task.planHash;
1838
+ const visited = new Set();
1839
+ while (cursor !== event.planHash) {
1840
+ if (visited.has(cursor))
1841
+ return false;
1842
+ visited.add(cursor);
1843
+ const links = task.knowledgeRebinds.filter(link => link.reboundPlanHash === cursor);
1844
+ if (links.length !== 1 || !links[0].previousPlanHash)
1845
+ return false;
1846
+ const link = links[0];
1847
+ const before = readVerifiedPlanArtifact(root, link.previousPlanHash, task.planHash);
1848
+ const after = readVerifiedPlanArtifact(root, cursor, task.planHash);
1849
+ assertKnowledgeBindingOnlyPlanChange(before, after);
1850
+ const from = normalizePlanKnowledgeBinding(before, 'source').binding;
1851
+ const to = normalizePlanKnowledgeBinding(after, 'target').binding;
1852
+ if (from.revision !== link.fromKnowledgeMapRevision || from.hash !== link.fromKnowledgeMapHash
1853
+ || to.revision !== link.toKnowledgeMapRevision || to.hash !== link.toKnowledgeMapHash)
1854
+ return false;
1855
+ cursor = link.previousPlanHash;
1856
+ }
1857
+ return true;
1858
+ }
1747
1859
  export function effectiveStepAllowedWrites(store, task, stepId) {
1748
1860
  const step = task.steps.find((candidate) => candidate.id === stepId);
1749
1861
  if (!step)
1750
1862
  throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
1751
1863
  const extensions = readCorrectiveDecisionEvents(store, task)
1752
1864
  .filter((event) => event.stepId === stepId
1753
- && event.planHash === task.planHash
1865
+ && (event.planHash === task.planHash || isKnowledgeReboundSupportDecision(store, task, event))
1754
1866
  && event.decision === 'continue-fix'
1755
1867
  && event.recoveryBasis === 'plan-integrity')
1756
1868
  .flatMap((event) => event.planIntegrityEvidence?.kind === 'check-support-anchor-outside-allowed-writes'
@@ -2020,7 +2132,7 @@ function validateRemediationReviewReferences(store, task, events) {
2020
2132
  }
2021
2133
  }
2022
2134
  }
2023
- function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents) {
2135
+ function validateCorrectiveDecisionCoverage(store, task, decisions, remediationEvents) {
2024
2136
  const remediationEventsById = new Map(remediationEvents.map((event) => [event.eventId, event]));
2025
2137
  const decisionBindings = new Set();
2026
2138
  for (const decision of decisions) {
@@ -2071,9 +2183,14 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
2071
2183
  const bindings = evidence?.kind === 'missing-npm-script-outside-allowed-writes'
2072
2184
  ? [{ eventId: evidence.remediationEventId, eventHash: evidence.remediationEventHash, attemptOrdinal: 1 }]
2073
2185
  : evidence?.remediationEvents ?? [];
2074
- if (!evidence
2075
- || bindings.length !== priorEvents.length
2076
- || priorEvents.some((linked, index) => linked.failureKind !== 'checks-failed'
2186
+ const support = evidence?.kind === 'check-support-anchor-outside-allowed-writes'
2187
+ || evidence?.kind === 'failed-check-component-owner-outside-allowed-writes';
2188
+ const scoped = support && bindings[0]?.attemptOrdinal !== 1
2189
+ ? selectPlanIntegrityRemediations(store, { ...task, planHash: decision.planHash, revision: evidence.taskRevision }, priorEvents, decisions.filter(d => d.stepId === decision.stepId && d.triggeringAttemptCount < decision.triggeringAttemptCount), task.planHash)
2190
+ : priorEvents;
2191
+ if (!evidence || !scoped
2192
+ || bindings.length !== scoped.length
2193
+ || scoped.some((linked, index) => linked.failureKind !== 'checks-failed'
2077
2194
  || linked.eventId !== bindings[index]?.eventId
2078
2195
  || linked.eventHash !== bindings[index]?.eventHash
2079
2196
  || linked.attemptOrdinal !== bindings[index]?.attemptOrdinal)) {