codex-workflow-v2 2.0.0-beta.13.14 → 2.0.0-beta.13.16

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 (125) hide show
  1. package/README.md +17 -0
  2. package/dist/reviewer-runtime-build.json +20 -8
  3. package/dist/src/alpha6/captured-check-evidence.d.ts +49 -0
  4. package/dist/src/alpha6/captured-check-evidence.js +146 -0
  5. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  6. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  7. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  8. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  9. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +3 -1
  10. package/dist/src/alpha6/root-cause-replan-carryover.js +4 -0
  11. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  12. package/dist/src/contracts.d.ts +5 -1
  13. package/dist/src/dependency-provenance.js +10 -0
  14. package/dist/src/dependency-provenance.js.map +1 -1
  15. package/dist/src/graph.js +25 -3
  16. package/dist/src/graph.js.map +1 -1
  17. package/dist/src/version.d.ts +1 -1
  18. package/dist/src/version.js +1 -1
  19. package/dist/src/workflow-blocker-route.d.ts +8 -0
  20. package/dist/src/workflow-blocker-route.js +106 -0
  21. package/dist/src/workflow-blocker-route.js.map +1 -0
  22. package/dist/src/workflow.d.ts +1 -0
  23. package/dist/src/workflow.js +139 -19
  24. package/dist/src/workflow.js.map +1 -1
  25. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  26. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  27. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  28. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  29. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
  30. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
  31. package/docs/release.md +49 -0
  32. package/package.json +2 -1
  33. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
  34. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +40 -11
  35. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +55 -0
  36. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  37. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  38. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  39. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +78 -12
  40. package/src/alpha6/adoption.ts +1105 -0
  41. package/src/alpha6/captured-check-evidence.ts +126 -0
  42. package/src/alpha6/check-support-anchor.ts +128 -0
  43. package/src/alpha6/component-owner.ts +168 -0
  44. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  45. package/src/alpha6/downstream-proof.ts +732 -0
  46. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  47. package/src/alpha6/handoff.ts +1443 -0
  48. package/src/alpha6/journal.ts +473 -0
  49. package/src/alpha6/literal-test-invocation.ts +84 -0
  50. package/src/alpha6/mechanical-feasibility.ts +488 -0
  51. package/src/alpha6/milestone.ts +2182 -0
  52. package/src/alpha6/npm-check-contract.ts +47 -0
  53. package/src/alpha6/plan-integrity-update.ts +20 -0
  54. package/src/alpha6/plan-integrity.ts +298 -0
  55. package/src/alpha6/plan-risk.ts +1480 -0
  56. package/src/alpha6/preexecution-replan.ts +187 -0
  57. package/src/alpha6/remediation-cause.ts +98 -0
  58. package/src/alpha6/remediation.ts +2965 -0
  59. package/src/alpha6/review.ts +1198 -0
  60. package/src/alpha6/root-cause-replan-carryover.ts +471 -0
  61. package/src/alpha6/store-sidecars.ts +335 -0
  62. package/src/alpha7/autonomy.ts +411 -0
  63. package/src/alpha7/corrective-recovery.ts +1332 -0
  64. package/src/artifacts.ts +130 -0
  65. package/src/beta1/project-transaction.ts +351 -0
  66. package/src/change-explanation.ts +153 -0
  67. package/src/cli-actions.ts +119 -0
  68. package/src/cli.ts +1583 -0
  69. package/src/contracts.ts +1510 -0
  70. package/src/credential-output.ts +89 -0
  71. package/src/credential-transport.ts +215 -0
  72. package/src/delegation.ts +190 -0
  73. package/src/dependency-provenance.ts +538 -0
  74. package/src/diagnostics.ts +93 -0
  75. package/src/domain/completed-step-carryover.ts +76 -0
  76. package/src/domain/discovery.ts +27 -0
  77. package/src/domain/plan-semantics.ts +58 -0
  78. package/src/domain/validation.ts +177 -0
  79. package/src/errors.ts +23 -0
  80. package/src/fs-utils.ts +61 -0
  81. package/src/gateway-handshake.ts +103 -0
  82. package/src/git.ts +158 -0
  83. package/src/graph.ts +342 -0
  84. package/src/historical-step-provenance.ts +136 -0
  85. package/src/index.ts +21 -0
  86. package/src/lifecycle/canonical-hash.ts +28 -0
  87. package/src/lifecycle/catalog.ts +202 -0
  88. package/src/lifecycle/compiler-inspection.ts +29 -0
  89. package/src/lifecycle/core-static-readiness.ts +132 -0
  90. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  91. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  92. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  93. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  94. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  95. package/src/lifecycle/corrective-replan.ts +843 -0
  96. package/src/lifecycle/evaluator.ts +48 -0
  97. package/src/lifecycle/fingerprint.ts +488 -0
  98. package/src/lifecycle/immutable.ts +8 -0
  99. package/src/lifecycle/implementation-table.ts +118 -0
  100. package/src/lifecycle/index.ts +8 -0
  101. package/src/lifecycle/schema-artifact.ts +263 -0
  102. package/src/lifecycle/semantic-registry.ts +572 -0
  103. package/src/lifecycle/types.ts +838 -0
  104. package/src/memory.ts +273 -0
  105. package/src/migration.ts +161 -0
  106. package/src/navigation-actions.ts +70 -0
  107. package/src/observation.ts +198 -0
  108. package/src/observed-routes.ts +661 -0
  109. package/src/operational-contract.ts +125 -0
  110. package/src/pending-review-update.ts +191 -0
  111. package/src/repository.ts +99 -0
  112. package/src/reviewer.ts +1876 -0
  113. package/src/state/corrective-replan-executor.ts +818 -0
  114. package/src/state/corrective-replan-public-schema.ts +83 -0
  115. package/src/state/corrective-replan-public.ts +908 -0
  116. package/src/state/corrective-replan-transaction.ts +947 -0
  117. package/src/state/corrective-yield-executor.ts +327 -0
  118. package/src/state/corrective-yield-transaction.ts +728 -0
  119. package/src/state/lock.ts +880 -0
  120. package/src/state/store.ts +567 -0
  121. package/src/transition-core.ts +330 -0
  122. package/src/ulid.ts +24 -0
  123. package/src/version.ts +2 -0
  124. package/src/workflow-blocker-route.ts +109 -0
  125. package/src/workflow.ts +11436 -0
@@ -0,0 +1,538 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import type {
4
+ DependencyProvenanceRecoveryPreflight,
5
+ DownstreamProofReplanCompatibility,
6
+ HistoricalStepDependencyCompatibility,
7
+ PlanIntegrityUpdateCompatibility,
8
+ ProjectKnowledgeMap,
9
+ RootCauseReplanDirtyCarryoverCompatibility,
10
+ TaskState,
11
+ } from './contracts.js';
12
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
13
+ import { changedFiles, currentBranch, headCommit, runGit } from './repository.js';
14
+ import { hashDirtyWorktree } from './alpha6/downstream-proof.js';
15
+ import { inspectWorkflowVersions } from './diagnostics.js';
16
+ import { pathAllowed } from './domain/validation.js';
17
+
18
+ interface RecoveryEnvironment {
19
+ activeLeaseCount: number;
20
+ staleLeaseCount: number;
21
+ pendingTransactionCount: number;
22
+ corruptTransactionCount: number;
23
+ coreOperationCount: number;
24
+ verifiedHistoricalStepCommits: string[];
25
+ versions: {
26
+ runtime: string;
27
+ declared: string | null;
28
+ locked: string | null;
29
+ installed: string | null;
30
+ currentBranch: string | null;
31
+ baseBranch: string | null;
32
+ };
33
+ }
34
+
35
+ export interface ActiveDownstreamProofCompatibility {
36
+ activeStepId: string;
37
+ planHash: string;
38
+ dirtyFiles: string[];
39
+ dirtyWorktreeHash: string;
40
+ invalidatedStepIds: string[];
41
+ }
42
+
43
+ const RECOVERY_FILES = ['package-lock.json', 'package.json'] as const;
44
+ const TERMINAL_TASK_STATUSES = new Set<TaskState['status']>(['merged', 'cancelled']);
45
+
46
+ export interface DependencyKnowledgeRefreshAssessment {
47
+ eligible: boolean;
48
+ changedKnowledgePaths: string[];
49
+ dependencyCommits: string[];
50
+ sourcePackageVersion: string | null;
51
+ targetPackageVersion: string;
52
+ blockers: string[];
53
+ }
54
+
55
+ /**
56
+ * Proves that a stale approved Knowledge Map differs only because one or more
57
+ * consecutive package.json edits changed the exact codex-workflow-v2 pin. The
58
+ * intervening repository history may contain ordinary product commits, but
59
+ * every commit that touched package.json after the approved content must be a
60
+ * package.json/package-lock.json-only dependency update.
61
+ */
62
+ export function inspectDependencyKnowledgeRefresh(
63
+ repositoryRoot: string,
64
+ milestoneBaseBranch: string,
65
+ approved: ProjectKnowledgeMap,
66
+ inspected: ProjectKnowledgeMap,
67
+ allowedDirtyFiles: readonly string[] = [],
68
+ ): DependencyKnowledgeRefreshAssessment {
69
+ const blockers: string[] = [];
70
+ const approvedByPath = new Map(approved.entries.map((entry) => [entry.path, entry]));
71
+ const inspectedByPath = new Map(inspected.entries.map((entry) => [entry.path, entry]));
72
+ const changedKnowledgePaths = [...new Set([
73
+ ...approved.entries.map((entry) => entry.path),
74
+ ...inspected.entries.map((entry) => entry.path),
75
+ ])].filter((candidatePath) => {
76
+ const before = approvedByPath.get(candidatePath);
77
+ const after = inspectedByPath.get(candidatePath);
78
+ return !before || !after
79
+ || before.id !== after.id
80
+ || before.category !== after.category
81
+ || before.scope !== after.scope
82
+ || before.authority !== after.authority
83
+ || before.discoveredBy !== after.discoveredBy
84
+ || before.contentHash !== after.contentHash;
85
+ }).sort();
86
+ if (approved.status !== 'active' || !approved.approval) {
87
+ blockers.push('Dependency Knowledge refresh requires a previously approved active map.');
88
+ }
89
+ if (inspected.status !== 'stale') {
90
+ blockers.push('Dependency Knowledge refresh requires an inspected stale map.');
91
+ }
92
+ if (JSON.stringify(changedKnowledgePaths) !== JSON.stringify(['package.json'])) {
93
+ blockers.push('Only package.json may differ in Project Knowledge.');
94
+ }
95
+
96
+ const versions = inspectWorkflowVersions(repositoryRoot, [milestoneBaseBranch]);
97
+ const versionSurfaces = [
98
+ versions.runtime,
99
+ versions.declared,
100
+ versions.locked,
101
+ versions.installed,
102
+ versions.currentBranch,
103
+ ...versions.milestoneBases.map((entry) => entry.version),
104
+ ];
105
+ if (versionSurfaces.some((version) => version !== PACKAGE_VERSION)) {
106
+ blockers.push(`Every Workflow version surface must equal ${PACKAGE_VERSION}.`);
107
+ }
108
+ const dirtyFiles = changedFiles(repositoryRoot).sort();
109
+ if (JSON.stringify(dirtyFiles) !== JSON.stringify([...allowedDirtyFiles].sort())) {
110
+ blockers.push(allowedDirtyFiles.length === 0
111
+ ? 'Dependency Knowledge refresh requires a clean checkout.'
112
+ : 'Dependency Knowledge refresh dirty files changed outside the bound compatibility allowlist.');
113
+ }
114
+
115
+ const approvedPackage = approvedByPath.get('package.json') ?? null;
116
+ const dependencyCommits: string[] = [];
117
+ let sourcePackageVersion: string | null = null;
118
+ if (!approvedPackage) {
119
+ blockers.push('The approved Knowledge Map does not contain package.json.');
120
+ } else {
121
+ try {
122
+ const historyOutput = runGit(repositoryRoot, ['rev-list', 'HEAD', '--', 'package.json']);
123
+ const packageHistory = historyOutput ? historyOutput.split('\n') : [];
124
+ const sourceIndex = packageHistory.findIndex((commit) =>
125
+ sha256(readGitFile(repositoryRoot, commit, 'package.json')) === approvedPackage.contentHash);
126
+ if (sourceIndex < 0) {
127
+ blockers.push('Approved package.json content is not present in current Git ancestry.');
128
+ } else {
129
+ sourcePackageVersion = manifestDependencyVersion(
130
+ parseJson(readGitFile(repositoryRoot, packageHistory[sourceIndex]!, 'package.json')),
131
+ );
132
+ for (const commit of packageHistory.slice(0, sourceIndex).reverse()) {
133
+ const parent = runGit(repositoryRoot, ['rev-parse', `${commit}^`]);
134
+ const filesOutput = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commit]);
135
+ const files = filesOutput ? filesOutput.split('\n').sort() : [];
136
+ if (JSON.stringify(files) !== JSON.stringify(RECOVERY_FILES)) {
137
+ blockers.push(`Knowledge-changing commit ${commit} is not dependency-only.`);
138
+ continue;
139
+ }
140
+ if (!workflowDependencyJsonDiffIsBounded(repositoryRoot, parent, commit, null)) {
141
+ blockers.push(`Knowledge-changing commit ${commit} changes fields beyond the Workflow dependency.`);
142
+ continue;
143
+ }
144
+ dependencyCommits.push(commit);
145
+ }
146
+ if (dependencyCommits.length === 0) {
147
+ blockers.push('No bounded Workflow dependency commit explains the Knowledge drift.');
148
+ }
149
+ }
150
+ } catch {
151
+ blockers.push('Dependency Knowledge refresh Git ancestry could not be verified.');
152
+ }
153
+ }
154
+
155
+ return {
156
+ eligible: blockers.length === 0,
157
+ changedKnowledgePaths,
158
+ dependencyCommits,
159
+ sourcePackageVersion,
160
+ targetPackageVersion: PACKAGE_VERSION,
161
+ blockers,
162
+ };
163
+ }
164
+
165
+ export function inspectDependencyProvenanceRecovery(
166
+ repositoryRoot: string,
167
+ task: TaskState,
168
+ environment: RecoveryEnvironment,
169
+ activeDownstreamProof: ActiveDownstreamProofCompatibility | null = null,
170
+ downstreamProofReplan: DownstreamProofReplanCompatibility | null = null,
171
+ historicalStepProvenance: HistoricalStepDependencyCompatibility | null = null,
172
+ planIntegrity: PlanIntegrityUpdateCompatibility | null = null,
173
+ rootCauseReplan: RootCauseReplanDirtyCarryoverCompatibility | null = null,
174
+ ): DependencyProvenanceRecoveryPreflight {
175
+ const blockers: string[] = [];
176
+ const dirty = changedFiles(repositoryRoot);
177
+ const head = headCommit(repositoryRoot);
178
+ let parent: string | null = null;
179
+ let commitFiles: string[] = [];
180
+ let dependencyCommits: Array<{
181
+ commitSha: string;
182
+ parentCommitSha: string;
183
+ packageVersion: string;
184
+ }> = [];
185
+ let retainedHistoricalCommits: string[] = [];
186
+
187
+ const compatibilityCount = [activeDownstreamProof, downstreamProofReplan, historicalStepProvenance,
188
+ planIntegrity, rootCauseReplan].filter(Boolean).length;
189
+ if (compatibilityCount > 1) blockers.push('Dependency recovery accepts exactly one dirty compatibility binding.');
190
+
191
+ const dirtyCompatibility = activeDownstreamProof ?? downstreamProofReplan ?? historicalStepProvenance
192
+ ?? planIntegrity ?? rootCauseReplan;
193
+ if (dirtyCompatibility) {
194
+ if (JSON.stringify(dirty.sort()) !== JSON.stringify([...dirtyCompatibility.dirtyFiles].sort())) {
195
+ blockers.push('Dirty files changed after the compatibility assessment.');
196
+ }
197
+ try {
198
+ if (hashDirtyWorktree(repositoryRoot, dirty) !== dirtyCompatibility.dirtyWorktreeHash) {
199
+ blockers.push('Dirty file content changed after the compatibility assessment.');
200
+ }
201
+ } catch (error) {
202
+ blockers.push(error instanceof Error ? error.message : String(error));
203
+ }
204
+ if (activeDownstreamProof) {
205
+ if (!task.planHash || task.planHash !== activeDownstreamProof.planHash) {
206
+ blockers.push('The current Task Plan must match the active downstream-proof compatibility binding.');
207
+ }
208
+ const inProgressSteps = task.steps.filter((step) => step.status === 'in_progress');
209
+ if (inProgressSteps.length !== 1 || inProgressSteps[0]?.id !== activeDownstreamProof.activeStepId) {
210
+ blockers.push('Exactly the bound downstream-proof Step must remain in progress.');
211
+ }
212
+ } else if (downstreamProofReplan) {
213
+ if (task.status !== 'awaiting_execution_authorization'
214
+ || task.planHash !== downstreamProofReplan.reboundPlanHash
215
+ || task.steps.some((step) => step.status === 'in_progress')) {
216
+ blockers.push('The Task must retain the exact stranded post-rebind/no-running-Step posture.');
217
+ }
218
+ } else if (historicalStepProvenance) {
219
+ if (task.status !== 'in_progress' || task.planHash !== historicalStepProvenance.planHash) {
220
+ blockers.push('The current in-progress Task Plan must match the historical Step compatibility binding.');
221
+ }
222
+ const inProgressSteps = task.steps.filter((step) => step.status === 'in_progress');
223
+ if (inProgressSteps.length !== 1 || inProgressSteps[0]?.id !== historicalStepProvenance.activeStepId) {
224
+ blockers.push('Exactly the bound corrective Step must remain in progress.');
225
+ }
226
+ } else if (planIntegrity) {
227
+ const failedStep = task.steps.find((step) => step.id === planIntegrity.stepId) ?? null;
228
+ if (planIntegrity.mode === 'post-rebind-scope-adoption') {
229
+ if (task.status !== 'awaiting_execution_authorization'
230
+ || task.planHash !== planIntegrity.planHash
231
+ || failedStep?.status !== 'planned'
232
+ || task.steps.some((step) => step.status === 'in_progress')
233
+ || !planIntegrity.scopeExtensions?.length
234
+ || planIntegrity.scopeExtensions.some((scopePath) =>
235
+ !failedStep.allowedWrites.some((allowedPath) => allowedPath === scopePath))) {
236
+ blockers.push('The Task must retain the exact post-rebind/no-running-Step check-support adoption posture.');
237
+ }
238
+ } else if (planIntegrity.mode === 'post-rebind-dirty-carryover') {
239
+ if (!['ready', 'in_progress'].includes(task.status)
240
+ || task.planHash !== planIntegrity.planHash
241
+ || failedStep?.status !== 'planned'
242
+ || task.steps.some((step) => step.status === 'in_progress')
243
+ || !planIntegrity.scopeExtensions?.length
244
+ || planIntegrity.scopeExtensions.some((scopePath) =>
245
+ !failedStep.allowedWrites.some((allowedPath) => allowedPath === scopePath)
246
+ || !failedStep.expectedOutputs.some((declared) =>
247
+ (declared.startsWith('path:') ? declared.slice(5) : declared) === scopePath))) {
248
+ blockers.push('The Task must retain the exact authorized/no-running-Step check-support dirty-carryover posture.');
249
+ }
250
+ } else if (planIntegrity.mode === 'navigation-fix-dirty-carryover') {
251
+ if (task.status !== 'ready'
252
+ || task.planHash !== planIntegrity.planHash
253
+ || failedStep?.status !== 'planned'
254
+ || failedStep.evidence !== null
255
+ || task.steps.some((step) => step.status === 'in_progress')
256
+ || planIntegrity.dirtyFiles.some((file) => !pathAllowed(file, failedStep.allowedWrites))) {
257
+ blockers.push('The Task must retain the exact ready/planned navigation-fix dirty-carryover posture.');
258
+ }
259
+ } else if (task.status !== 'needs_fix'
260
+ || task.planHash !== planIntegrity.planHash
261
+ || failedStep?.status !== 'failed'
262
+ || task.steps.some((step) => step.status === 'in_progress')) {
263
+ blockers.push('The Task must retain the exact failed/no-running-Step Plan-integrity posture.');
264
+ }
265
+ } else if (rootCauseReplan) {
266
+ const correctiveStep = task.steps.find((step) => step.id === rootCauseReplan.stepId) ?? null;
267
+ if (task.status !== 'ready'
268
+ || correctiveStep?.status !== 'planned'
269
+ || correctiveStep.evidence !== null
270
+ || task.steps.some((step) => step.status === 'in_progress')
271
+ || task.id !== rootCauseReplan.taskId
272
+ || task.revision !== rootCauseReplan.sourceTaskRevision
273
+ || !task.planHash) {
274
+ blockers.push('The Task must retain the exact authorized corrective-replan/no-running-Step posture.');
275
+ }
276
+ if (rootCauseReplan.dirtyFiles.some((file) => !correctiveStep
277
+ || !pathAllowed(file, correctiveStep.allowedWrites))) {
278
+ blockers.push('Corrective carryover cannot expand the replacement Step allowedWrites.');
279
+ }
280
+ }
281
+ } else {
282
+ if (dirty.length > 0) blockers.push('Repository checkout must be clean.');
283
+ }
284
+ if (task.workspaceOwner !== 'local') blockers.push('Recovery is limited to a local Task workspace.');
285
+ if (!task.taskBranch || currentBranch(repositoryRoot) !== task.taskBranch) {
286
+ blockers.push('The current branch must be the recorded Task branch.');
287
+ }
288
+ if (!task.baseCommit) blockers.push('The Task must have a recorded base commit.');
289
+ if (TERMINAL_TASK_STATUSES.has(task.status)) blockers.push('Terminal Tasks cannot recover dependency provenance.');
290
+ if (!dirtyCompatibility && task.steps.some((step) => step.status === 'in_progress')) {
291
+ blockers.push('No Step may be in progress.');
292
+ }
293
+ if (environment.activeLeaseCount > 0) blockers.push('No active writer lease may exist.');
294
+ if (environment.staleLeaseCount > 0) blockers.push('No stale writer lease may exist.');
295
+ if (environment.pendingTransactionCount > 0) blockers.push('No pending Workflow transaction may exist.');
296
+ if (environment.corruptTransactionCount > 0) blockers.push('No corrupt Workflow transaction may exist.');
297
+ if (environment.coreOperationCount > 0) blockers.push('No Core Task operation may exist.');
298
+
299
+ for (const [surface, version] of Object.entries(environment.versions)) {
300
+ if (version !== PACKAGE_VERSION) blockers.push(`${surface} Workflow version must equal ${PACKAGE_VERSION}.`);
301
+ }
302
+
303
+ try {
304
+ if (task.systemCommits.includes(head)) blockers.push('The candidate commit is already registered.');
305
+ if (task.baseCommit) {
306
+ const outputHistory = runGit(repositoryRoot, ['rev-list', '--reverse', `${task.baseCommit}..HEAD`]);
307
+ const history = outputHistory ? outputHistory.split('\n') : [];
308
+ const expectedRegistered = [
309
+ ...task.steps.flatMap((step) => (step.evidence ? [step.evidence.commitSha] : [])),
310
+ ...task.systemCommits,
311
+ ...(task.invalidatedStepCommits ?? []),
312
+ ...(task.dependencyProvenanceRecoveries ?? []).flatMap((recovery) => recovery.retainedHistoricalCommits ?? []),
313
+ ...environment.verifiedHistoricalStepCommits.filter((commit) => history.includes(commit)),
314
+ ];
315
+ const expectedSet = new Set(expectedRegistered);
316
+ const candidateStart = history.findIndex((commit) => !expectedSet.has(commit));
317
+ const registeredPrefix = candidateStart < 0 ? history : history.slice(0, candidateStart);
318
+ const candidateChain = candidateStart < 0 ? [] : history.slice(candidateStart);
319
+ retainedHistoricalCommits = environment.verifiedHistoricalStepCommits.filter((commit) =>
320
+ registeredPrefix.includes(commit)
321
+ && !task.systemCommits.includes(commit)
322
+ && !task.steps.some((step) => step.evidence?.commitSha === commit)
323
+ );
324
+ if (historicalStepProvenance
325
+ && !retainedHistoricalCommits.includes(historicalStepProvenance.retainedCommitSha)) {
326
+ blockers.push('The dependency candidate no longer retains the bound reviewed historical Step commit.');
327
+ }
328
+ if (history.at(-1) !== head || !sameCommitSet(registeredPrefix, [...new Set(expectedRegistered)])) {
329
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
330
+ }
331
+ parent = registeredPrefix.at(-1) ?? task.baseCommit;
332
+ let expectedParent = parent;
333
+ for (const commit of candidateChain) {
334
+ const inspected = inspectBoundedWorkflowDependencyCommit(repositoryRoot, commit);
335
+ if (!inspected) {
336
+ blockers.push(`Unregistered commit ${commit} is not a consecutive bounded Workflow dependency update.`);
337
+ blockers.push('The candidate commit must change exactly package.json and package-lock.json and only the Workflow dependency pin.');
338
+ blockers.push(`The candidate commit contains changes beyond the exact ${PACKAGE_NAME}@${PACKAGE_VERSION} dependency update.`);
339
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
340
+ continue;
341
+ }
342
+ if (inspected.parentCommitSha !== expectedParent) {
343
+ blockers.push(`Unregistered commit ${commit} does not continue the bounded Workflow dependency parent chain.`);
344
+ blockers.push('Only a trailing dependency-only commit chain may follow the exact recorded Task history.');
345
+ continue;
346
+ }
347
+ dependencyCommits.push(inspected);
348
+ expectedParent = commit;
349
+ }
350
+ if (dependencyCommits.length !== candidateChain.length || dependencyCommits.at(-1)?.commitSha !== head) {
351
+ blockers.push('The complete unregistered suffix must be a bounded Workflow dependency-only chain ending at HEAD.');
352
+ }
353
+ if (dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION) {
354
+ blockers.push(`The dependency-only chain must end at ${PACKAGE_NAME}@${PACKAGE_VERSION}.`);
355
+ }
356
+ }
357
+ commitFiles = dependencyCommits.length > 0 ? [...RECOVERY_FILES] : [];
358
+ } catch {
359
+ blockers.push('The candidate dependency commit could not be inspected.');
360
+ }
361
+
362
+ return {
363
+ eligible: blockers.length === 0,
364
+ action: blockers.length === 0
365
+ ? activeDownstreamProof
366
+ ? 'update downstream-proof-dependency-recover'
367
+ : downstreamProofReplan
368
+ ? 'update downstream-proof-replan-dependency-recover'
369
+ : historicalStepProvenance
370
+ ? 'update historical-step-dependency-recover'
371
+ : planIntegrity
372
+ ? 'update plan-integrity-dependency-recover'
373
+ : 'update dependency-provenance-recover'
374
+ : null,
375
+ projectId: task.projectId,
376
+ taskId: task.id,
377
+ taskRevision: task.revision,
378
+ commitSha: blockers.length === 0 ? head : null,
379
+ parentCommitSha: blockers.length === 0 ? parent : null,
380
+ packageVersion: PACKAGE_VERSION,
381
+ changedFiles: commitFiles,
382
+ dependencyCommits,
383
+ retainedHistoricalCommits,
384
+ activeDownstreamProof,
385
+ downstreamProofReplan,
386
+ historicalStepProvenance,
387
+ planIntegrity,
388
+ rootCauseReplan,
389
+ blockers,
390
+ };
391
+ }
392
+
393
+ function sameCommitSet(actual: string[], expected: string[]): boolean {
394
+ return actual.length === expected.length
395
+ && actual.every((commit) => expected.includes(commit))
396
+ && expected.every((commit) => actual.includes(commit));
397
+ }
398
+
399
+ export function inspectBoundedWorkflowDependencyCommit(
400
+ repositoryRoot: string,
401
+ commitSha: string,
402
+ ): { commitSha: string; parentCommitSha: string; packageVersion: string } | null {
403
+ try {
404
+ const parentCommitSha = runGit(repositoryRoot, ['rev-parse', `${commitSha}^`]);
405
+ const output = runGit(repositoryRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', commitSha]);
406
+ const files = output ? output.split('\n').sort() : [];
407
+ if (JSON.stringify(files) !== JSON.stringify(RECOVERY_FILES)) return null;
408
+ const packageVersion = manifestDependencyVersion(parseJson(runGit(repositoryRoot, ['show', `${commitSha}:package.json`])));
409
+ if (!packageVersion || !workflowDependencyJsonDiffIsBounded(
410
+ repositoryRoot,
411
+ parentCommitSha,
412
+ commitSha,
413
+ packageVersion,
414
+ )) return null;
415
+ return { commitSha, parentCommitSha, packageVersion };
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+
421
+ export function workflowDependencyVersionAt(repositoryRoot: string, commitSha: string): string | null {
422
+ try {
423
+ return manifestDependencyVersion(parseJson(runGit(repositoryRoot, ['show', `${commitSha}:package.json`])));
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+
429
+ function workflowDependencyJsonDiffIsBounded(
430
+ repositoryRoot: string,
431
+ parent: string,
432
+ revision: string,
433
+ expectedAfterVersion: string | null,
434
+ ): boolean {
435
+ const beforeManifestContent = runGit(repositoryRoot, ['show', `${parent}:package.json`]);
436
+ const afterManifestContent = runGit(repositoryRoot, ['show', `${revision}:package.json`]);
437
+ const beforeLockContent = runGit(repositoryRoot, ['show', `${parent}:package-lock.json`]);
438
+ const afterLockContent = runGit(repositoryRoot, ['show', `${revision}:package-lock.json`]);
439
+ const beforeManifest = parseJson(beforeManifestContent);
440
+ const afterManifest = parseJson(afterManifestContent);
441
+ const beforeLock = parseJson(beforeLockContent);
442
+ const afterLock = parseJson(afterLockContent);
443
+ if (!beforeManifest || !afterManifest || !beforeLock || !afterLock) return false;
444
+ const beforeManifestDependencies = manifestDependencyValues(beforeManifest);
445
+ const beforeVersion = beforeManifestDependencies.length === 1 ? beforeManifestDependencies[0] : null;
446
+ const afterVersion = manifestDependencyVersion(afterManifest);
447
+ if (!beforeVersion || !afterVersion || beforeVersion === afterVersion) return false;
448
+ if (!targetsExactDevDependency(beforeManifest, beforeVersion)) return false;
449
+ if (!targetsExactDevDependency(afterManifest, afterVersion)) return false;
450
+ if (expectedAfterVersion !== null && afterVersion !== expectedAfterVersion) return false;
451
+ if (!lockTargetsExactVersion(beforeLock, beforeVersion)) return false;
452
+ if (!lockTargetsExactVersion(afterLock, afterVersion)) return false;
453
+ return JSON.stringify(stripManifestDependency(beforeManifest)) === JSON.stringify(stripManifestDependency(afterManifest))
454
+ && JSON.stringify(stripLockDependency(beforeLock)) === JSON.stringify(stripLockDependency(afterLock));
455
+ }
456
+
457
+ function parseJson(content: string): Record<string, unknown> | null {
458
+ try {
459
+ const value = JSON.parse(content) as unknown;
460
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+
466
+ function manifestDependencyValues(manifest: Record<string, unknown>): string[] {
467
+ const values: string[] = [];
468
+ for (const key of ['devDependencies', 'dependencies']) {
469
+ const group = record(manifest[key]);
470
+ if (typeof group?.[PACKAGE_NAME] === 'string') values.push(group[PACKAGE_NAME] as string);
471
+ }
472
+ return values;
473
+ }
474
+
475
+ function manifestDependencyVersion(manifest: Record<string, unknown> | null): string | null {
476
+ if (!manifest) return null;
477
+ const values = manifestDependencyValues(manifest);
478
+ return values.length === 1 ? values[0]! : null;
479
+ }
480
+
481
+ function lockTargetsExactVersion(lock: Record<string, unknown>, version: string): boolean {
482
+ const packages = record(lock.packages);
483
+ const entry = record(packages?.[`node_modules/${PACKAGE_NAME}`]);
484
+ const root = record(packages?.['']);
485
+ if (entry?.version !== version || !targetsExactDevDependency(root ?? {}, version)) return false;
486
+ const dependencies = record(lock.dependencies);
487
+ const legacyEntry = record(dependencies?.[PACKAGE_NAME]);
488
+ return !legacyEntry || legacyEntry.version === version;
489
+ }
490
+
491
+ function targetsExactDevDependency(manifest: Record<string, unknown>, version: string): boolean {
492
+ const devDependencies = record(manifest.devDependencies);
493
+ const dependencies = record(manifest.dependencies);
494
+ return devDependencies?.[PACKAGE_NAME] === version && dependencies?.[PACKAGE_NAME] === undefined;
495
+ }
496
+
497
+ function stripManifestDependency(source: Record<string, unknown>): Record<string, unknown> {
498
+ const clone = structuredClone(source);
499
+ for (const key of ['devDependencies', 'dependencies']) {
500
+ const group = record(clone[key]);
501
+ if (group) delete group[PACKAGE_NAME];
502
+ }
503
+ return clone;
504
+ }
505
+
506
+ function stripLockDependency(source: Record<string, unknown>): Record<string, unknown> {
507
+ const clone = structuredClone(source);
508
+ const packages = record(clone.packages);
509
+ if (packages) {
510
+ delete packages[`node_modules/${PACKAGE_NAME}`];
511
+ const root = record(packages['']);
512
+ if (root) {
513
+ for (const key of ['devDependencies', 'dependencies']) {
514
+ const group = record(root[key]);
515
+ if (group) delete group[PACKAGE_NAME];
516
+ }
517
+ }
518
+ }
519
+ const dependencies = record(clone.dependencies);
520
+ if (dependencies) delete dependencies[PACKAGE_NAME];
521
+ return clone;
522
+ }
523
+
524
+ function record(value: unknown): Record<string, unknown> | null {
525
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
526
+ }
527
+
528
+ function sha256(value: string): string {
529
+ return createHash('sha256').update(value).digest('hex');
530
+ }
531
+
532
+ function readGitFile(repositoryRoot: string, revision: string, filePath: string): string {
533
+ const result = spawnSync('git', ['-C', repositoryRoot, 'show', `${revision}:${filePath}`], {
534
+ encoding: 'utf8',
535
+ });
536
+ if (result.status !== 0) throw new Error(result.stderr.trim() || `Cannot read ${revision}:${filePath}.`);
537
+ return result.stdout;
538
+ }
@@ -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
+ }