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,47 @@
1
+ import path from 'node:path';
2
+
3
+ const NPM_RUN_CHECK = /^npm\s+run(?:-script)?\s+([A-Za-z0-9._:-]+)(?:\s+--\s+(.+))?$/;
4
+ const SAFE_FORWARD_ARG = /^--[A-Za-z0-9._:-]+=[^\s]+$/;
5
+ const NODE_AFFECTED_SCRIPT = /^node\s+([A-Za-z0-9._/-]+\.(?:cjs|js|mjs))\s+affected$/;
6
+ const SELECTOR = /^(?:--base|--head|--files)=.+$/;
7
+
8
+ export interface ParsedNpmRunCheck {
9
+ script: string;
10
+ forwardedArguments: string[];
11
+ }
12
+
13
+ export interface RequiredSelectorContract {
14
+ runnerPath: string;
15
+ selectors: readonly ['--base', '--head', '--files'];
16
+ }
17
+
18
+ export function parseNpmRunCheck(command: string): ParsedNpmRunCheck | null {
19
+ const match = NPM_RUN_CHECK.exec(command.trim());
20
+ if (!match) return null;
21
+ const forwardedArguments = match[2]?.trim().split(/\s+/).filter(Boolean) ?? [];
22
+ if (forwardedArguments.some((argument) => !SAFE_FORWARD_ARG.test(argument))) return null;
23
+ return { script: match[1]!, forwardedArguments };
24
+ }
25
+
26
+ export function inspectRequiredSelectorContract(
27
+ scriptCommand: string,
28
+ readRunner: (runnerPath: string) => string | null,
29
+ ): RequiredSelectorContract | null {
30
+ const match = NODE_AFFECTED_SCRIPT.exec(scriptCommand.trim());
31
+ if (!match) return null;
32
+ const runnerPath = match[1]!;
33
+ if (path.posix.isAbsolute(runnerPath)
34
+ || runnerPath.split('/').some((segment) => segment === '.' || segment === '..')) return null;
35
+ const source = readRunner(runnerPath);
36
+ if (!source
37
+ || !source.includes('process.argv.slice(3)')
38
+ || !source.includes('--base')
39
+ || !source.includes('--head')
40
+ || !source.includes('--files')
41
+ || !source.includes('Affected mode requires')) return null;
42
+ return { runnerPath, selectors: ['--base', '--head', '--files'] };
43
+ }
44
+
45
+ export function hasRequiredSelector(arguments_: readonly string[]): boolean {
46
+ return arguments_.some((argument) => SELECTOR.test(argument));
47
+ }
@@ -0,0 +1,20 @@
1
+ import type { PlanIntegrityRecoveryAssessment } from './plan-integrity.js';
2
+ import { validLiteralTestInvocationEvidence } from './component-owner.js';
3
+
4
+ /** Source eligibility only: callers retain target, version surfaces, lease and transaction guards. */
5
+ export function eligiblePlanIntegrityUpdateSource(
6
+ source: { train: number; patch: number | null } | null,
7
+ assessment: PlanIntegrityRecoveryAssessment | null,
8
+ hasRunningStep: boolean,
9
+ ): boolean {
10
+ if (!source || source.patch === null) return false;
11
+ // Preserve the original beta12 protocol, including its existing compatibility modes.
12
+ if (source.train === 12 && source.patch >= 11) return true;
13
+ // No general beta13 transport: assessment is freshly rederived by Core from the
14
+ // failed current execution cycle, tracked runner/spec and actual diagnostics.
15
+ return source.train === 13 && source.patch >= 11 && !hasRunningStep
16
+ && assessment?.applicable === true && assessment.eligible === true
17
+ && assessment.blockers.length === 0
18
+ && assessment.evidence?.kind === 'failed-check-component-owner-outside-allowed-writes'
19
+ && assessment.evidence.conflicts.some(validLiteralTestInvocationEvidence);
20
+ }
@@ -0,0 +1,298 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import type {
6
+ PlanIntegrityConflictEvidence,
7
+ PlanIntegrityConflictEvidenceV2,
8
+ PlanIntegrityConflictEvidenceV3,
9
+ PlanIntegrityConflictEvidenceV4,
10
+ TaskState,
11
+ } from '../contracts.js';
12
+ import { pathAllowed } from '../domain/validation.js';
13
+ import { WorkflowError } from '../errors.js';
14
+ import { hashCanonical } from '../lifecycle/fingerprint.js';
15
+ import { changedFiles, headCommit } from '../repository.js';
16
+ import type { FileStateStore } from '../state/store.js';
17
+ import {
18
+ hasRequiredSelector,
19
+ inspectRequiredSelectorContract,
20
+ parseNpmRunCheck,
21
+ } from './npm-check-contract.js';
22
+ import { inspectCheckSupportAnchors } from './check-support-anchor.js';
23
+ import { inspectFailedCheckComponentOwners, semanticBoundaryHash } from './component-owner.js';
24
+ import {
25
+ readCorrectiveDecisionEvents,
26
+ readRemediationEvents,
27
+ selectPlanIntegrityRemediations,
28
+ } from './remediation.js';
29
+
30
+ const ROOT_MANIFEST = 'package.json' as const;
31
+ const TASK_ID = /^TASK-[0-9A-HJKMNP-TV-Z]{26}$/;
32
+
33
+ export interface PlanIntegrityRecoveryAssessment {
34
+ readonly applicable: boolean;
35
+ readonly eligible: boolean;
36
+ readonly taskId: string;
37
+ readonly taskRevision: number;
38
+ readonly stepId: string;
39
+ readonly planHash: string | null;
40
+ readonly blockers: readonly string[];
41
+ readonly evidence: PlanIntegrityConflictEvidence | null;
42
+ }
43
+
44
+ export function defaultPlanIntegrityRecoveryActor(taskId: string): string {
45
+ if (!TASK_ID.test(taskId)) {
46
+ throw new WorkflowError('INVALID_ARGUMENT', `Cannot derive a Plan-integrity recovery actor from invalid Task ID ${taskId}.`);
47
+ }
48
+ return `agent:plan-integrity-recovery:${taskId.toLowerCase()}`;
49
+ }
50
+
51
+ export function assessPlanIntegrityRecovery(
52
+ store: FileStateStore,
53
+ repositoryRoot: string,
54
+ task: TaskState,
55
+ stepId: string,
56
+ ): PlanIntegrityRecoveryAssessment {
57
+ const base = {
58
+ taskId: task.id,
59
+ taskRevision: task.revision,
60
+ stepId,
61
+ planHash: task.planHash,
62
+ };
63
+ const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
64
+ if (!step || task.status !== 'needs_fix' || step.status !== 'failed' || !task.planHash) {
65
+ return { ...base, applicable: false, eligible: false, blockers: [], evidence: null };
66
+ }
67
+
68
+ const allRemediations = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
69
+ const decisions = readCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId);
70
+ const remediations = selectPlanIntegrityRemediations(store, task, allRemediations, decisions);
71
+ if (!remediations || remediations.some(event => event.failureKind !== 'checks-failed')) {
72
+ return { ...base, applicable: false, eligible: false, blockers: [], evidence: null };
73
+ }
74
+
75
+ const manifest = inspectRootManifest(repositoryRoot);
76
+ const runnerBlockers: string[] = [];
77
+ const conflicts: PlanIntegrityConflictEvidenceV2['conflicts'] = [];
78
+ for (const command of step.checks) {
79
+ const invocation = parseNpmRunCheck(command);
80
+ if (!invocation) continue;
81
+ const scriptCommand = manifest.scripts.get(invocation.script) ?? null;
82
+ if (scriptCommand === null) {
83
+ if (pathAllowed(ROOT_MANIFEST, step.allowedWrites)) continue;
84
+ conflicts.push({
85
+ reasonCode: 'MISSING_NPM_SCRIPT' as const,
86
+ script: invocation.script,
87
+ command: command.trim(),
88
+ scriptCommand: null,
89
+ runnerPath: null,
90
+ runnerHash: null,
91
+ });
92
+ continue;
93
+ }
94
+ const runnerInspections = new Map<string, ReturnType<typeof inspectRepositoryFile>>();
95
+ const selectorContract = inspectRequiredSelectorContract(scriptCommand, (runnerPath) => {
96
+ const inspection = inspectRepositoryFile(repositoryRoot, runnerPath);
97
+ runnerInspections.set(runnerPath, inspection);
98
+ return inspection.content;
99
+ });
100
+ if (!selectorContract || hasRequiredSelector(invocation.forwardedArguments)) continue;
101
+ const runnerInspection = runnerInspections.get(selectorContract.runnerPath) ?? null;
102
+ if (runnerInspection?.blocker) runnerBlockers.push(runnerInspection.blocker);
103
+ if (pathAllowed(ROOT_MANIFEST, step.allowedWrites)
104
+ || pathAllowed(selectorContract.runnerPath, step.allowedWrites)) continue;
105
+ conflicts.push({
106
+ reasonCode: 'REQUIRED_SELECTOR_MISSING' as const,
107
+ script: invocation.script,
108
+ command: command.trim(),
109
+ scriptCommand,
110
+ runnerPath: selectorContract.runnerPath,
111
+ runnerHash: runnerInspection?.hash ?? null,
112
+ });
113
+ }
114
+ conflicts.sort((left, right) => left.command.localeCompare(right.command) || left.reasonCode.localeCompare(right.reasonCode));
115
+ const supportConflicts: PlanIntegrityConflictEvidenceV3['conflicts'] = conflicts.length === 0
116
+ ? inspectCheckSupportAnchors({
117
+ step,
118
+ scripts: manifest.scripts,
119
+ readFile: (relativePath) => {
120
+ const inspected = inspectRepositoryFile(repositoryRoot, relativePath);
121
+ return { content: inspected.content, hash: inspected.hash };
122
+ },
123
+ pathExists: (relativePath) => gitPathExists(repositoryRoot, headCommit(repositoryRoot), relativePath),
124
+ }).map((conflict) => ({
125
+ ...conflict,
126
+ expectedOutputPaths: [...conflict.expectedOutputPaths],
127
+ anchoredMigrationNames: [...conflict.anchoredMigrationNames],
128
+ }))
129
+ : [];
130
+ const componentOwnerConflicts: PlanIntegrityConflictEvidenceV4['conflicts'] =
131
+ conflicts.length === 0 && supportConflicts.length === 0
132
+ ? inspectFailedCheckComponentOwners({
133
+ step,
134
+ scripts: manifest.scripts,
135
+ isTracked: (relativePath) => gitPathExists(repositoryRoot, headCommit(repositoryRoot), relativePath),
136
+ readFile: (relativePath) => {
137
+ const inspected = inspectRepositoryFile(repositoryRoot, relativePath);
138
+ return { content: inspected.content, hash: inspected.hash };
139
+ },
140
+ }).map(({ causeFingerprint: _causeFingerprint, ...conflict }) => conflict)
141
+ : [];
142
+ if (conflicts.length === 0 && supportConflicts.length === 0 && componentOwnerConflicts.length === 0) {
143
+ return { ...base, applicable: false, eligible: false, blockers: [], evidence: null };
144
+ }
145
+
146
+ if (remediations.length !== allRemediations.length && conflicts.length > 0)
147
+ return { ...base, applicable: false, eligible: false, blockers: [], evidence: null };
148
+
149
+ const files = [...changedFiles(repositoryRoot)].sort();
150
+ const outsideAllowedWrites = files.filter((file) => !pathAllowed(file, step.allowedWrites));
151
+ const forbiddenChanges = files.filter((file) => pathAllowed(file, step.forbiddenScope));
152
+ const blockers = [
153
+ ...(manifest.blocker ? [manifest.blocker] : []),
154
+ ...runnerBlockers,
155
+ ...(outsideAllowedWrites.length > 0
156
+ ? [`Changed files outside ${stepId} allowedWrites: ${outsideAllowedWrites.join(', ')}`]
157
+ : []),
158
+ ...(forbiddenChanges.length > 0
159
+ ? [`Changed files intersect ${stepId} forbiddenScope: ${forbiddenChanges.join(', ')}`]
160
+ : []),
161
+ ...supportConflicts.flatMap((conflict) => pathAllowed(conflict.supportPath, step.forbiddenScope)
162
+ ? [`Check-support authority ${conflict.supportPath} intersects ${stepId} forbiddenScope.`]
163
+ : []),
164
+ ...componentOwnerConflicts.flatMap((conflict) => pathAllowed(conflict.ownerPath, step.forbiddenScope)
165
+ ? [`Component owner ${conflict.ownerPath} intersects ${stepId} forbiddenScope.`]
166
+ : []),
167
+ ];
168
+ const stepDefinitionHash = hashCanonical({
169
+ id: step.id,
170
+ title: step.title,
171
+ objective: step.objective,
172
+ requirements: step.requirements,
173
+ dependencies: step.dependencies,
174
+ expectedOutputs: step.expectedOutputs,
175
+ allowedWrites: step.allowedWrites,
176
+ forbiddenScope: step.forbiddenScope,
177
+ checks: step.checks,
178
+ });
179
+ const remediationBindings = remediations.map((event) => ({
180
+ eventId: event.eventId,
181
+ eventHash: event.eventHash,
182
+ attemptOrdinal: event.attemptOrdinal,
183
+ }));
184
+ const evidenceBase = supportConflicts.length > 0 ? {
185
+ kind: 'check-support-anchor-outside-allowed-writes' as const,
186
+ taskRevision: task.revision,
187
+ remediationEvents: remediationBindings,
188
+ manifestPath: ROOT_MANIFEST,
189
+ manifestHash: manifest.hash,
190
+ gitHead: headCommit(repositoryRoot),
191
+ stepDefinitionHash,
192
+ conflicts: supportConflicts,
193
+ scopeExtensions: [...new Set(supportConflicts.map((conflict) => conflict.supportPath))].sort(),
194
+ changedFiles: files,
195
+ } : componentOwnerConflicts.length > 0 ? {
196
+ kind: 'failed-check-component-owner-outside-allowed-writes' as const,
197
+ taskRevision: task.revision,
198
+ remediationEvents: remediationBindings,
199
+ gitHead: headCommit(repositoryRoot),
200
+ stepDefinitionHash,
201
+ semanticBoundaryHash: semanticBoundaryHash(task),
202
+ failedCheckCauseFingerprint: step.failurePause!.failedChecks!.find((failure) =>
203
+ componentOwnerConflicts.some((conflict) => conflict.command === failure.command))!.causeFingerprint,
204
+ conflicts: componentOwnerConflicts,
205
+ scopeExtensions: [...new Set(componentOwnerConflicts.map((conflict) => conflict.ownerPath))].sort(),
206
+ changedFiles: files,
207
+ } : {
208
+ kind: 'non-executable-npm-check-outside-allowed-writes' as const,
209
+ taskRevision: task.revision,
210
+ remediationEvents: remediationBindings,
211
+ manifestPath: ROOT_MANIFEST,
212
+ manifestHash: manifest.hash,
213
+ gitHead: headCommit(repositoryRoot),
214
+ stepDefinitionHash,
215
+ conflicts,
216
+ changedFiles: files,
217
+ };
218
+ const evidence: PlanIntegrityConflictEvidence = {
219
+ ...evidenceBase,
220
+ conflictHash: hashCanonical({
221
+ domain: supportConflicts.length > 0
222
+ ? 'codex-workflow/plan-integrity-conflict/v3'
223
+ : componentOwnerConflicts.length > 0
224
+ ? 'codex-workflow/plan-integrity-conflict/v4'
225
+ : 'codex-workflow/plan-integrity-conflict/v2',
226
+ taskId: task.id,
227
+ planHash: task.planHash,
228
+ stepId,
229
+ evidence: evidenceBase,
230
+ }),
231
+ };
232
+ return {
233
+ ...base,
234
+ applicable: true,
235
+ eligible: blockers.length === 0,
236
+ blockers,
237
+ evidence,
238
+ };
239
+ }
240
+
241
+ function gitPathExists(repositoryRoot: string, commit: string, relativePath: string): boolean {
242
+ const result = spawnSync('git', ['-C', repositoryRoot, 'cat-file', '-e', `${commit}:${relativePath}`], {
243
+ encoding: 'utf8',
244
+ });
245
+ if (result.status === 0) return true;
246
+ if (result.status === 128) return false;
247
+ throw new WorkflowError('COMMAND_FAILED', `Unable to inspect ${relativePath} at ${commit}.`, {
248
+ stderr: result.stderr.trim(),
249
+ exitCode: result.status,
250
+ });
251
+ }
252
+
253
+ function inspectRootManifest(repositoryRoot: string): {
254
+ readonly scripts: ReadonlyMap<string, string>;
255
+ readonly hash: string | null;
256
+ readonly blocker: string | null;
257
+ } {
258
+ const file = path.join(repositoryRoot, ROOT_MANIFEST);
259
+ if (!existsSync(file)) return { scripts: new Map(), hash: null, blocker: null };
260
+ const stat = lstatSync(file);
261
+ if (!stat.isFile()) {
262
+ return { scripts: new Map(), hash: null, blocker: 'Root package.json is not a regular file.' };
263
+ }
264
+ const bytes = readFileSync(file);
265
+ const hash = createHash('sha256').update(bytes).digest('hex');
266
+ try {
267
+ const parsed = JSON.parse(bytes.toString('utf8')) as { scripts?: unknown };
268
+ if (parsed.scripts === undefined) return { scripts: new Map(), hash, blocker: null };
269
+ if (!parsed.scripts || typeof parsed.scripts !== 'object' || Array.isArray(parsed.scripts)) {
270
+ return { scripts: new Map(), hash, blocker: 'Root package.json scripts field is malformed.' };
271
+ }
272
+ return {
273
+ scripts: new Map(Object.entries(parsed.scripts)
274
+ .filter((entry): entry is [string, string] => typeof entry[1] === 'string')),
275
+ hash,
276
+ blocker: null,
277
+ };
278
+ } catch {
279
+ return { scripts: new Map(), hash, blocker: 'Root package.json is not valid JSON.' };
280
+ }
281
+ }
282
+
283
+ function inspectRepositoryFile(repositoryRoot: string, relativePath: string): {
284
+ content: string | null;
285
+ hash: string | null;
286
+ blocker: string | null;
287
+ } {
288
+ const file = path.join(repositoryRoot, relativePath);
289
+ if (!existsSync(file)) return { content: null, hash: null, blocker: `${relativePath} is missing.` };
290
+ const stat = lstatSync(file);
291
+ if (!stat.isFile()) return { content: null, hash: null, blocker: `${relativePath} is not a regular file.` };
292
+ const bytes = readFileSync(file);
293
+ return {
294
+ content: bytes.toString('utf8'),
295
+ hash: createHash('sha256').update(bytes).digest('hex'),
296
+ blocker: null,
297
+ };
298
+ }