codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14

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 (271) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +89 -29
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  19. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  20. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  21. package/dist/src/alpha6/milestone.d.ts +2 -0
  22. package/dist/src/alpha6/milestone.js +10 -1
  23. package/dist/src/alpha6/milestone.js.map +1 -1
  24. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  25. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  26. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  27. package/dist/src/alpha6/plan-integrity.js +7 -5
  28. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  29. package/dist/src/alpha6/remediation.d.ts +6 -21
  30. package/dist/src/alpha6/remediation.js +161 -476
  31. package/dist/src/alpha6/remediation.js.map +1 -1
  32. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  33. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  34. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  35. package/dist/src/beta1/project-transaction.d.ts +3 -1
  36. package/dist/src/beta1/project-transaction.js +6 -2
  37. package/dist/src/beta1/project-transaction.js.map +1 -1
  38. package/dist/src/checks/runner.d.ts +26 -0
  39. package/dist/src/checks/runner.js +223 -0
  40. package/dist/src/checks/runner.js.map +1 -0
  41. package/dist/src/checks/task-sync.d.ts +10 -0
  42. package/dist/src/checks/task-sync.js +41 -0
  43. package/dist/src/checks/task-sync.js.map +1 -0
  44. package/dist/src/checks/worker.d.ts +1 -0
  45. package/dist/src/checks/worker.js +154 -0
  46. package/dist/src/checks/worker.js.map +1 -0
  47. package/dist/src/cli-actions.d.ts +2 -2
  48. package/dist/src/cli-actions.js +3 -15
  49. package/dist/src/cli-actions.js.map +1 -1
  50. package/dist/src/cli.js +14 -98
  51. package/dist/src/cli.js.map +1 -1
  52. package/dist/src/contracts.d.ts +35 -32
  53. package/dist/src/dependency-provenance.d.ts +2 -2
  54. package/dist/src/dependency-provenance.js +12 -69
  55. package/dist/src/dependency-provenance.js.map +1 -1
  56. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  57. package/dist/src/domain/base-sync-conflict.js +32 -0
  58. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  59. package/dist/src/domain/step-start-admission.d.ts +4 -0
  60. package/dist/src/domain/step-start-admission.js +9 -0
  61. package/dist/src/domain/step-start-admission.js.map +1 -0
  62. package/dist/src/domain/virgin-registration.d.ts +3 -0
  63. package/dist/src/domain/virgin-registration.js +65 -0
  64. package/dist/src/domain/virgin-registration.js.map +1 -0
  65. package/dist/src/errors.d.ts +1 -1
  66. package/dist/src/errors.js.map +1 -1
  67. package/dist/src/gateway-handshake.js +0 -8
  68. package/dist/src/gateway-handshake.js.map +1 -1
  69. package/dist/src/git.d.ts +3 -1
  70. package/dist/src/git.js +41 -14
  71. package/dist/src/git.js.map +1 -1
  72. package/dist/src/graph.js +25 -3
  73. package/dist/src/graph.js.map +1 -1
  74. package/dist/src/index.d.ts +1 -0
  75. package/dist/src/navigation-actions.d.ts +9 -0
  76. package/dist/src/navigation-actions.js +72 -0
  77. package/dist/src/navigation-actions.js.map +1 -0
  78. package/dist/src/navigation-update-artifact.d.ts +5 -0
  79. package/dist/src/navigation-update-artifact.js +227 -0
  80. package/dist/src/navigation-update-artifact.js.map +1 -0
  81. package/dist/src/navigation-update.d.ts +39 -0
  82. package/dist/src/navigation-update.js +82 -0
  83. package/dist/src/navigation-update.js.map +1 -0
  84. package/dist/src/observation.js +52 -29
  85. package/dist/src/observation.js.map +1 -1
  86. package/dist/src/observed-routes.js +5 -6
  87. package/dist/src/observed-routes.js.map +1 -1
  88. package/dist/src/pending-review-update.d.ts +0 -13
  89. package/dist/src/pending-review-update.js +1 -6
  90. package/dist/src/pending-review-update.js.map +1 -1
  91. package/dist/src/reviewer.d.ts +1 -1
  92. package/dist/src/reviewer.js +20 -17
  93. package/dist/src/reviewer.js.map +1 -1
  94. package/dist/src/runtime.d.ts +3 -0
  95. package/dist/src/runtime.js +6 -0
  96. package/dist/src/runtime.js.map +1 -0
  97. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  98. package/dist/src/state/corrective-replan-executor.js +37 -1
  99. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  100. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  101. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  102. package/dist/src/state/corrective-replan-public.js +1 -1
  103. package/dist/src/state/corrective-replan-public.js.map +1 -1
  104. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  105. package/dist/src/state/corrective-replan-transaction.js +12 -11
  106. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  107. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  108. package/dist/src/state/corrective-yield-transaction.js +11 -10
  109. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  110. package/dist/src/state/lock.d.ts +4 -0
  111. package/dist/src/state/lock.js +21 -0
  112. package/dist/src/state/lock.js.map +1 -1
  113. package/dist/src/version.d.ts +1 -1
  114. package/dist/src/version.js +1 -1
  115. package/dist/src/version.js.map +1 -1
  116. package/dist/src/workflow-blocker-route.d.ts +8 -0
  117. package/dist/src/workflow-blocker-route.js +106 -0
  118. package/dist/src/workflow-blocker-route.js.map +1 -0
  119. package/dist/src/workflow.d.ts +48 -63
  120. package/dist/src/workflow.js +666 -1460
  121. package/dist/src/workflow.js.map +1 -1
  122. package/docs/autonomy-guardrails.md +26 -304
  123. package/docs/decisions.md +14 -112
  124. package/docs/development-flow.md +35 -238
  125. package/docs/project-memory.md +31 -50
  126. package/docs/release-app-evidence.md +190 -0
  127. package/docs/release.md +95 -388
  128. package/docs/updating-existing-project.md +19 -717
  129. package/package.json +11 -13
  130. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  131. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  132. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  133. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  134. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  135. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  136. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  137. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
  138. package/references/state-machine.md +4 -4
  139. package/roles/technical-planner.md +1 -1
  140. package/schemas/corrective-decision-event.schema.json +3 -1
  141. package/schemas/project-knowledge-map.schema.json +3 -1
  142. package/schemas/remediation-event.schema.json +10 -1
  143. package/schemas/task.schema.json +46 -2
  144. package/schemas/transition-payloads.schema.json +16 -1
  145. package/src/alpha6/adoption.ts +1123 -0
  146. package/src/alpha6/captured-check-evidence.ts +132 -0
  147. package/src/alpha6/check-support-anchor.ts +128 -0
  148. package/src/alpha6/component-owner.ts +168 -0
  149. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  150. package/src/alpha6/downstream-proof.ts +520 -0
  151. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  152. package/src/alpha6/handoff.ts +1443 -0
  153. package/src/alpha6/journal.ts +473 -0
  154. package/src/alpha6/literal-test-invocation.ts +84 -0
  155. package/src/alpha6/mechanical-feasibility.ts +488 -0
  156. package/src/alpha6/milestone.ts +2192 -0
  157. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  158. package/src/alpha6/npm-check-contract.ts +47 -0
  159. package/src/alpha6/plan-integrity.ts +298 -0
  160. package/src/alpha6/plan-risk.ts +1480 -0
  161. package/src/alpha6/preexecution-replan.ts +187 -0
  162. package/src/alpha6/remediation-cause.ts +98 -0
  163. package/src/alpha6/remediation.ts +2438 -0
  164. package/src/alpha6/review.ts +1198 -0
  165. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  166. package/src/alpha6/store-sidecars.ts +335 -0
  167. package/src/alpha7/autonomy.ts +411 -0
  168. package/src/alpha7/corrective-recovery.ts +1332 -0
  169. package/src/artifacts.ts +130 -0
  170. package/src/beta1/project-transaction.ts +355 -0
  171. package/src/change-explanation.ts +153 -0
  172. package/src/checks/runner.ts +245 -0
  173. package/src/checks/task-sync.ts +41 -0
  174. package/src/checks/worker.ts +149 -0
  175. package/src/cli-actions.ts +107 -0
  176. package/src/cli.ts +1457 -0
  177. package/src/contracts.ts +1494 -0
  178. package/src/credential-output.ts +89 -0
  179. package/src/credential-transport.ts +215 -0
  180. package/src/delegation.ts +190 -0
  181. package/src/dependency-provenance.ts +472 -0
  182. package/src/diagnostics.ts +93 -0
  183. package/src/domain/base-sync-conflict.ts +35 -0
  184. package/src/domain/completed-step-carryover.ts +76 -0
  185. package/src/domain/discovery.ts +27 -0
  186. package/src/domain/plan-semantics.ts +58 -0
  187. package/src/domain/step-start-admission.ts +10 -0
  188. package/src/domain/validation.ts +177 -0
  189. package/src/domain/virgin-registration.ts +46 -0
  190. package/src/errors.ts +24 -0
  191. package/src/fs-utils.ts +61 -0
  192. package/src/gateway-handshake.ts +95 -0
  193. package/src/git.ts +183 -0
  194. package/src/graph.ts +342 -0
  195. package/src/historical-step-provenance.ts +136 -0
  196. package/src/index.ts +23 -0
  197. package/src/lifecycle/canonical-hash.ts +28 -0
  198. package/src/lifecycle/catalog.ts +202 -0
  199. package/src/lifecycle/compiler-inspection.ts +29 -0
  200. package/src/lifecycle/core-static-readiness.ts +132 -0
  201. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  202. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  203. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  204. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  205. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  206. package/src/lifecycle/corrective-replan.ts +843 -0
  207. package/src/lifecycle/evaluator.ts +48 -0
  208. package/src/lifecycle/fingerprint.ts +488 -0
  209. package/src/lifecycle/immutable.ts +8 -0
  210. package/src/lifecycle/implementation-table.ts +118 -0
  211. package/src/lifecycle/index.ts +8 -0
  212. package/src/lifecycle/schema-artifact.ts +263 -0
  213. package/src/lifecycle/semantic-registry.ts +572 -0
  214. package/src/lifecycle/types.ts +838 -0
  215. package/src/memory.ts +273 -0
  216. package/src/migration.ts +161 -0
  217. package/src/navigation-actions.ts +70 -0
  218. package/src/navigation-update-artifact.ts +198 -0
  219. package/src/navigation-update.ts +121 -0
  220. package/src/observation.ts +225 -0
  221. package/src/observed-routes.ts +660 -0
  222. package/src/operational-contract.ts +125 -0
  223. package/src/pending-review-update.ts +175 -0
  224. package/src/repository.ts +99 -0
  225. package/src/reviewer.ts +1879 -0
  226. package/src/runtime.ts +6 -0
  227. package/src/state/corrective-replan-executor.ts +818 -0
  228. package/src/state/corrective-replan-public-schema.ts +83 -0
  229. package/src/state/corrective-replan-public.ts +908 -0
  230. package/src/state/corrective-replan-transaction.ts +949 -0
  231. package/src/state/corrective-yield-executor.ts +327 -0
  232. package/src/state/corrective-yield-transaction.ts +730 -0
  233. package/src/state/lock.ts +902 -0
  234. package/src/state/store.ts +567 -0
  235. package/src/transition-core.ts +330 -0
  236. package/src/ulid.ts +24 -0
  237. package/src/version.ts +2 -0
  238. package/src/workflow-blocker-route.ts +109 -0
  239. package/src/workflow.ts +10172 -0
  240. package/docs/alpha7.1-implementation-brief.md +0 -268
  241. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  242. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  243. package/docs/beta1-stabilization-brief.md +0 -165
  244. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  245. package/docs/beta13.2-signal-review-recovery.md +0 -38
  246. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  247. package/docs/change-model.md +0 -118
  248. package/docs/delegated-approval.md +0 -254
  249. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  250. package/docs/pdf/README.md +0 -24
  251. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  252. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  253. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  254. package/docs/pdf/requirements.txt +0 -1
  255. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  256. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  257. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  258. package/docs/pending-review-update.md +0 -15
  259. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  260. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  261. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  262. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  263. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  264. package/docs/problem-briefs/README.md +0 -120
  265. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  266. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  267. package/docs/split-required-recovery.md +0 -47
  268. package/docs/stable-release-defect-register.md +0 -730
  269. package/docs/validation-report.md +0 -182
  270. package/scripts/generate-pdf-docs.py +0 -524
  271. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,472 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import type {
4
+ DependencyProvenanceRecoveryPreflight,
5
+ HistoricalStepDependencyCompatibility,
6
+ ProjectKnowledgeMap,
7
+ PlanIntegrityUpdateCompatibility,
8
+ TaskState,
9
+ } from './contracts.js';
10
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
11
+ import { changedFiles, currentBranch, headCommit, runGit } from './repository.js';
12
+ import { hashDirtyWorktree } from './alpha6/downstream-proof.js';
13
+ import { inspectWorkflowVersions } from './diagnostics.js';
14
+
15
+ interface RecoveryEnvironment {
16
+ activeLeaseCount: number;
17
+ staleLeaseCount: number;
18
+ pendingTransactionCount: number;
19
+ corruptTransactionCount: number;
20
+ coreOperationCount: number;
21
+ verifiedHistoricalStepCommits: string[];
22
+ versions: {
23
+ runtime: string;
24
+ declared: string | null;
25
+ locked: string | null;
26
+ installed: string | null;
27
+ currentBranch: string | null;
28
+ baseBranch: string | null;
29
+ };
30
+ }
31
+
32
+ export interface ActiveDownstreamProofCompatibility {
33
+ activeStepId: string;
34
+ planHash: string;
35
+ dirtyFiles: string[];
36
+ dirtyWorktreeHash: string;
37
+ invalidatedStepIds: string[];
38
+ }
39
+
40
+ const RECOVERY_FILES = ['package-lock.json', 'package.json'] as const;
41
+ const TERMINAL_TASK_STATUSES = new Set<TaskState['status']>(['merged', 'cancelled']);
42
+
43
+ export interface DependencyKnowledgeRefreshAssessment {
44
+ eligible: boolean;
45
+ changedKnowledgePaths: string[];
46
+ dependencyCommits: string[];
47
+ sourcePackageVersion: string | null;
48
+ targetPackageVersion: string;
49
+ blockers: string[];
50
+ }
51
+
52
+ /**
53
+ * Proves that a stale approved Knowledge Map differs only because one or more
54
+ * consecutive package.json edits changed the exact codex-workflow-v2 pin. The
55
+ * intervening repository history may contain ordinary product commits, but
56
+ * every commit that touched package.json after the approved content must be a
57
+ * package.json/package-lock.json-only dependency update.
58
+ */
59
+ export function inspectDependencyKnowledgeRefresh(
60
+ repositoryRoot: string,
61
+ milestoneBaseBranch: string,
62
+ approved: ProjectKnowledgeMap,
63
+ inspected: ProjectKnowledgeMap,
64
+ allowedDirtyFiles: readonly string[] = [],
65
+ ): DependencyKnowledgeRefreshAssessment {
66
+ const blockers: string[] = [];
67
+ const approvedByPath = new Map(approved.entries.map((entry) => [entry.path, entry]));
68
+ const inspectedByPath = new Map(inspected.entries.map((entry) => [entry.path, entry]));
69
+ const changedKnowledgePaths = [...new Set([
70
+ ...approved.entries.map((entry) => entry.path),
71
+ ...inspected.entries.map((entry) => entry.path),
72
+ ])].filter((candidatePath) => {
73
+ const before = approvedByPath.get(candidatePath);
74
+ const after = inspectedByPath.get(candidatePath);
75
+ return !before || !after
76
+ || before.id !== after.id
77
+ || before.category !== after.category
78
+ || before.scope !== after.scope
79
+ || before.authority !== after.authority
80
+ || before.discoveredBy !== after.discoveredBy
81
+ || before.contentHash !== after.contentHash;
82
+ }).sort();
83
+ if (approved.status !== 'active' || !approved.approval) {
84
+ blockers.push('Dependency Knowledge refresh requires a previously approved active map.');
85
+ }
86
+ if (inspected.status !== 'stale') {
87
+ blockers.push('Dependency Knowledge refresh requires an inspected stale map.');
88
+ }
89
+ if (JSON.stringify(changedKnowledgePaths) !== JSON.stringify(['package.json'])) {
90
+ blockers.push('Only package.json may differ in Project Knowledge.');
91
+ }
92
+
93
+ const versions = inspectWorkflowVersions(repositoryRoot, [milestoneBaseBranch]);
94
+ const versionSurfaces = [
95
+ versions.runtime,
96
+ versions.declared,
97
+ versions.locked,
98
+ versions.installed,
99
+ versions.currentBranch,
100
+ ...versions.milestoneBases.map((entry) => entry.version),
101
+ ];
102
+ if (versionSurfaces.some((version) => version !== PACKAGE_VERSION)) {
103
+ blockers.push(`Every Workflow version surface must equal ${PACKAGE_VERSION}.`);
104
+ }
105
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
106
+ if (JSON.stringify(dirtyFiles) !== JSON.stringify([...allowedDirtyFiles].sort())) {
107
+ blockers.push(allowedDirtyFiles.length === 0
108
+ ? 'Dependency Knowledge refresh requires a clean checkout.'
109
+ : 'Dependency Knowledge refresh dirty files changed outside the bound compatibility allowlist.');
110
+ }
111
+
112
+ const approvedPackage = approvedByPath.get('package.json') ?? null;
113
+ const dependencyCommits: string[] = [];
114
+ let sourcePackageVersion: string | null = null;
115
+ if (!approvedPackage) {
116
+ blockers.push('The approved Knowledge Map does not contain package.json.');
117
+ } else {
118
+ try {
119
+ const historyOutput = runGit(repositoryRoot, ['rev-list', 'HEAD', '--', 'package.json']);
120
+ const packageHistory = historyOutput ? historyOutput.split('\n') : [];
121
+ const sourceIndex = packageHistory.findIndex((commit) =>
122
+ sha256(readGitFile(repositoryRoot, commit, 'package.json')) === approvedPackage.contentHash);
123
+ if (sourceIndex < 0) {
124
+ blockers.push('Approved package.json content is not present in current Git ancestry.');
125
+ } else {
126
+ sourcePackageVersion = manifestDependencyVersion(
127
+ parseJson(readGitFile(repositoryRoot, packageHistory[sourceIndex]!, 'package.json')),
128
+ );
129
+ for (const commit of packageHistory.slice(0, sourceIndex).reverse()) {
130
+ const parent = runGit(repositoryRoot, ['rev-parse', `${commit}^`]);
131
+ const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
132
+ const files = filesOutput ? filesOutput.split('\n').sort() : [];
133
+ if (JSON.stringify(files) !== JSON.stringify(RECOVERY_FILES)) {
134
+ blockers.push(`Knowledge-changing commit ${commit} is not dependency-only.`);
135
+ continue;
136
+ }
137
+ if (!workflowDependencyJsonDiffIsBounded(repositoryRoot, parent, commit, null)) {
138
+ blockers.push(`Knowledge-changing commit ${commit} changes fields beyond the Workflow dependency.`);
139
+ continue;
140
+ }
141
+ dependencyCommits.push(commit);
142
+ }
143
+ if (dependencyCommits.length === 0) {
144
+ blockers.push('No bounded Workflow dependency commit explains the Knowledge drift.');
145
+ }
146
+ }
147
+ } catch {
148
+ blockers.push('Dependency Knowledge refresh Git ancestry could not be verified.');
149
+ }
150
+ }
151
+
152
+ return {
153
+ eligible: blockers.length === 0,
154
+ changedKnowledgePaths,
155
+ dependencyCommits,
156
+ sourcePackageVersion,
157
+ targetPackageVersion: PACKAGE_VERSION,
158
+ blockers,
159
+ };
160
+ }
161
+
162
+ export function inspectDependencyProvenanceRecovery(
163
+ repositoryRoot: string,
164
+ task: TaskState,
165
+ environment: RecoveryEnvironment,
166
+ activeDownstreamProof: ActiveDownstreamProofCompatibility | null = null,
167
+ historicalStepProvenance: HistoricalStepDependencyCompatibility | null = null,
168
+ retainedNavigationCarryover: PlanIntegrityUpdateCompatibility | null = null,
169
+ ): DependencyProvenanceRecoveryPreflight {
170
+ const blockers: string[] = [];
171
+ const dirty = changedFiles(repositoryRoot);
172
+ const head = headCommit(repositoryRoot);
173
+ let parent: string | null = null;
174
+ let commitFiles: string[] = [];
175
+ let dependencyCommits: Array<{
176
+ commitSha: string;
177
+ parentCommitSha: string;
178
+ packageVersion: string;
179
+ }> = [];
180
+ let retainedHistoricalCommits: string[] = [];
181
+
182
+ const compatibilityCount = [activeDownstreamProof, historicalStepProvenance, retainedNavigationCarryover].filter(Boolean).length;
183
+ if (compatibilityCount > 1) blockers.push('Dependency recovery accepts exactly one dirty compatibility binding.');
184
+
185
+ const dirtyCompatibility = activeDownstreamProof ?? historicalStepProvenance ?? retainedNavigationCarryover;
186
+ if (dirtyCompatibility) {
187
+ if (JSON.stringify(dirty.sort()) !== JSON.stringify([...dirtyCompatibility.dirtyFiles].sort())) {
188
+ blockers.push('Dirty files changed after the compatibility assessment.');
189
+ }
190
+ try {
191
+ if (hashDirtyWorktree(repositoryRoot, dirty) !== dirtyCompatibility.dirtyWorktreeHash) {
192
+ blockers.push('Dirty file content changed after the compatibility assessment.');
193
+ }
194
+ } catch (error) {
195
+ blockers.push(error instanceof Error ? error.message : String(error));
196
+ }
197
+ if (activeDownstreamProof) {
198
+ if (!task.planHash || task.planHash !== activeDownstreamProof.planHash) {
199
+ blockers.push('The current Task Plan must match the active downstream-proof compatibility binding.');
200
+ }
201
+ const inProgressSteps = task.steps.filter((step) => step.status === 'in_progress');
202
+ if (inProgressSteps.length !== 1 || inProgressSteps[0]?.id !== activeDownstreamProof.activeStepId) {
203
+ blockers.push('Exactly the bound downstream-proof Step must remain in progress.');
204
+ }
205
+ } else if (historicalStepProvenance) {
206
+ if (task.status !== 'in_progress' || task.planHash !== historicalStepProvenance.planHash) {
207
+ blockers.push('The current in-progress Task Plan must match the historical Step compatibility binding.');
208
+ }
209
+ const inProgressSteps = task.steps.filter((step) => step.status === 'in_progress');
210
+ if (inProgressSteps.length !== 1 || inProgressSteps[0]?.id !== historicalStepProvenance.activeStepId) {
211
+ blockers.push('Exactly the bound corrective Step must remain in progress.');
212
+ }
213
+
214
+ }
215
+ } else {
216
+ if (dirty.length > 0) blockers.push('Repository checkout must be clean.');
217
+ }
218
+ if (task.workspaceOwner !== 'local') blockers.push('Recovery is limited to a local Task workspace.');
219
+ if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
220
+ blockers.push('The current branch must be the recorded Task branch.');
221
+ }
222
+ if (!task.baseCommit) blockers.push('The Task must have a recorded base commit.');
223
+ if (TERMINAL_TASK_STATUSES.has(task.status)) blockers.push('Terminal Tasks cannot recover dependency provenance.');
224
+ if (!dirtyCompatibility && task.steps.some((step) => step.status === 'in_progress')) {
225
+ blockers.push('No Step may be in progress.');
226
+ }
227
+ if (environment.activeLeaseCount > 0) blockers.push('No active writer lease may exist.');
228
+ if (environment.staleLeaseCount > 0) blockers.push('No stale writer lease may exist.');
229
+ if (environment.pendingTransactionCount > 0) blockers.push('No pending Workflow transaction may exist.');
230
+ if (environment.corruptTransactionCount > 0) blockers.push('No corrupt Workflow transaction may exist.');
231
+ if (environment.coreOperationCount > 0) blockers.push('No Core Task operation may exist.');
232
+
233
+ for (const [surface, version] of Object.entries(environment.versions)) {
234
+ if (version !== PACKAGE_VERSION) blockers.push(`${surface} Workflow version must equal ${PACKAGE_VERSION}.`);
235
+ }
236
+
237
+ try {
238
+ if (task.systemCommits.includes(head)) blockers.push('The candidate commit is already registered.');
239
+ if (task.baseCommit) {
240
+ const outputHistory = runGit(repositoryRoot, ['rev-list', '--reverse', `${task.baseCommit}..HEAD`]);
241
+ const history = outputHistory ? outputHistory.split('\n') : [];
242
+ const expectedRegistered = [
243
+ ...task.steps.flatMap((step) => (step.evidence ? [step.evidence.commitSha] : [])),
244
+ ...task.systemCommits,
245
+ ...(task.invalidatedStepCommits ?? []),
246
+ ...(task.dependencyProvenanceRecoveries ?? []).flatMap((recovery) => recovery.retainedHistoricalCommits ?? []),
247
+ ...environment.verifiedHistoricalStepCommits.filter((commit) => history.includes(commit)),
248
+ ];
249
+ const expectedSet = new Set(expectedRegistered);
250
+ const candidateStart = history.findIndex((commit) => !expectedSet.has(commit));
251
+ const registeredPrefix = candidateStart < 0 ? history : history.slice(0, candidateStart);
252
+ const candidateChain = candidateStart < 0 ? [] : history.slice(candidateStart);
253
+ retainedHistoricalCommits = environment.verifiedHistoricalStepCommits.filter((commit) =>
254
+ registeredPrefix.includes(commit)
255
+ && !task.systemCommits.includes(commit)
256
+ && !task.steps.some((step) => step.evidence?.commitSha === commit)
257
+ );
258
+ if (historicalStepProvenance
259
+ && !retainedHistoricalCommits.includes(historicalStepProvenance.retainedCommitSha)) {
260
+ blockers.push('The dependency candidate no longer retains the bound reviewed historical Step commit.');
261
+ }
262
+ if (history.at(-1) !== head || !sameCommitSet(registeredPrefix, [...new Set(expectedRegistered)])) {
263
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
264
+ }
265
+ parent = registeredPrefix.at(-1) ?? task.baseCommit;
266
+ let expectedParent = parent;
267
+ for (const commit of candidateChain) {
268
+ const inspected = inspectBoundedWorkflowDependencyCommit(repositoryRoot, commit);
269
+ if (!inspected) {
270
+ blockers.push(`Unregistered commit ${commit} is not a consecutive bounded Workflow dependency update.`);
271
+ blockers.push('The candidate commit must change exactly package.json and package-lock.json and only the Workflow dependency pin.');
272
+ blockers.push(`The candidate commit contains changes beyond the exact ${PACKAGE_NAME}@${PACKAGE_VERSION} dependency update.`);
273
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
274
+ continue;
275
+ }
276
+ if (inspected.parentCommitSha !== expectedParent) {
277
+ blockers.push(`Unregistered commit ${commit} does not continue the bounded Workflow dependency parent chain.`);
278
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
279
+ continue;
280
+ }
281
+ dependencyCommits.push(inspected);
282
+ expectedParent = commit;
283
+ }
284
+ if (dependencyCommits.length !== candidateChain.length || dependencyCommits.at(-1)?.commitSha !== head) {
285
+ blockers.push('The complete unregistered suffix must be a bounded Workflow dependency-only chain ending at HEAD.');
286
+ }
287
+ if (dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION) {
288
+ blockers.push(`The dependency-only chain must end at ${PACKAGE_NAME}@${PACKAGE_VERSION}.`);
289
+ }
290
+ }
291
+ commitFiles = dependencyCommits.length > 0 ? [...RECOVERY_FILES] : [];
292
+ } catch {
293
+ blockers.push('The candidate dependency commit could not be inspected.');
294
+ }
295
+
296
+ if (retainedNavigationCarryover && (dependencyCommits.length !== 1 || task.status !== 'ready'
297
+ || task.steps.some(step => step.status === 'in_progress')
298
+ || !task.steps.some(step => step.id === retainedNavigationCarryover.stepId && step.status === 'planned' && step.evidence === null))) {
299
+ blockers.push('Retained navigation carryover requires one dependency commit and the unchanged ready/planned Step.');
300
+ }
301
+
302
+ return {
303
+ eligible: blockers.length === 0,
304
+ action: blockers.length === 0
305
+ ? activeDownstreamProof
306
+ ? 'update downstream-proof-dependency-recover'
307
+ : historicalStepProvenance
308
+ ? 'update historical-step-dependency-recover'
309
+ : 'update dependency-provenance-recover'
310
+ : null,
311
+ projectId: task.projectId,
312
+ taskId: task.id,
313
+ taskRevision: task.revision,
314
+ commitSha: blockers.length === 0 ? head : null,
315
+ parentCommitSha: blockers.length === 0 ? parent : null,
316
+ packageVersion: PACKAGE_VERSION,
317
+ changedFiles: commitFiles,
318
+ dependencyCommits,
319
+ retainedHistoricalCommits,
320
+ activeDownstreamProof,
321
+ historicalStepProvenance,
322
+ ...(retainedNavigationCarryover ? { retainedNavigationCarryover } : {}),
323
+ blockers,
324
+ };
325
+ }
326
+
327
+ function sameCommitSet(actual: string[], expected: string[]): boolean {
328
+ return actual.length === expected.length
329
+ && actual.every((commit) => expected.includes(commit))
330
+ && expected.every((commit) => actual.includes(commit));
331
+ }
332
+
333
+ export function inspectBoundedWorkflowDependencyCommit(
334
+ repositoryRoot: string,
335
+ commitSha: string,
336
+ ): { commitSha: string; parentCommitSha: string; packageVersion: string } | null {
337
+ try {
338
+ const parentCommitSha = runGit(repositoryRoot, ['rev-parse', `${commitSha}^`]);
339
+ const output = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commitSha]);
340
+ const files = output ? output.split('\n').sort() : [];
341
+ if (JSON.stringify(files) !== JSON.stringify(RECOVERY_FILES)) return null;
342
+ const packageVersion = manifestDependencyVersion(parseJson(runGit(repositoryRoot, ['show', `${commitSha}:package.json`])));
343
+ if (!packageVersion || !workflowDependencyJsonDiffIsBounded(
344
+ repositoryRoot,
345
+ parentCommitSha,
346
+ commitSha,
347
+ packageVersion,
348
+ )) return null;
349
+ return { commitSha, parentCommitSha, packageVersion };
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ export function workflowDependencyVersionAt(repositoryRoot: string, commitSha: string): string | null {
356
+ try {
357
+ return manifestDependencyVersion(parseJson(runGit(repositoryRoot, ['show', `${commitSha}:package.json`])));
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+
363
+ function workflowDependencyJsonDiffIsBounded(
364
+ repositoryRoot: string,
365
+ parent: string,
366
+ revision: string,
367
+ expectedAfterVersion: string | null,
368
+ ): boolean {
369
+ const beforeManifestContent = runGit(repositoryRoot, ['show', `${parent}:package.json`]);
370
+ const afterManifestContent = runGit(repositoryRoot, ['show', `${revision}:package.json`]);
371
+ const beforeLockContent = runGit(repositoryRoot, ['show', `${parent}:package-lock.json`]);
372
+ const afterLockContent = runGit(repositoryRoot, ['show', `${revision}:package-lock.json`]);
373
+ const beforeManifest = parseJson(beforeManifestContent);
374
+ const afterManifest = parseJson(afterManifestContent);
375
+ const beforeLock = parseJson(beforeLockContent);
376
+ const afterLock = parseJson(afterLockContent);
377
+ if (!beforeManifest || !afterManifest || !beforeLock || !afterLock) return false;
378
+ const beforeManifestDependencies = manifestDependencyValues(beforeManifest);
379
+ const beforeVersion = beforeManifestDependencies.length === 1 ? beforeManifestDependencies[0] : null;
380
+ const afterVersion = manifestDependencyVersion(afterManifest);
381
+ if (!beforeVersion || !afterVersion || beforeVersion === afterVersion) return false;
382
+ if (!targetsExactDevDependency(beforeManifest, beforeVersion)) return false;
383
+ if (!targetsExactDevDependency(afterManifest, afterVersion)) return false;
384
+ if (expectedAfterVersion !== null && afterVersion !== expectedAfterVersion) return false;
385
+ if (!lockTargetsExactVersion(beforeLock, beforeVersion)) return false;
386
+ if (!lockTargetsExactVersion(afterLock, afterVersion)) return false;
387
+ return JSON.stringify(stripManifestDependency(beforeManifest)) === JSON.stringify(stripManifestDependency(afterManifest))
388
+ && JSON.stringify(stripLockDependency(beforeLock)) === JSON.stringify(stripLockDependency(afterLock));
389
+ }
390
+
391
+ function parseJson(content: string): Record<string, unknown> | null {
392
+ try {
393
+ const value = JSON.parse(content) as unknown;
394
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+
400
+ function manifestDependencyValues(manifest: Record<string, unknown>): string[] {
401
+ const values: string[] = [];
402
+ for (const key of ['devDependencies', 'dependencies']) {
403
+ const group = record(manifest[key]);
404
+ if (typeof group?.[PACKAGE_NAME] === 'string') values.push(group[PACKAGE_NAME] as string);
405
+ }
406
+ return values;
407
+ }
408
+
409
+ function manifestDependencyVersion(manifest: Record<string, unknown> | null): string | null {
410
+ if (!manifest) return null;
411
+ const values = manifestDependencyValues(manifest);
412
+ return values.length === 1 ? values[0]! : null;
413
+ }
414
+
415
+ function lockTargetsExactVersion(lock: Record<string, unknown>, version: string): boolean {
416
+ const packages = record(lock.packages);
417
+ const entry = record(packages?.[`node_modules/${PACKAGE_NAME}`]);
418
+ const root = record(packages?.['']);
419
+ if (entry?.version !== version || !targetsExactDevDependency(root ?? {}, version)) return false;
420
+ const dependencies = record(lock.dependencies);
421
+ const legacyEntry = record(dependencies?.[PACKAGE_NAME]);
422
+ return !legacyEntry || legacyEntry.version === version;
423
+ }
424
+
425
+ function targetsExactDevDependency(manifest: Record<string, unknown>, version: string): boolean {
426
+ const devDependencies = record(manifest.devDependencies);
427
+ const dependencies = record(manifest.dependencies);
428
+ return devDependencies?.[PACKAGE_NAME] === version && dependencies?.[PACKAGE_NAME] === undefined;
429
+ }
430
+
431
+ function stripManifestDependency(source: Record<string, unknown>): Record<string, unknown> {
432
+ const clone = structuredClone(source);
433
+ for (const key of ['devDependencies', 'dependencies']) {
434
+ const group = record(clone[key]);
435
+ if (group) delete group[PACKAGE_NAME];
436
+ }
437
+ return clone;
438
+ }
439
+
440
+ function stripLockDependency(source: Record<string, unknown>): Record<string, unknown> {
441
+ const clone = structuredClone(source);
442
+ const packages = record(clone.packages);
443
+ if (packages) {
444
+ delete packages[`node_modules/${PACKAGE_NAME}`];
445
+ const root = record(packages['']);
446
+ if (root) {
447
+ for (const key of ['devDependencies', 'dependencies']) {
448
+ const group = record(root[key]);
449
+ if (group) delete group[PACKAGE_NAME];
450
+ }
451
+ }
452
+ }
453
+ const dependencies = record(clone.dependencies);
454
+ if (dependencies) delete dependencies[PACKAGE_NAME];
455
+ return clone;
456
+ }
457
+
458
+ function record(value: unknown): Record<string, unknown> | null {
459
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
460
+ }
461
+
462
+ function sha256(value: string): string {
463
+ return createHash('sha256').update(value).digest('hex');
464
+ }
465
+
466
+ function readGitFile(repositoryRoot: string, revision: string, filePath: string): string {
467
+ const result = spawnSync('git', ['-C', repositoryRoot, 'show', `${revision}:${filePath}`], {
468
+ encoding: 'utf8',
469
+ });
470
+ if (result.status !== 0) throw new Error(result.stderr.trim() || `Cannot read ${revision}:${filePath}.`);
471
+ return result.stdout;
472
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inspectGraphView } from './graph.js';
4
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
5
+ import { runGit } from './repository.js';
6
+
7
+ export interface DependencyDiagnostic {
8
+ packageFile: string;
9
+ declared: string | null;
10
+ exact: boolean;
11
+ current: boolean;
12
+ }
13
+
14
+ export function inspectLocalDependency(repositoryRoot: string): DependencyDiagnostic {
15
+ const packageFile = path.join(repositoryRoot, 'package.json');
16
+ if (!existsSync(packageFile)) return { packageFile, declared: null, exact: false, current: false };
17
+ const manifest = JSON.parse(readFileSync(packageFile, 'utf8')) as {
18
+ dependencies?: Record<string, string>;
19
+ devDependencies?: Record<string, string>;
20
+ };
21
+ const declared = manifest.devDependencies?.[PACKAGE_NAME] ?? manifest.dependencies?.[PACKAGE_NAME] ?? null;
22
+ return {
23
+ packageFile,
24
+ declared,
25
+ exact: declared === PACKAGE_VERSION,
26
+ current: declared === PACKAGE_VERSION,
27
+ };
28
+ }
29
+
30
+ export function integrationDiagnostics(repositoryRoot: string) {
31
+ return {
32
+ package: { name: PACKAGE_NAME, version: PACKAGE_VERSION },
33
+ localDependency: inspectLocalDependency(repositoryRoot),
34
+ graphView: inspectGraphView(),
35
+ };
36
+ }
37
+
38
+ export function inspectWorkflowVersions(repositoryRoot: string, baseBranches: string[]) {
39
+ const packageFile = path.join(repositoryRoot, 'package.json');
40
+ const lockFile = path.join(repositoryRoot, 'package-lock.json');
41
+ const declared = existsSync(packageFile) ? dependencyFromManifest(readFileSync(packageFile, 'utf8')) : null;
42
+ const locked = existsSync(lockFile) ? dependencyFromLock(readFileSync(lockFile, 'utf8')) : null;
43
+ const installedFile = path.join(repositoryRoot, 'node_modules', PACKAGE_NAME, 'package.json');
44
+ const installed = existsSync(installedFile) ? manifestVersion(readFileSync(installedFile, 'utf8')) : null;
45
+ const currentBranch = gitManifestDependency(repositoryRoot, 'HEAD');
46
+ const milestoneBases = [...new Set(baseBranches)].map((branch) => ({
47
+ branch,
48
+ version: gitManifestDependency(repositoryRoot, branch),
49
+ }));
50
+ return { runtime: PACKAGE_VERSION, declared, locked, installed, currentBranch, milestoneBases };
51
+ }
52
+
53
+ function dependencyFromManifest(content: string): string | null {
54
+ try {
55
+ const manifest = JSON.parse(content) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
56
+ return manifest.devDependencies?.[PACKAGE_NAME] ?? manifest.dependencies?.[PACKAGE_NAME] ?? null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function dependencyFromLock(content: string): string | null {
63
+ try {
64
+ const lock = JSON.parse(content) as {
65
+ packages?: Record<string, { version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }>;
66
+ dependencies?: Record<string, { version?: string }>;
67
+ };
68
+ return lock.packages?.[`node_modules/${PACKAGE_NAME}`]?.version
69
+ ?? lock.packages?.['']?.devDependencies?.[PACKAGE_NAME]
70
+ ?? lock.packages?.['']?.dependencies?.[PACKAGE_NAME]
71
+ ?? lock.dependencies?.[PACKAGE_NAME]?.version
72
+ ?? null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function manifestVersion(content: string): string | null {
79
+ try {
80
+ const value = (JSON.parse(content) as { version?: unknown }).version;
81
+ return typeof value === 'string' ? value : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function gitManifestDependency(repositoryRoot: string, revision: string): string | null {
88
+ try {
89
+ return dependencyFromManifest(runGit(repositoryRoot, ['show', `${revision}:package.json`]));
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync, lstatSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { TaskState } from '../contracts.js';
4
+ import { WorkflowError } from '../errors.js';
5
+ import { readJson, writeJsonAtomic } from '../fs-utils.js';
6
+ import type { FileStateStore } from '../state/store.js';
7
+
8
+ type Conflict = { protocol: 'base-sync-conflict-v1'; taskId: string; taskRevision: number; taskHead: string; baseHead: string };
9
+ const fileFor = (store: FileStateStore, task: TaskState) => path.join(store.taskRoot(task.projectId, task.id), 'base-sync-conflict.json');
10
+
11
+ /** A negative receipt can suppress a repeated attempt, never authorize Git work. */
12
+ export function currentBaseSyncConflict(store: FileStateStore, task: TaskState, taskHead: string, baseHead: string): Conflict | null {
13
+ const file = fileFor(store, task);
14
+ if (!existsSync(file)) return null;
15
+ const stat = lstatSync(file);
16
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2048) throw new WorkflowError('STATE_CORRUPT', 'Invalid base-sync conflict receipt.');
17
+ const value = readJson<Conflict>(file);
18
+ if (!value || Object.keys(value).sort().join(',') !== 'baseHead,protocol,taskHead,taskId,taskRevision'
19
+ || value.protocol !== 'base-sync-conflict-v1' || value.taskId !== task.id
20
+ || !Number.isSafeInteger(value.taskRevision) || value.taskRevision < 1
21
+ || typeof value.taskHead !== 'string' || typeof value.baseHead !== 'string'
22
+ || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.taskHead) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.baseHead)) {
23
+ throw new WorkflowError('STATE_CORRUPT', 'Invalid base-sync conflict receipt.');
24
+ }
25
+ if (value.taskRevision === task.revision && value.taskHead !== taskHead) {
26
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Reviewed Task HEAD changed after a base-sync conflict; retained review authority cannot be reused.');
27
+ }
28
+ return value.taskRevision === task.revision && value.taskHead === taskHead && value.baseHead === baseHead ? value : null;
29
+ }
30
+
31
+ export function recordBaseSyncConflict(store: FileStateStore, task: TaskState, taskHead: string, baseHead: string): void {
32
+ // Validate existing evidence before replacing a stale negative receipt.
33
+ currentBaseSyncConflict(store, task, taskHead, baseHead);
34
+ writeJsonAtomic(fileFor(store, task), { protocol: 'base-sync-conflict-v1', taskId: task.id, taskRevision: task.revision, taskHead, baseHead } satisfies Conflict);
35
+ }
@@ -0,0 +1,76 @@
1
+ import type { PlanStepInput, TaskPlanInput, TaskState } from '../contracts.js';
2
+
3
+ type TaskStep = TaskState['steps'][number];
4
+
5
+ /**
6
+ * Derive the completed Steps whose authority remains valid under a replacement Plan.
7
+ *
8
+ * A completed Step is retained only when its executable definition is unchanged and
9
+ * every dependency is itself retained. The recursive dependency check makes the
10
+ * result independent from candidate Plan ordering and fails closed on malformed
11
+ * dependency cycles even though ordinary Plan validation rejects cycles earlier.
12
+ */
13
+ export function deriveCompletedStepCarryover(
14
+ task: TaskState,
15
+ candidateSteps: readonly TaskPlanInput['steps'][number][],
16
+ ): ReadonlyMap<string, TaskStep> {
17
+ const duplicateCandidateIds = new Set<string>();
18
+ const candidateById = new Map<string, PlanStepInput>();
19
+ for (const candidate of candidateSteps) {
20
+ if (candidateById.has(candidate.id)) duplicateCandidateIds.add(candidate.id);
21
+ else candidateById.set(candidate.id, normalizePlanStepDefinition(candidate));
22
+ }
23
+
24
+ const exactCompletedById = new Map<string, TaskStep>();
25
+ for (const previous of task.steps) {
26
+ if (previous.status !== 'completed' || duplicateCandidateIds.has(previous.id)) continue;
27
+ const candidate = candidateById.get(previous.id);
28
+ if (!candidate) continue;
29
+ if (samePlanStepDefinition(previous, candidate)) exactCompletedById.set(previous.id, previous);
30
+ }
31
+
32
+ const retained = new Map<string, TaskStep>();
33
+ const rejected = new Set<string>();
34
+ const visiting = new Set<string>();
35
+ const retain = (stepId: string): boolean => {
36
+ if (retained.has(stepId)) return true;
37
+ if (rejected.has(stepId) || visiting.has(stepId)) return false;
38
+ const previous = exactCompletedById.get(stepId);
39
+ if (!previous) {
40
+ rejected.add(stepId);
41
+ return false;
42
+ }
43
+ visiting.add(stepId);
44
+ const dependenciesRetained = previous.dependencies.every((dependencyId) => retain(dependencyId));
45
+ visiting.delete(stepId);
46
+ if (!dependenciesRetained) {
47
+ rejected.add(stepId);
48
+ return false;
49
+ }
50
+ retained.set(stepId, previous);
51
+ return true;
52
+ };
53
+
54
+ for (const stepId of exactCompletedById.keys()) retain(stepId);
55
+ return retained;
56
+ }
57
+
58
+ export function normalizePlanStepDefinition(
59
+ step: PlanStepInput | TaskStep,
60
+ ): PlanStepInput {
61
+ return {
62
+ id: step.id,
63
+ title: step.title,
64
+ objective: step.objective,
65
+ requirements: [...step.requirements],
66
+ dependencies: [...step.dependencies],
67
+ expectedOutputs: [...step.expectedOutputs],
68
+ allowedWrites: [...step.allowedWrites],
69
+ forbiddenScope: [...step.forbiddenScope],
70
+ checks: [...step.checks],
71
+ };
72
+ }
73
+
74
+ function samePlanStepDefinition(previous: TaskStep, candidate: PlanStepInput): boolean {
75
+ return JSON.stringify(normalizePlanStepDefinition(previous)) === JSON.stringify(candidate);
76
+ }