codex-workflow-v2 2.0.0-beta.12.9 → 2.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +58 -16
  2. package/dist/reviewer-runtime-build.json +1092 -0
  3. package/dist/src/alpha6/check-support-anchor.d.ts +24 -0
  4. package/dist/src/alpha6/check-support-anchor.js +102 -0
  5. package/dist/src/alpha6/check-support-anchor.js.map +1 -0
  6. package/dist/src/alpha6/component-owner.d.ts +28 -0
  7. package/dist/src/alpha6/component-owner.js +95 -0
  8. package/dist/src/alpha6/component-owner.js.map +1 -0
  9. package/dist/src/alpha6/downstream-proof.js +4 -2
  10. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  11. package/dist/src/alpha6/mechanical-feasibility.js +42 -12
  12. package/dist/src/alpha6/mechanical-feasibility.js.map +1 -1
  13. package/dist/src/alpha6/milestone.d.ts +6 -2
  14. package/dist/src/alpha6/milestone.js +164 -10
  15. package/dist/src/alpha6/milestone.js.map +1 -1
  16. package/dist/src/alpha6/npm-check-contract.d.ts +11 -0
  17. package/dist/src/alpha6/npm-check-contract.js +36 -0
  18. package/dist/src/alpha6/npm-check-contract.js.map +1 -0
  19. package/dist/src/alpha6/plan-integrity.js +157 -27
  20. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  21. package/dist/src/alpha6/plan-risk.js +3 -1
  22. package/dist/src/alpha6/plan-risk.js.map +1 -1
  23. package/dist/src/alpha6/remediation-cause.d.ts +13 -0
  24. package/dist/src/alpha6/remediation-cause.js +77 -0
  25. package/dist/src/alpha6/remediation-cause.js.map +1 -0
  26. package/dist/src/alpha6/remediation.d.ts +15 -4
  27. package/dist/src/alpha6/remediation.js +430 -67
  28. package/dist/src/alpha6/remediation.js.map +1 -1
  29. package/dist/src/alpha6/review.js +34 -4
  30. package/dist/src/alpha6/review.js.map +1 -1
  31. package/dist/src/artifacts.js +6 -0
  32. package/dist/src/artifacts.js.map +1 -1
  33. package/dist/src/cli-actions.d.ts +25 -0
  34. package/dist/src/cli-actions.js +109 -0
  35. package/dist/src/cli-actions.js.map +1 -0
  36. package/dist/src/cli.js +216 -258
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/contracts.d.ts +166 -5
  39. package/dist/src/credential-transport.d.ts +38 -0
  40. package/dist/src/credential-transport.js +156 -0
  41. package/dist/src/credential-transport.js.map +1 -0
  42. package/dist/src/dependency-provenance.d.ts +23 -2
  43. package/dist/src/dependency-provenance.js +221 -34
  44. package/dist/src/dependency-provenance.js.map +1 -1
  45. package/dist/src/domain/completed-step-carryover.d.ts +13 -0
  46. package/dist/src/domain/completed-step-carryover.js +71 -0
  47. package/dist/src/domain/completed-step-carryover.js.map +1 -0
  48. package/dist/src/domain/validation.d.ts +9 -1
  49. package/dist/src/domain/validation.js +86 -0
  50. package/dist/src/domain/validation.js.map +1 -1
  51. package/dist/src/gateway-handshake.d.ts +11 -0
  52. package/dist/src/gateway-handshake.js +92 -0
  53. package/dist/src/gateway-handshake.js.map +1 -0
  54. package/dist/src/index.d.ts +2 -0
  55. package/dist/src/index.js +2 -0
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +5 -0
  58. package/dist/src/lifecycle/corrective-replan-binding-manifest.js +9 -0
  59. package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -1
  60. package/dist/src/lifecycle/corrective-replan.js +1 -1
  61. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  62. package/dist/src/lifecycle/semantic-registry.js +1 -1
  63. package/dist/src/observation.js +1 -2
  64. package/dist/src/observation.js.map +1 -1
  65. package/dist/src/observed-routes.d.ts +114 -0
  66. package/dist/src/observed-routes.js +545 -0
  67. package/dist/src/observed-routes.js.map +1 -0
  68. package/dist/src/operational-contract.d.ts +26 -0
  69. package/dist/src/operational-contract.js +94 -0
  70. package/dist/src/operational-contract.js.map +1 -0
  71. package/dist/src/reviewer.d.ts +218 -8
  72. package/dist/src/reviewer.js +927 -25
  73. package/dist/src/reviewer.js.map +1 -1
  74. package/dist/src/state/corrective-replan-executor.js +18 -15
  75. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  76. package/dist/src/state/corrective-replan-public.js +35 -10
  77. package/dist/src/state/corrective-replan-public.js.map +1 -1
  78. package/dist/src/state/corrective-replan-transaction.js +12 -4
  79. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  80. package/dist/src/state/corrective-yield-transaction.js +11 -2
  81. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  82. package/dist/src/state/lock.d.ts +1 -0
  83. package/dist/src/state/lock.js +24 -0
  84. package/dist/src/state/lock.js.map +1 -1
  85. package/dist/src/version.d.ts +1 -1
  86. package/dist/src/version.js +1 -1
  87. package/dist/src/version.js.map +1 -1
  88. package/dist/src/workflow.d.ts +65 -4
  89. package/dist/src/workflow.js +1704 -155
  90. package/dist/src/workflow.js.map +1 -1
  91. package/docs/autonomy-guardrails.md +41 -14
  92. package/docs/delegated-approval.md +6 -1
  93. package/docs/development-flow.md +42 -12
  94. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  95. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  96. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  97. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +181 -22
  98. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +148 -26
  99. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +326 -52
  100. package/docs/problem-briefs/01-pre-implementation-integrity.md +11 -7
  101. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +13 -6
  102. package/docs/problem-briefs/README.md +11 -11
  103. package/docs/release.md +83 -9
  104. package/docs/updating-existing-project.md +205 -0
  105. package/docs/validation-report.md +156 -99
  106. package/package.json +12 -3
  107. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +42 -1
  108. package/schemas/authorization-event.schema.json +10 -3
  109. package/schemas/corrective-decision-event.schema.json +164 -9
  110. package/schemas/corrective-replan-credentials.private.schema.json +4 -2
  111. package/schemas/milestone-scope-change-event.schema.json +25 -1
  112. package/schemas/remediation-event.schema.json +17 -1
  113. package/schemas/review-result.schema.json +1 -0
  114. package/schemas/step-review-event.schema.json +15 -0
  115. package/schemas/task.schema.json +61 -1
  116. package/schemas/transition-definition.schema.json +8 -0
@@ -1,13 +1,17 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, lstatSync, mkdtempSync, readFileSync, readlinkSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
3
5
  import os from 'node:os';
4
6
  import path from 'node:path';
5
- import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
6
9
  import { canonicalJsonStringify } from './alpha6/store-sidecars.js';
7
10
  import { changedFiles, headCommit, runGit } from './repository.js';
11
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
8
12
  const FULL_GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
9
13
  export const STRICT_REVIEWER_TIMEOUT_MS = 180_000;
10
- const REVIEW_SCHEMA = {
14
+ export const STRICT_REVIEW_OUTPUT_SCHEMA = {
11
15
  type: 'object',
12
16
  additionalProperties: false,
13
17
  required: ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'],
@@ -21,7 +25,17 @@ const REVIEW_SCHEMA = {
21
25
  items: {
22
26
  type: 'object',
23
27
  additionalProperties: false,
24
- required: ['id', 'severity', 'requirement', 'summary', 'evidence', 'requiredAction'],
28
+ required: [
29
+ 'id',
30
+ 'severity',
31
+ 'requirement',
32
+ 'summary',
33
+ 'evidence',
34
+ 'requiredAction',
35
+ 'causeId',
36
+ 'route',
37
+ 'planConflict',
38
+ ],
25
39
  properties: {
26
40
  id: { type: 'string', minLength: 1 },
27
41
  severity: { enum: ['critical', 'major', 'minor'] },
@@ -29,6 +43,12 @@ const REVIEW_SCHEMA = {
29
43
  summary: { type: 'string', minLength: 1 },
30
44
  evidence: { type: 'string', minLength: 1 },
31
45
  requiredAction: { type: 'string', minLength: 1 },
46
+ causeId: {
47
+ anyOf: [
48
+ { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$' },
49
+ { type: 'null' },
50
+ ],
51
+ },
32
52
  route: { enum: ['fix', 'replan'] },
33
53
  planConflict: {
34
54
  anyOf: [
@@ -50,7 +70,9 @@ const REVIEW_SCHEMA = {
50
70
  },
51
71
  },
52
72
  };
53
- export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, runner = spawnReviewer) {
73
+ /** @deprecated Use STRICT_REVIEW_OUTPUT_SCHEMA. Retained for direct-module compatibility. */
74
+ export const REVIEW_SCHEMA = STRICT_REVIEW_OUTPUT_SCHEMA;
75
+ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, runner = spawnReviewer, scopeAugmentations = []) {
54
76
  const launch = runStrictReviewerLaunch(repositoryRoot, {
55
77
  protocol: 'codex-workflow-strict-review-v1',
56
78
  task,
@@ -64,6 +86,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
64
86
  'Before reviewing, attempt to create .codex-reviewer-write-probe in the repository.',
65
87
  'The attempt must be denied by the sandbox. Report isolationProbe="denied" only when it was denied.',
66
88
  'For each finding use route="fix" by default. Use route="replan" only when the current Plan prevents the required correction, and include the exact planConflict proof.',
89
+ 'Give each finding a stable causeId for the underlying defect; never encode the review attempt number.',
67
90
  'Do not modify any repository file. Return only the requested JSON result.',
68
91
  ],
69
92
  envelope,
@@ -75,6 +98,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
75
98
  plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
76
99
  evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
77
100
  diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
101
+ scopeAugmentations,
78
102
  },
79
103
  },
80
104
  }, runner);
@@ -89,7 +113,7 @@ export function launchStrictReviewer(repositoryRoot, task, taskRoot, envelope, r
89
113
  after: launch.after,
90
114
  };
91
115
  }
92
- export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, runner = spawnReviewer) {
116
+ export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, runner = spawnReviewer, scopeAugmentations = []) {
93
117
  const step = task.steps.find((candidate) => candidate.id === stepId);
94
118
  if (!step) {
95
119
  return failedStepLaunch(repositoryRoot, [], null, `Step ${stepId} was not found on task ${task.id}.`, currentEvidence.commitSha);
@@ -108,7 +132,7 @@ export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelop
108
132
  if (reviewedCommit !== currentHead) {
109
133
  return failedStepLaunch(repositoryRoot, [], null, `Strict step reviewer requires the canonical Step evidence commit to match repository HEAD for ${stepId} on task ${task.id}.`, reviewedCommit);
110
134
  }
111
- const packet = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence);
135
+ const packet = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations);
112
136
  const launch = runStrictReviewerLaunch(repositoryRoot, {
113
137
  protocol: 'codex-workflow-strict-step-review-v1',
114
138
  task,
@@ -130,7 +154,8 @@ export function launchStrictStepReviewer(repositoryRoot, task, taskRoot, envelop
130
154
  reviewerOutputHash: launch.reviewerOutputHash,
131
155
  };
132
156
  }
133
- export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence) {
157
+ export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, taskRoot, envelope, stepId, currentEvidence, scopeAugmentations = [], reviewMode = 'ordinary') {
158
+ assertReviewMode(reviewMode);
134
159
  const step = task.steps.find((candidate) => candidate.id === stepId);
135
160
  if (!step || !step.evidence || canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
136
161
  throw new Error(`Strict Step Review packet requires exact canonical evidence for ${stepId}.`);
@@ -139,7 +164,16 @@ export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, task
139
164
  if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit) || reviewedCommit !== headCommit(repositoryRoot)) {
140
165
  throw new Error(`Strict Step Review packet requires ${stepId} evidence to match repository HEAD.`);
141
166
  }
142
- const packet = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence);
167
+ const reviewerRuntime = prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:${stepId}:${reviewedCommit}:${reviewMode}`);
168
+ assertReviewerRuntimePrepared(reviewerRuntime);
169
+ const basePacket = strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations);
170
+ const packet = {
171
+ ...basePacket,
172
+ instruction: [...basePacket.instruction, reviewInstruction(reviewMode)],
173
+ reviewMode,
174
+ reviewPolicy: reviewPolicy(reviewMode),
175
+ reviewerRuntime,
176
+ };
143
177
  const repositorySeal = sealRepository(repositoryRoot);
144
178
  return {
145
179
  protocol: 'codex-workflow-external-strict-step-review-v1',
@@ -150,13 +184,18 @@ export function prepareExternalStrictStepReviewPacket(repositoryRoot, task, task
150
184
  packetHash: canonicalHash(packet),
151
185
  repositorySeal,
152
186
  repositorySealHash: canonicalHash(repositorySeal),
187
+ reviewMode,
188
+ reviewerRuntime,
153
189
  };
154
190
  }
155
- export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, taskRoot, envelope) {
191
+ export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, taskRoot, envelope, scopeAugmentations = [], reviewMode = 'ordinary') {
192
+ assertReviewMode(reviewMode);
156
193
  const reviewedCommit = headCommit(repositoryRoot);
157
194
  if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
158
195
  throw new Error(`Strict Task Review packet requires a full lowercase Git SHA for ${task.id}.`);
159
196
  }
197
+ const reviewerRuntime = prepareReviewerRuntimePreflight(repositoryRoot, `${task.id}:task:${reviewedCommit}:${reviewMode}`);
198
+ assertReviewerRuntimePrepared(reviewerRuntime);
160
199
  const packet = {
161
200
  protocol: 'codex-workflow-strict-review-v1',
162
201
  instruction: [
@@ -164,9 +203,13 @@ export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, task
164
203
  'Review the supplied Task result and bind every finding to reviewedCommit.',
165
204
  'For each finding use route="fix" by default. Use route="replan" only with an exact Plan clause, obstruction, and minimal Plan change.',
166
205
  'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
206
+ reviewInstruction(reviewMode),
167
207
  ],
208
+ reviewMode,
209
+ reviewPolicy: reviewPolicy(reviewMode),
210
+ reviewerRuntime,
168
211
  envelope,
169
- task: strictTaskReviewSubject(repositoryRoot, task, taskRoot),
212
+ task: { ...strictTaskReviewSubject(repositoryRoot, task, taskRoot), scopeAugmentations },
170
213
  };
171
214
  const repositorySeal = sealRepository(repositoryRoot);
172
215
  return {
@@ -177,6 +220,8 @@ export function prepareExternalStrictTaskReviewPacket(repositoryRoot, task, task
177
220
  packetHash: canonicalHash(packet),
178
221
  repositorySeal,
179
222
  repositorySealHash: canonicalHash(repositorySeal),
223
+ reviewMode,
224
+ reviewerRuntime,
180
225
  };
181
226
  }
182
227
  export function validateExternalStrictStepReviewInput(value, expected) {
@@ -188,13 +233,14 @@ export function validateExternalStrictTaskReviewInput(value, expected) {
188
233
  export function strictReviewInputHash(review) {
189
234
  return hashReviewInput(review);
190
235
  }
191
- function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence) {
236
+ function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step, reviewedCommit, currentEvidence, scopeAugmentations) {
192
237
  return {
193
238
  protocol: 'codex-workflow-strict-step-review-v1',
194
239
  instruction: [
195
240
  'Act only as the independent reviewer described by the context envelope.',
196
241
  'Review only the supplied Task Step and bind findings to reviewedCommit.',
197
242
  'For each finding set route="fix" unless the required correction cannot fit the current Plan. Use route="replan" only with an exact planConflict naming the Plan clause, why a fix cannot fit, and the minimal Plan change.',
243
+ 'When a finding has a stable semantic root cause, provide a causeId that remains unchanged across review attempts; never derive it from attempt number, prose wording, timestamps, or temporary paths.',
198
244
  'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
199
245
  ],
200
246
  envelope,
@@ -219,6 +265,7 @@ function strictStepReviewPayload(repositoryRoot, task, taskRoot, envelope, step,
219
265
  status: step.status,
220
266
  reviewedCommit,
221
267
  evidence: currentEvidence,
268
+ scopeAugmentations,
222
269
  },
223
270
  };
224
271
  }
@@ -234,7 +281,7 @@ function strictTaskReviewSubject(repositoryRoot, task, taskRoot) {
234
281
  };
235
282
  }
236
283
  function validateExternalReviewEnvelope(value, expected) {
237
- if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'review'])) {
284
+ if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'reviewMode', 'review'])) {
238
285
  throw new Error('invalid external review envelope');
239
286
  }
240
287
  if (value.protocol !== expected.protocol)
@@ -243,6 +290,8 @@ function validateExternalReviewEnvelope(value, expected) {
243
290
  throw new Error('external review packet hash mismatch');
244
291
  if (value.repositorySealHash !== expected.repositorySealHash)
245
292
  throw new Error('external review repository seal mismatch');
293
+ if (value.reviewMode !== expected.reviewMode)
294
+ throw new Error('external review mode mismatch');
246
295
  if (typeof value.reviewerThreadId !== 'string' || !value.reviewerThreadId.trim()) {
247
296
  throw new Error('external reviewer thread id is required');
248
297
  }
@@ -269,6 +318,11 @@ function validateExternalReviewEnvelope(value, expected) {
269
318
  const route = (candidate.route === undefined ? 'fix' : candidate.route);
270
319
  if (route !== 'fix' && route !== 'replan')
271
320
  throw new Error('invalid external strict review finding route');
321
+ const causeId = candidate.causeId;
322
+ if (causeId !== undefined
323
+ && (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
324
+ throw new Error('invalid external strict review finding causeId');
325
+ }
272
326
  const planConflict = validatePlanConflict(candidate.planConflict, route);
273
327
  return {
274
328
  id: candidate.id,
@@ -277,6 +331,7 @@ function validateExternalReviewEnvelope(value, expected) {
277
331
  summary: candidate.summary,
278
332
  evidence: candidate.evidence,
279
333
  requiredAction: candidate.requiredAction,
334
+ ...(causeId === undefined ? {} : { causeId }),
280
335
  route,
281
336
  planConflict,
282
337
  };
@@ -291,13 +346,816 @@ function validateExternalReviewEnvelope(value, expected) {
291
346
  findings,
292
347
  };
293
348
  }
294
- function isReviewFindingRecord(value) {
295
- if (!value || typeof value !== 'object' || Array.isArray(value))
349
+ export function prepareReviewerRuntimePreflight(repositoryRoot, binding) {
350
+ const root = path.resolve(repositoryRoot);
351
+ const manifest = path.join(root, 'package.json');
352
+ const lockfile = path.join(root, 'package-lock.json');
353
+ const nodeModulesPath = path.join(root, 'node_modules');
354
+ const runtimeModulePath = fileURLToPath(import.meta.url);
355
+ const installedPackageRoot = path.join(nodeModulesPath, PACKAGE_NAME);
356
+ const installedPackageManifestPath = path.join(installedPackageRoot, 'package.json');
357
+ const runtimeBuildManifestPath = path.join(installedPackageRoot, 'dist', 'reviewer-runtime-build.json');
358
+ const installedReviewerModulePath = path.join(installedPackageRoot, 'dist', 'src', 'reviewer.js');
359
+ const installedCliPath = path.join(installedPackageRoot, 'dist', 'src', 'cli.js');
360
+ const installedGatewayModulePath = path.join(installedPackageRoot, 'dist', 'src', 'gateway-handshake.js');
361
+ const installedWorkflowModulePath = path.join(installedPackageRoot, 'dist', 'src', 'workflow.js');
362
+ const cachePath = path.join(os.tmpdir(), 'codex-workflow-review-runtime', canonicalHash({ repositoryRoot: root, binding }));
363
+ mkdirSync(cachePath, { recursive: true, mode: 0o700 });
364
+ chmodSync(cachePath, 0o700);
365
+ const cacheProbe = path.join(cachePath, '.write-probe');
366
+ let cacheWritable = false;
367
+ try {
368
+ writeFileSync(cacheProbe, 'ok\n', { mode: 0o600 });
369
+ rmSync(cacheProbe, { force: true });
370
+ cacheWritable = true;
371
+ }
372
+ catch {
373
+ rmSync(cacheProbe, { force: true });
374
+ }
375
+ const outsideSealedCheckout = isOutside(root, cachePath);
376
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
377
+ const packageManifestPath = existsSync(manifest) ? manifest : null;
378
+ const packageLockPath = existsSync(lockfile) ? lockfile : null;
379
+ const nodeModulesPresent = existsSync(nodeModulesPath) && statSync(nodeModulesPath).isDirectory();
380
+ const repositoryManifest = readJsonRecord(packageManifestPath);
381
+ const repositoryLock = readJsonRecord(packageLockPath);
382
+ const installedPackageDirectory = isPlainDirectory(installedPackageRoot);
383
+ const installedManifest = readJsonRecord(installedPackageDirectory && isPlainFileInside(installedPackageRoot, installedPackageManifestPath)
384
+ ? installedPackageManifestPath
385
+ : null);
386
+ const declaredVersion = nestedString(repositoryManifest, ['devDependencies', PACKAGE_NAME]);
387
+ const lockedVersion = nestedString(repositoryLock, ['packages', `node_modules/${PACKAGE_NAME}`, 'version']);
388
+ const lockDeclaredVersion = nestedString(repositoryLock, ['packages', '', 'devDependencies', PACKAGE_NAME]);
389
+ const installedVersion = stringField(installedManifest, 'version');
390
+ const installedName = stringField(installedManifest, 'name');
391
+ const exactRepositoryLocal = declaredVersion === PACKAGE_VERSION
392
+ && lockedVersion === PACKAGE_VERSION
393
+ && lockDeclaredVersion === PACKAGE_VERSION
394
+ && installedName === PACKAGE_NAME
395
+ && installedVersion === PACKAGE_VERSION;
396
+ const runtimeModuleSha256 = sha256File(runtimeModulePath);
397
+ const runtimeCliModulePath = fileURLToPath(new URL('./cli.js', import.meta.url));
398
+ const runtimeGatewayModulePath = fileURLToPath(new URL('./gateway-handshake.js', import.meta.url));
399
+ const runtimeWorkflowModulePath = fileURLToPath(new URL('./workflow.js', import.meta.url));
400
+ const buildReadiness = inspectReviewerBuild(installedPackageRoot, runtimeBuildManifestPath, installedPackageManifestPath, installedCliPath, sha256File(runtimeCliModulePath), installedReviewerModulePath, runtimeModuleSha256, installedGatewayModulePath, sha256File(runtimeGatewayModulePath), installedWorkflowModulePath, sha256File(runtimeWorkflowModulePath));
401
+ const runtimeBuildManifest = buildReadiness.state === 'ready'
402
+ ? parseRuntimeBuildManifest(readJsonRecord(runtimeBuildManifestPath))
403
+ : null;
404
+ const runtimeDependencyReadiness = inspectRuntimeDependencyClosure(nodeModulesPath, installedPackageRoot, runtimeBuildManifest, exactRepositoryLocal && buildReadiness.state === 'ready');
405
+ const runtimeCheck = inspectReviewerRuntimeCheck(installedPackageManifestPath, installedGatewayModulePath, installedReviewerModulePath, installedWorkflowModulePath, runtimeDependencyReadiness.evidenceSha256, exactRepositoryLocal
406
+ && buildReadiness.state === 'ready'
407
+ && runtimeDependencyReadiness.state === 'ready');
408
+ const dependencyState = packageManifestPath === null
409
+ ? 'not-applicable'
410
+ : !nodeModulesPresent || installedManifest === null
411
+ ? 'not-installed'
412
+ : !exactRepositoryLocal
413
+ ? 'version-mismatch'
414
+ : buildReadiness.state === 'missing'
415
+ ? 'build-missing'
416
+ : buildReadiness.state === 'stale'
417
+ ? 'build-stale'
418
+ : runtimeDependencyReadiness.state === 'missing'
419
+ ? 'runtime-dependency-missing'
420
+ : runtimeDependencyReadiness.state === 'stale'
421
+ ? 'runtime-dependency-stale'
422
+ : runtimeCheck.state !== 'passed'
423
+ ? 'runtime-check-failed'
424
+ : 'ready';
425
+ const blockers = [
426
+ ...(nodeMajor >= 22 ? [] : [`Node ${process.version} is below required major 22.`]),
427
+ ...(packageManifestPath ? [] : ['Repository package.json is required to prove the exact Workflow dependency.']),
428
+ ...(declaredVersion === PACKAGE_VERSION
429
+ ? []
430
+ : [`Repository devDependencies must declare exact ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${declaredVersion ?? 'missing'}.`]),
431
+ ...(packageLockPath ? [] : ['Repository package-lock.json is required to prove the locked Workflow dependency.']),
432
+ ...(lockedVersion === PACKAGE_VERSION && lockDeclaredVersion === PACKAGE_VERSION
433
+ ? []
434
+ : [`Repository lockfile must bind both root and installed ${PACKAGE_NAME} entries to ${PACKAGE_VERSION}; found root ${lockDeclaredVersion ?? 'missing'}, package ${lockedVersion ?? 'missing'}.`]),
435
+ ...(installedManifest ? [] : [`Install exact ${PACKAGE_NAME}@${PACKAGE_VERSION} in repository-local node_modules before dispatching review.`]),
436
+ ...(installedManifest === null || (installedName === PACKAGE_NAME && installedVersion === PACKAGE_VERSION)
437
+ ? []
438
+ : [`Repository-local installed Workflow package must be ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${installedName ?? 'missing'}@${installedVersion ?? 'missing'}.`]),
439
+ ...(installedManifest === null || installedName !== PACKAGE_NAME || installedVersion !== PACKAGE_VERSION
440
+ ? []
441
+ : buildReadiness.state === 'ready'
442
+ ? []
443
+ : buildReadiness.state === 'missing'
444
+ ? [`Build evidence is missing or incomplete at ${runtimeBuildManifestPath}; reinstall the exact published package or build it before sealing review.`]
445
+ : [`Build evidence at ${runtimeBuildManifestPath} does not match installed runtime bytes; reinstall the exact package before sealing review.`]),
446
+ ...(exactRepositoryLocal && buildReadiness.state === 'ready' && runtimeDependencyReadiness.state !== 'ready'
447
+ ? runtimeDependencyReadiness.state === 'missing'
448
+ ? ['The attested repository-local runtime dependency closure is incomplete; reinstall the exact package dependencies before sealing review.']
449
+ : ['The attested repository-local runtime dependency closure has version, path, symlink, or content drift; reinstall the exact package dependencies before sealing review.']
450
+ : []),
451
+ ...(exactRepositoryLocal
452
+ && buildReadiness.state === 'ready'
453
+ && runtimeDependencyReadiness.state === 'ready'
454
+ && runtimeCheck.state !== 'passed'
455
+ ? runtimeCheck.failureKind === 'contract-mismatch'
456
+ ? [`Repository-local gateway handshake did not report exact package/protocol/schema authority from ${installedGatewayModulePath}; reinstall the exact package before sealing review.`]
457
+ : [`Repository-local reviewer runtime could not load its exact gateway/reviewer/workflow graph from ${installedPackageRoot}; verify package dependencies and reinstall the exact package before sealing review.`]
458
+ : []),
459
+ ...(cacheWritable ? [] : ['Ephemeral reviewer cache is not writable.']),
460
+ ...(outsideSealedCheckout ? [] : ['Ephemeral reviewer cache is inside the sealed checkout.']),
461
+ ];
462
+ return {
463
+ protocol: 'codex-workflow-reviewer-runtime-preflight-v1',
464
+ prepared: blockers.length === 0,
465
+ node: {
466
+ executable: process.execPath,
467
+ version: process.version,
468
+ requiredMajor: 22,
469
+ supported: nodeMajor >= 22,
470
+ },
471
+ workflowPackage: {
472
+ name: PACKAGE_NAME,
473
+ version: PACKAGE_VERSION,
474
+ runtimeModulePath,
475
+ runtimeModuleSha256,
476
+ installedPackageRoot,
477
+ installedPackageManifestPath,
478
+ installedPackageManifestSha256: installedManifest ? sha256File(installedPackageManifestPath) : null,
479
+ declaredVersion,
480
+ lockedVersion,
481
+ installedVersion,
482
+ exactRepositoryLocal,
483
+ },
484
+ buildReadiness,
485
+ runtimeDependencyReadiness,
486
+ runtimeCheck,
487
+ repositoryDependencyReadiness: {
488
+ packageManifestPath,
489
+ packageManifestSha256: packageManifestPath ? sha256File(packageManifestPath) : null,
490
+ packageLockPath,
491
+ packageLockSha256: packageLockPath ? sha256File(packageLockPath) : null,
492
+ nodeModulesPath,
493
+ nodeModulesPresent,
494
+ state: dependencyState,
495
+ installAttempted: false,
496
+ installCommand: null,
497
+ },
498
+ ephemeralCache: {
499
+ path: cachePath,
500
+ writable: cacheWritable,
501
+ outsideSealedCheckout,
502
+ lifecycle: 'os-temporary',
503
+ },
504
+ sealedCheckoutMutationAllowed: false,
505
+ blockers,
506
+ };
507
+ }
508
+ function inspectReviewerRuntimeCheck(installedPackageManifestPath, gatewayEntrypointPath, reviewerEntrypointPath, workflowEntrypointPath, dependencyClosureEvidenceSha256, eligible) {
509
+ const gatewayEntrypointSha256 = eligible ? sha256File(gatewayEntrypointPath) : null;
510
+ const reviewerEntrypointSha256 = eligible ? sha256File(reviewerEntrypointPath) : null;
511
+ const workflowEntrypointSha256 = eligible ? sha256File(workflowEntrypointPath) : null;
512
+ if (!eligible) {
513
+ return runtimeCheckEvidence({
514
+ state: 'not-run',
515
+ gatewayEntrypointPath,
516
+ gatewayEntrypointSha256,
517
+ reviewerEntrypointPath,
518
+ reviewerEntrypointSha256,
519
+ workflowEntrypointPath,
520
+ workflowEntrypointSha256,
521
+ dependencyClosureEvidenceSha256,
522
+ observedPackageName: null,
523
+ observedPackageVersion: null,
524
+ observedProtocolVersion: null,
525
+ observedStateSchemaVersion: null,
526
+ resultSha256: null,
527
+ failureKind: null,
528
+ });
529
+ }
530
+ let observedPackageName = null;
531
+ let observedPackageVersion = null;
532
+ let observedProtocolVersion = null;
533
+ let observedStateSchemaVersion = null;
534
+ let resultSha256 = null;
535
+ try {
536
+ const requireInstalled = createRequire(installedPackageManifestPath);
537
+ const gatewayModule = runtimeModuleRecord(requireInstalled(gatewayEntrypointPath));
538
+ const reviewerModule = runtimeModuleRecord(requireInstalled(reviewerEntrypointPath));
539
+ const workflowModule = runtimeModuleRecord(requireInstalled(workflowEntrypointPath));
540
+ const handshake = gatewayModule && typeof gatewayModule.gatewayHandshake === 'function'
541
+ ? runtimeModuleRecord(gatewayModule.gatewayHandshake())
542
+ : null;
543
+ if (handshake) {
544
+ observedPackageName = typeof handshake.packageName === 'string' ? handshake.packageName : null;
545
+ observedPackageVersion = typeof handshake.packageVersion === 'string' ? handshake.packageVersion : null;
546
+ observedProtocolVersion = typeof handshake.protocolVersion === 'number' ? handshake.protocolVersion : null;
547
+ observedStateSchemaVersion = typeof handshake.stateSchemaVersion === 'number' ? handshake.stateSchemaVersion : null;
548
+ resultSha256 = canonicalHash(handshake);
549
+ }
550
+ const passed = handshake !== null
551
+ && reviewerModule !== null
552
+ && typeof reviewerModule.prepareReviewerRuntimePreflight === 'function'
553
+ && workflowModule !== null
554
+ && typeof workflowModule.WorkflowService === 'function'
555
+ && observedPackageName === PACKAGE_NAME
556
+ && observedPackageVersion === PACKAGE_VERSION
557
+ && observedProtocolVersion === PROTOCOL_VERSION
558
+ && observedStateSchemaVersion === STATE_SCHEMA_VERSION;
559
+ return runtimeCheckEvidence({
560
+ state: passed ? 'passed' : 'failed',
561
+ gatewayEntrypointPath,
562
+ gatewayEntrypointSha256,
563
+ reviewerEntrypointPath,
564
+ reviewerEntrypointSha256,
565
+ workflowEntrypointPath,
566
+ workflowEntrypointSha256,
567
+ dependencyClosureEvidenceSha256,
568
+ observedPackageName,
569
+ observedPackageVersion,
570
+ observedProtocolVersion,
571
+ observedStateSchemaVersion,
572
+ resultSha256,
573
+ failureKind: passed ? null : 'contract-mismatch',
574
+ });
575
+ }
576
+ catch {
577
+ return runtimeCheckEvidence({
578
+ state: 'failed',
579
+ gatewayEntrypointPath,
580
+ gatewayEntrypointSha256,
581
+ reviewerEntrypointPath,
582
+ reviewerEntrypointSha256,
583
+ workflowEntrypointPath,
584
+ workflowEntrypointSha256,
585
+ dependencyClosureEvidenceSha256,
586
+ observedPackageName,
587
+ observedPackageVersion,
588
+ observedProtocolVersion,
589
+ observedStateSchemaVersion,
590
+ resultSha256,
591
+ failureKind: 'module-load-failed',
592
+ });
593
+ }
594
+ }
595
+ function assertReviewerRuntimePrepared(preflight) {
596
+ if (!preflight.prepared) {
597
+ throw new Error(`External sealed review packet requires a prepared reviewer runtime: ${preflight.blockers.join(' ')}`);
598
+ }
599
+ }
600
+ function runtimeCheckEvidence(value) {
601
+ const evidence = {
602
+ kind: 'repository-local-gateway-load',
603
+ loader: 'node:createRequire',
604
+ executable: process.execPath,
605
+ ...value,
606
+ sealedCheckoutMutationAllowed: false,
607
+ };
608
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
609
+ }
610
+ function runtimeModuleRecord(value) {
611
+ return value && typeof value === 'object' && !Array.isArray(value)
612
+ ? value
613
+ : null;
614
+ }
615
+ function inspectRuntimeDependencyClosure(nodeModulesPath, installedPackageRoot, manifest, eligible) {
616
+ const expected = manifest?.runtimeDependencies ?? [];
617
+ const expectedFileCount = expected.reduce((total, dependency) => total + dependency.files.length, 0);
618
+ if (!eligible || !manifest) {
619
+ const evidence = {
620
+ state: 'not-run',
621
+ expectedPackageCount: expected.length,
622
+ verifiedPackageCount: 0,
623
+ expectedFileCount,
624
+ verifiedFileCount: 0,
625
+ packages: [],
626
+ };
627
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
628
+ }
629
+ const nodeModulesReady = (() => {
630
+ try {
631
+ return lstatSync(nodeModulesPath).isDirectory()
632
+ && isUnredirectedPathInside(path.dirname(nodeModulesPath), nodeModulesPath);
633
+ }
634
+ catch {
635
+ return false;
636
+ }
637
+ })();
638
+ const repositoryRoot = path.dirname(nodeModulesPath);
639
+ const expectedByName = new Map(expected.map((dependency) => [dependency.name, dependency]));
640
+ const installedWorkflowManifest = readJsonRecord(path.join(installedPackageRoot, 'package.json'));
641
+ const rootDependencyNames = runtimeDependencyNames(installedWorkflowManifest);
642
+ let graphDrift = rootDependencyNames === null;
643
+ const queue = (rootDependencyNames ?? [])
644
+ .map((name) => ({ name, parentPackageRoot: installedPackageRoot }));
645
+ const queuedEdges = new Set(queue.map(({ name, parentPackageRoot }) => `${parentPackageRoot}\0${name}`));
646
+ const inspectedInstances = new Map();
647
+ while (queue.length > 0) {
648
+ const edge = queue.shift();
649
+ const expectedDependency = expectedByName.get(edge.name);
650
+ if (!expectedDependency) {
651
+ graphDrift = true;
652
+ continue;
653
+ }
654
+ const resolution = resolveRuntimeDependencyPackageRoot(repositoryRoot, edge.parentPackageRoot, edge.name);
655
+ const packageRoot = resolution ?? runtimeDependencySearchPaths(repositoryRoot, edge.parentPackageRoot, edge.name)[0] ?? path.join(nodeModulesPath, ...edge.name.split('/'));
656
+ const instanceKey = `${edge.name}\0${packageRoot}`;
657
+ let inspected = inspectedInstances.get(instanceKey);
658
+ if (!inspected) {
659
+ inspected = inspectRuntimeDependencyPackage(repositoryRoot, packageRoot, expectedDependency, nodeModulesReady);
660
+ inspectedInstances.set(instanceKey, inspected);
661
+ }
662
+ if (inspected.state !== 'ready')
663
+ continue;
664
+ const installedDependencyManifest = readJsonRecord(inspected.packageManifestPath);
665
+ const childNames = runtimeDependencyNames(installedDependencyManifest);
666
+ if (childNames === null) {
667
+ graphDrift = true;
668
+ continue;
669
+ }
670
+ for (const childName of childNames) {
671
+ if (!expectedByName.has(childName)) {
672
+ graphDrift = true;
673
+ continue;
674
+ }
675
+ const edgeKey = `${packageRoot}\0${childName}`;
676
+ if (queuedEdges.has(edgeKey))
677
+ continue;
678
+ queuedEdges.add(edgeKey);
679
+ queue.push({ name: childName, parentPackageRoot: packageRoot });
680
+ }
681
+ }
682
+ const packages = [...inspectedInstances.values()].sort((left, right) => (left.name.localeCompare(right.name) || left.packageRoot.localeCompare(right.packageRoot)));
683
+ const packageInstancesByName = new Map();
684
+ for (const dependency of packages) {
685
+ const instances = packageInstancesByName.get(dependency.name) ?? [];
686
+ instances.push(dependency);
687
+ packageInstancesByName.set(dependency.name, instances);
688
+ }
689
+ const verifiedNames = expected.filter((dependency) => {
690
+ const instances = packageInstancesByName.get(dependency.name) ?? [];
691
+ return instances.length > 0 && instances.every((instance) => instance.state === 'ready');
692
+ });
693
+ const expectedNamesReached = verifiedNames.length === expected.length;
694
+ const state = graphDrift
695
+ || packages.some((dependency) => dependency.state === 'stale')
696
+ || (packages.length === 0 && expected.length > 0 && nodeModulesReady)
697
+ ? 'stale'
698
+ : packages.some((dependency) => dependency.state === 'missing')
699
+ || !nodeModulesReady
700
+ ? 'missing'
701
+ : expectedNamesReached
702
+ ? 'ready'
703
+ : 'stale';
704
+ const evidence = {
705
+ state,
706
+ expectedPackageCount: expected.length,
707
+ verifiedPackageCount: verifiedNames.length,
708
+ expectedFileCount,
709
+ verifiedFileCount: verifiedNames.reduce((total, dependency) => total + dependency.files.length, 0),
710
+ packages,
711
+ };
712
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
713
+ }
714
+ function inspectRuntimeDependencyPackage(repositoryRoot, packageRoot, expected, nodeModulesReady) {
715
+ const packageManifestPath = path.join(packageRoot, 'package.json');
716
+ const base = {
717
+ name: expected.name,
718
+ version: expected.version,
719
+ packageRoot,
720
+ packageManifestPath,
721
+ expectedFileCount: expected.files.length,
722
+ };
723
+ if (!nodeModulesReady || !existsSync(packageRoot)) {
724
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
725
+ }
726
+ try {
727
+ if (!isPackageName(expected.name) || !lstatSync(packageRoot).isDirectory()) {
728
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
729
+ }
730
+ const realRepositoryRoot = realpathSync(repositoryRoot);
731
+ const realPackageRoot = realpathSync(packageRoot);
732
+ if (!isUnredirectedPathInside(repositoryRoot, packageRoot)
733
+ || isOutside(realRepositoryRoot, realPackageRoot)) {
734
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
735
+ }
736
+ const manifestFile = inspectRuntimeFile(packageRoot, packageManifestPath);
737
+ if (manifestFile.state === 'missing') {
738
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
739
+ }
740
+ const installedManifest = readJsonRecord(manifestFile.state === 'ready' ? packageManifestPath : null);
741
+ if (manifestFile.state !== 'ready'
742
+ || manifestFile.sha256 !== expected.packageManifestSha256
743
+ || stringField(installedManifest, 'name') !== expected.name
744
+ || stringField(installedManifest, 'version') !== expected.version) {
745
+ return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
746
+ }
747
+ const collected = collectRuntimeDependencyFiles(packageRoot);
748
+ if (collected.unsafe) {
749
+ return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
750
+ }
751
+ const observed = new Map(collected.files.map((file) => [file.path, file.sha256]));
752
+ let missing = false;
753
+ let drift = collected.files.length !== expected.files.length;
754
+ let verifiedFileCount = 0;
755
+ for (const file of expected.files) {
756
+ if (safeRuntimeDependencyPath(file.path) === null) {
757
+ drift = true;
758
+ continue;
759
+ }
760
+ const hash = observed.get(file.path);
761
+ if (hash === undefined)
762
+ missing = true;
763
+ else if (hash !== file.sha256)
764
+ drift = true;
765
+ else
766
+ verifiedFileCount += 1;
767
+ }
768
+ return {
769
+ ...base,
770
+ packageManifestSha256: manifestFile.sha256,
771
+ verifiedFileCount,
772
+ state: missing ? 'missing' : drift ? 'stale' : 'ready',
773
+ };
774
+ }
775
+ catch {
776
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
777
+ }
778
+ }
779
+ function resolveRuntimeDependencyPackageRoot(repositoryRoot, parentPackageRoot, dependencyName) {
780
+ for (const candidate of runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName)) {
781
+ try {
782
+ lstatSync(candidate);
783
+ return candidate;
784
+ }
785
+ catch {
786
+ // Continue through Node's parent-aware node_modules search order.
787
+ }
788
+ }
789
+ return null;
790
+ }
791
+ function runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName) {
792
+ if (!isPackageName(dependencyName))
793
+ return [];
794
+ const root = path.resolve(repositoryRoot);
795
+ let cursor = path.resolve(parentPackageRoot);
796
+ if (isOutside(root, cursor))
797
+ return [];
798
+ const candidates = [];
799
+ while (true) {
800
+ if (path.basename(cursor) !== 'node_modules') {
801
+ candidates.push(path.join(cursor, 'node_modules', ...dependencyName.split('/')));
802
+ }
803
+ if (cursor === root)
804
+ break;
805
+ const parent = path.dirname(cursor);
806
+ if (parent === cursor || isOutside(root, parent))
807
+ break;
808
+ cursor = parent;
809
+ }
810
+ return candidates;
811
+ }
812
+ function runtimeDependencyNames(manifest) {
813
+ const dependencies = manifest?.dependencies;
814
+ if (dependencies === undefined)
815
+ return [];
816
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies))
817
+ return null;
818
+ const names = Object.keys(dependencies);
819
+ return names.every(isPackageName) ? names.sort((left, right) => left.localeCompare(right)) : null;
820
+ }
821
+ function collectRuntimeDependencyFiles(packageRoot, directory = packageRoot) {
822
+ try {
823
+ const files = [];
824
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
825
+ const absolutePath = path.join(directory, entry.name);
826
+ if (entry.isSymbolicLink())
827
+ return { unsafe: true, files: [] };
828
+ if (entry.isDirectory()) {
829
+ if (entry.name === 'node_modules')
830
+ return { unsafe: true, files: [] };
831
+ const nested = collectRuntimeDependencyFiles(packageRoot, absolutePath);
832
+ if (nested.unsafe)
833
+ return nested;
834
+ files.push(...nested.files);
835
+ continue;
836
+ }
837
+ if (!entry.isFile() || entry.name === 'package.json' || !/\.(?:cjs|js|json|mjs|node)$/.test(entry.name))
838
+ continue;
839
+ const relativePath = path.relative(packageRoot, absolutePath).split(path.sep).join('/');
840
+ if (safeRuntimeDependencyPath(relativePath) === null
841
+ || !lstatSync(absolutePath).isFile()
842
+ || isOutside(realpathSync(packageRoot), realpathSync(absolutePath))) {
843
+ return { unsafe: true, files: [] };
844
+ }
845
+ files.push({ path: relativePath, sha256: sha256File(absolutePath) });
846
+ }
847
+ return { unsafe: false, files: files.sort((left, right) => left.path.localeCompare(right.path)) };
848
+ }
849
+ catch {
850
+ return { unsafe: true, files: [] };
851
+ }
852
+ }
853
+ function inspectReviewerBuild(installedPackageRoot, manifestPath, installedPackageManifestPath, cliPath, runningCliSha256, reviewerPath, runningRuntimeSha256, gatewayPath, runningGatewaySha256, workflowPath, runningWorkflowSha256) {
854
+ const manifestFile = inspectRuntimeFile(installedPackageRoot, manifestPath);
855
+ const rawManifest = readJsonRecord(manifestFile.state === 'ready' ? manifestPath : null);
856
+ const parsed = parseRuntimeBuildManifest(rawManifest);
857
+ const installedPackageManifestSha256 = inspectRuntimeFile(installedPackageRoot, installedPackageManifestPath).sha256;
858
+ let state = manifestFile.state === 'unsafe'
859
+ ? 'stale'
860
+ : parsed
861
+ ? 'ready'
862
+ : 'missing';
863
+ let verifiedFileCount = 0;
864
+ if (parsed) {
865
+ const expectedFingerprint = runtimeBuildFingerprint(parsed);
866
+ const filePaths = new Set(parsed.files.map((file) => file.path));
867
+ if (parsed.packageName !== PACKAGE_NAME
868
+ || parsed.packageVersion !== PACKAGE_VERSION
869
+ || parsed.packageManifestSha256 !== installedPackageManifestSha256
870
+ || parsed.fingerprint !== expectedFingerprint
871
+ || canonicalJsonStringify(parsed.requiredEntrypoints) !== canonicalJsonStringify([
872
+ 'dist/src/cli.js',
873
+ 'dist/src/gateway-handshake.js',
874
+ 'dist/src/reviewer.js',
875
+ 'dist/src/workflow.js',
876
+ ])
877
+ || !parsed.requiredEntrypoints.every((entry) => filePaths.has(entry))) {
878
+ state = 'stale';
879
+ }
880
+ for (const file of parsed.files) {
881
+ const safeRelative = safeRuntimeBuildPath(file.path);
882
+ if (safeRelative === null) {
883
+ state = 'stale';
884
+ continue;
885
+ }
886
+ const absolutePath = path.join(installedPackageRoot, safeRelative);
887
+ const observed = inspectRuntimeFile(installedPackageRoot, absolutePath);
888
+ if (observed.state === 'missing') {
889
+ state = 'missing';
890
+ continue;
891
+ }
892
+ if (observed.state === 'unsafe' || observed.sha256 !== file.sha256) {
893
+ state = 'stale';
894
+ continue;
895
+ }
896
+ verifiedFileCount += 1;
897
+ }
898
+ }
899
+ const cliFile = inspectRuntimeFile(installedPackageRoot, cliPath);
900
+ const reviewerFile = inspectRuntimeFile(installedPackageRoot, reviewerPath);
901
+ const gatewayFile = inspectRuntimeFile(installedPackageRoot, gatewayPath);
902
+ const workflowFile = inspectRuntimeFile(installedPackageRoot, workflowPath);
903
+ const cliSha256 = cliFile.sha256;
904
+ const reviewerSha256 = reviewerFile.sha256;
905
+ const gatewaySha256 = gatewayFile.sha256;
906
+ const workflowSha256 = workflowFile.sha256;
907
+ if (parsed && [cliFile, reviewerFile, gatewayFile, workflowFile].some((file) => file.state === 'unsafe')) {
908
+ state = 'stale';
909
+ }
910
+ else if (parsed && [cliSha256, reviewerSha256, gatewaySha256, workflowSha256].some((hash) => hash === null)) {
911
+ state = 'missing';
912
+ }
913
+ const runningCliMatchesInstalledBuild = cliSha256 !== null && cliSha256 === runningCliSha256;
914
+ const runningModuleMatchesInstalledBuild = reviewerSha256 !== null && reviewerSha256 === runningRuntimeSha256;
915
+ const runningGatewayMatchesInstalledBuild = gatewaySha256 !== null && gatewaySha256 === runningGatewaySha256;
916
+ const runningWorkflowMatchesInstalledBuild = workflowSha256 !== null && workflowSha256 === runningWorkflowSha256;
917
+ if (parsed && (!runningCliMatchesInstalledBuild
918
+ || !runningModuleMatchesInstalledBuild
919
+ || !runningGatewayMatchesInstalledBuild
920
+ || !runningWorkflowMatchesInstalledBuild))
921
+ state = 'stale';
922
+ const manifestSha256 = manifestFile.sha256;
923
+ const manifestFingerprint = parsed?.fingerprint ?? null;
924
+ const expectedFileCount = parsed?.files.length ?? 0;
925
+ return {
926
+ state,
927
+ manifestPath,
928
+ manifestSha256,
929
+ manifestFingerprint,
930
+ verifiedFileCount,
931
+ expectedFileCount,
932
+ cliEntrypointPath: cliPath,
933
+ cliEntrypointSha256: cliSha256,
934
+ runningCliMatchesInstalledBuild,
935
+ reviewerModulePath: reviewerPath,
936
+ reviewerModuleSha256: reviewerSha256,
937
+ runningModuleMatchesInstalledBuild,
938
+ gatewayModulePath: gatewayPath,
939
+ gatewayModuleSha256: gatewaySha256,
940
+ runningGatewayMatchesInstalledBuild,
941
+ workflowModulePath: workflowPath,
942
+ workflowModuleSha256: workflowSha256,
943
+ runningWorkflowMatchesInstalledBuild,
944
+ evidenceSha256: canonicalHash({
945
+ installedPackageRoot,
946
+ manifestSha256,
947
+ manifestFingerprint,
948
+ installedPackageManifestSha256,
949
+ verifiedFileCount,
950
+ expectedFileCount,
951
+ cliSha256,
952
+ runningCliSha256,
953
+ reviewerSha256,
954
+ runningRuntimeSha256,
955
+ gatewaySha256,
956
+ runningGatewaySha256,
957
+ workflowSha256,
958
+ runningWorkflowSha256,
959
+ state,
960
+ }),
961
+ };
962
+ }
963
+ function parseRuntimeBuildManifest(value) {
964
+ if (!value
965
+ || value.protocol !== 'codex-workflow-runtime-build-manifest-v1'
966
+ || typeof value.packageName !== 'string'
967
+ || typeof value.packageVersion !== 'string'
968
+ || typeof value.packageManifestSha256 !== 'string'
969
+ || !/^[a-f0-9]{64}$/.test(value.packageManifestSha256)
970
+ || !Array.isArray(value.requiredEntrypoints)
971
+ || !value.requiredEntrypoints.every((entry) => typeof entry === 'string')
972
+ || !Array.isArray(value.files)
973
+ || !Array.isArray(value.runtimeDependencies)
974
+ || typeof value.fingerprint !== 'string')
975
+ return null;
976
+ const files = parseAttestedFiles(value.files);
977
+ if (files.length !== value.files.length || new Set(files.map((entry) => entry.path)).size !== files.length)
978
+ return null;
979
+ const runtimeDependencies = value.runtimeDependencies.flatMap((entry) => {
980
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
981
+ return [];
982
+ const candidate = entry;
983
+ if (typeof candidate.name !== 'string'
984
+ || !isPackageName(candidate.name)
985
+ || typeof candidate.version !== 'string'
986
+ || typeof candidate.packageManifestSha256 !== 'string'
987
+ || !/^[a-f0-9]{64}$/.test(candidate.packageManifestSha256)
988
+ || !Array.isArray(candidate.files))
989
+ return [];
990
+ const dependencyFiles = parseAttestedFiles(candidate.files);
991
+ return dependencyFiles.length === candidate.files.length
992
+ && new Set(dependencyFiles.map((file) => file.path)).size === dependencyFiles.length
993
+ ? [{
994
+ name: candidate.name,
995
+ version: candidate.version,
996
+ packageManifestSha256: candidate.packageManifestSha256,
997
+ files: dependencyFiles,
998
+ }]
999
+ : [];
1000
+ });
1001
+ if (runtimeDependencies.length !== value.runtimeDependencies.length
1002
+ || new Set(runtimeDependencies.map((entry) => entry.name)).size !== runtimeDependencies.length)
1003
+ return null;
1004
+ return {
1005
+ protocol: value.protocol,
1006
+ packageName: value.packageName,
1007
+ packageVersion: value.packageVersion,
1008
+ packageManifestSha256: value.packageManifestSha256,
1009
+ requiredEntrypoints: [...value.requiredEntrypoints],
1010
+ files,
1011
+ runtimeDependencies,
1012
+ fingerprint: value.fingerprint,
1013
+ };
1014
+ }
1015
+ function parseAttestedFiles(value) {
1016
+ return value.flatMap((entry) => {
1017
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
1018
+ return [];
1019
+ const candidate = entry;
1020
+ return typeof candidate.path === 'string' && /^[a-f0-9]{64}$/.test(String(candidate.sha256))
1021
+ ? [{ path: candidate.path, sha256: String(candidate.sha256) }]
1022
+ : [];
1023
+ });
1024
+ }
1025
+ function runtimeBuildFingerprint(value) {
1026
+ const lines = [
1027
+ value.protocol,
1028
+ value.packageName,
1029
+ value.packageVersion,
1030
+ value.packageManifestSha256,
1031
+ ...value.requiredEntrypoints,
1032
+ ];
1033
+ for (const file of value.files)
1034
+ lines.push(file.path, file.sha256);
1035
+ for (const dependency of value.runtimeDependencies) {
1036
+ lines.push(dependency.name, dependency.version, dependency.packageManifestSha256);
1037
+ for (const file of dependency.files)
1038
+ lines.push(file.path, file.sha256);
1039
+ }
1040
+ return createHash('sha256').update(lines.join('\0')).digest('hex');
1041
+ }
1042
+ function safeRuntimeBuildPath(value) {
1043
+ const normalized = value.split('\\').join('/');
1044
+ if (!normalized.startsWith('dist/src/') || !normalized.endsWith('.js') || path.posix.normalize(normalized) !== normalized)
1045
+ return null;
1046
+ if (path.posix.isAbsolute(normalized) || normalized.split('/').includes('..'))
1047
+ return null;
1048
+ return normalized;
1049
+ }
1050
+ function safeRuntimeDependencyPath(value) {
1051
+ const normalized = value.split('\\').join('/');
1052
+ if (!/\.(?:cjs|js|json|mjs|node)$/.test(normalized)
1053
+ || path.posix.normalize(normalized) !== normalized
1054
+ || path.posix.isAbsolute(normalized)
1055
+ || normalized.split('/').includes('..')
1056
+ || normalized.split('/').includes('node_modules'))
1057
+ return null;
1058
+ return normalized;
1059
+ }
1060
+ function isPackageName(value) {
1061
+ return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
1062
+ }
1063
+ function readJsonRecord(filePath) {
1064
+ if (!filePath)
1065
+ return null;
1066
+ try {
1067
+ const value = JSON.parse(readFileSync(filePath, 'utf8'));
1068
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
1069
+ }
1070
+ catch {
1071
+ return null;
1072
+ }
1073
+ }
1074
+ function nestedString(value, keys) {
1075
+ let cursor = value;
1076
+ for (const key of keys) {
1077
+ if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor))
1078
+ return null;
1079
+ cursor = cursor[key];
1080
+ }
1081
+ return typeof cursor === 'string' ? cursor : null;
1082
+ }
1083
+ function stringField(value, key) {
1084
+ const candidate = value?.[key];
1085
+ return typeof candidate === 'string' ? candidate : null;
1086
+ }
1087
+ function isPlainDirectory(directoryPath) {
1088
+ try {
1089
+ return existsSync(directoryPath) && lstatSync(directoryPath).isDirectory();
1090
+ }
1091
+ catch {
296
1092
  return false;
297
- const required = ['id', 'severity', 'requirement', 'summary', 'evidence', 'requiredAction'];
298
- const allowed = new Set([...required, 'route', 'planConflict']);
299
- return required.every((key) => key in value)
300
- && Object.keys(value).every((key) => allowed.has(key));
1093
+ }
1094
+ }
1095
+ function isUnredirectedPathInside(root, candidate) {
1096
+ try {
1097
+ const logicalRoot = path.resolve(root);
1098
+ const logicalCandidate = path.resolve(candidate);
1099
+ if (isOutside(logicalRoot, logicalCandidate))
1100
+ return false;
1101
+ const expectedRealPath = path.join(realpathSync(logicalRoot), path.relative(logicalRoot, logicalCandidate));
1102
+ return realpathSync(logicalCandidate) === expectedRealPath;
1103
+ }
1104
+ catch {
1105
+ return false;
1106
+ }
1107
+ }
1108
+ function isPlainFileInside(root, filePath) {
1109
+ return inspectRuntimeFile(root, filePath).state === 'ready';
1110
+ }
1111
+ function inspectRuntimeFile(root, filePath) {
1112
+ try {
1113
+ if (!existsSync(filePath))
1114
+ return { state: 'missing', sha256: null };
1115
+ if (!lstatSync(filePath).isFile())
1116
+ return { state: 'unsafe', sha256: null };
1117
+ const realRoot = realpathSync(root);
1118
+ const realFile = realpathSync(filePath);
1119
+ if (isOutside(realRoot, realFile))
1120
+ return { state: 'unsafe', sha256: null };
1121
+ return { state: 'ready', sha256: sha256File(realFile) };
1122
+ }
1123
+ catch {
1124
+ return { state: 'unsafe', sha256: null };
1125
+ }
1126
+ }
1127
+ function assertReviewMode(value) {
1128
+ if (value !== 'ordinary' && value !== 'security') {
1129
+ throw new Error('review mode must be ordinary or security');
1130
+ }
1131
+ }
1132
+ function reviewInstruction(mode) {
1133
+ return mode === 'security'
1134
+ ? 'Perform the requested security-focused review within the supplied sealed scope.'
1135
+ : 'Perform an ordinary correctness and acceptance review. Do not initiate or substitute an implicit security scan; report security-relevant defects only when encountered in the supplied scope.';
1136
+ }
1137
+ function reviewPolicy(mode) {
1138
+ return { mode, implicitSecurityScan: false, securityFocusRequested: mode === 'security' };
1139
+ }
1140
+ function isOutside(root, candidate) {
1141
+ const relative = path.relative(root, candidate);
1142
+ return relative.startsWith('..') && !path.isAbsolute(relative);
1143
+ }
1144
+ function sha256File(filePath) {
1145
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
1146
+ }
1147
+ function isReviewFindingRecord(value) {
1148
+ return isExactRecord(value, [
1149
+ 'id',
1150
+ 'severity',
1151
+ 'requirement',
1152
+ 'summary',
1153
+ 'evidence',
1154
+ 'requiredAction',
1155
+ 'causeId',
1156
+ 'route',
1157
+ 'planConflict',
1158
+ ]);
301
1159
  }
302
1160
  function validatePlanConflict(value, route) {
303
1161
  if (route === 'fix') {
@@ -349,7 +1207,7 @@ function runStrictReviewerLaunch(repositoryRoot, options, runner) {
349
1207
  const temporary = mkdtempSync(path.join(os.tmpdir(), 'codex-workflow-review-'));
350
1208
  const schemaFile = path.join(temporary, 'review-result.schema.json');
351
1209
  const outputFile = path.join(temporary, 'last-message.json');
352
- writeFileSync(schemaFile, `${JSON.stringify(REVIEW_SCHEMA, null, 2)}\n`, { mode: 0o600 });
1210
+ writeFileSync(schemaFile, `${JSON.stringify(STRICT_REVIEW_OUTPUT_SCHEMA, null, 2)}\n`, { mode: 0o600 });
353
1211
  const probe = path.join(repositoryRoot, '.codex-reviewer-write-probe');
354
1212
  if (existsSync(probe)) {
355
1213
  rmSync(temporary, { recursive: true, force: true });
@@ -449,13 +1307,58 @@ function sealsEqual(left, right) {
449
1307
  }
450
1308
  function parseStrictReview(value) {
451
1309
  const parsed = JSON.parse(value);
452
- if (!['passed', 'failed', 'unverified'].includes(parsed.status ?? ''))
1310
+ if (!isExactRecord(parsed, ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'])) {
1311
+ throw new Error('invalid review');
1312
+ }
1313
+ if (!['passed', 'failed', 'unverified'].includes(String(parsed.status)))
453
1314
  throw new Error('invalid status');
454
- if (!parsed.reviewer?.trim() || !parsed.summary?.trim() || !Array.isArray(parsed.findings))
1315
+ if (typeof parsed.reviewer !== 'string' || !parsed.reviewer.trim()
1316
+ || typeof parsed.summary !== 'string' || !parsed.summary.trim()
1317
+ || !Array.isArray(parsed.findings))
455
1318
  throw new Error('invalid review');
456
- if (!['denied', 'written', 'not-attempted'].includes(parsed.isolationProbe ?? ''))
1319
+ if (!['denied', 'written', 'not-attempted'].includes(String(parsed.isolationProbe)))
457
1320
  throw new Error('invalid probe');
458
- return parsed;
1321
+ const findings = parsed.findings.map((candidate) => {
1322
+ if (!isReviewFindingRecord(candidate)
1323
+ || typeof candidate.id !== 'string' || !candidate.id.trim()
1324
+ || !['critical', 'major', 'minor'].includes(String(candidate.severity))
1325
+ || (candidate.requirement !== null && typeof candidate.requirement !== 'string')
1326
+ || typeof candidate.summary !== 'string' || !candidate.summary.trim()
1327
+ || typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
1328
+ || typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
1329
+ throw new Error('invalid review finding');
1330
+ }
1331
+ if (candidate.route !== 'fix' && candidate.route !== 'replan')
1332
+ throw new Error('invalid review finding route');
1333
+ const route = candidate.route;
1334
+ const causeId = candidate.causeId;
1335
+ if (causeId !== null
1336
+ && (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
1337
+ throw new Error('invalid review finding causeId');
1338
+ }
1339
+ const planConflict = validatePlanConflict(candidate.planConflict, route);
1340
+ return {
1341
+ id: candidate.id,
1342
+ severity: candidate.severity,
1343
+ requirement: candidate.requirement,
1344
+ summary: candidate.summary,
1345
+ evidence: candidate.evidence,
1346
+ requiredAction: candidate.requiredAction,
1347
+ ...(causeId === null ? {} : { causeId }),
1348
+ route,
1349
+ planConflict,
1350
+ };
1351
+ });
1352
+ if ((parsed.status === 'passed' && findings.length > 0)
1353
+ || (parsed.status === 'failed' && findings.length === 0))
1354
+ throw new Error('invalid finding cardinality');
1355
+ return {
1356
+ status: parsed.status,
1357
+ reviewer: parsed.reviewer,
1358
+ summary: parsed.summary,
1359
+ findings,
1360
+ isolationProbe: parsed.isolationProbe,
1361
+ };
459
1362
  }
460
1363
  function parseStrictReviewWithHash(value) {
461
1364
  const output = parseStrictReview(value);
@@ -499,5 +1402,4 @@ function failedStepLaunch(repositoryRoot, command, processStatus, summary, revie
499
1402
  reviewerOutputHash: hashReviewInput(base.review),
500
1403
  };
501
1404
  }
502
- export { REVIEW_SCHEMA };
503
1405
  //# sourceMappingURL=reviewer.js.map