@open-mercato/cli 0.6.7-develop.6677.1.beabb7ca12 → 0.6.7-develop.6686.1.bdda5ed397

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 (211) hide show
  1. package/.turbo/turbo-build.log +0 -1
  2. package/build.mjs +21 -5
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  4. package/dist/agentic/codex/enforcement-rules.md +2 -16
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  9. package/dist/agentic/guides/ai-workflows.md +76 -0
  10. package/dist/agentic/guides/architecture.md +107 -0
  11. package/dist/agentic/guides/backend-ui.md +80 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +104 -0
  14. package/dist/agentic/guides/integrations.md +81 -0
  15. package/dist/agentic/guides/module-facts.json +217 -81
  16. package/dist/agentic/guides/module-system.md +12 -127
  17. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  18. package/dist/agentic/guides/modules/api_docs.md +1 -1
  19. package/dist/agentic/guides/modules/api_keys.md +1 -1
  20. package/dist/agentic/guides/modules/attachments.md +2 -2
  21. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  22. package/dist/agentic/guides/modules/auth.md +1 -1
  23. package/dist/agentic/guides/modules/business_rules.md +1 -1
  24. package/dist/agentic/guides/modules/catalog.md +5 -5
  25. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  26. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  27. package/dist/agentic/guides/modules/checkout.md +1 -1
  28. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  29. package/dist/agentic/guides/modules/configs.md +1 -1
  30. package/dist/agentic/guides/modules/content.md +1 -1
  31. package/dist/agentic/guides/modules/currencies.md +1 -1
  32. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  33. package/dist/agentic/guides/modules/customers.md +2 -2
  34. package/dist/agentic/guides/modules/dashboards.md +1 -1
  35. package/dist/agentic/guides/modules/data_sync.md +1 -1
  36. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  37. package/dist/agentic/guides/modules/directory.md +1 -1
  38. package/dist/agentic/guides/modules/entities.md +2 -2
  39. package/dist/agentic/guides/modules/events.md +1 -1
  40. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  41. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  42. package/dist/agentic/guides/modules/generators.md +1 -1
  43. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  44. package/dist/agentic/guides/modules/integrations.md +1 -1
  45. package/dist/agentic/guides/modules/messages.md +1 -1
  46. package/dist/agentic/guides/modules/notifications.md +1 -1
  47. package/dist/agentic/guides/modules/onboarding.md +1 -1
  48. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  49. package/dist/agentic/guides/modules/perspectives.md +1 -1
  50. package/dist/agentic/guides/modules/planner.md +1 -1
  51. package/dist/agentic/guides/modules/portal.md +1 -1
  52. package/dist/agentic/guides/modules/progress.md +1 -1
  53. package/dist/agentic/guides/modules/query_index.md +2 -2
  54. package/dist/agentic/guides/modules/record_locks.md +1 -1
  55. package/dist/agentic/guides/modules/resources.md +1 -1
  56. package/dist/agentic/guides/modules/sales.md +17 -17
  57. package/dist/agentic/guides/modules/scheduler.md +1 -1
  58. package/dist/agentic/guides/modules/search.md +1 -1
  59. package/dist/agentic/guides/modules/security.md +1 -1
  60. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  61. package/dist/agentic/guides/modules/sso.md +1 -1
  62. package/dist/agentic/guides/modules/staff.md +2 -2
  63. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  64. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  65. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  66. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  67. package/dist/agentic/guides/modules/translations.md +1 -1
  68. package/dist/agentic/guides/modules/webhooks.md +2 -2
  69. package/dist/agentic/guides/modules/wms.md +2 -2
  70. package/dist/agentic/guides/modules/workflows.md +1 -1
  71. package/dist/agentic/guides/testing-debugging.md +74 -0
  72. package/dist/agentic/guides/upstream/AGENTS.md +297 -0
  73. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +303 -0
  74. package/dist/agentic/guides/upstream/manifest.json +8 -0
  75. package/dist/agentic/shared/AGENTS.md.template +123 -311
  76. package/dist/agentic/shared/ai/agentic.config.json +1 -0
  77. package/dist/agentic/shared/ai/harness/README.md +48 -0
  78. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  79. package/dist/agentic/shared/ai/harness/cases.json +13487 -0
  80. package/dist/agentic/shared/ai/harness/cases.schema.json +132 -0
  81. package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
  82. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
  83. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +20 -0
  84. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +43 -0
  85. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +110 -0
  86. package/dist/agentic/shared/ai/harness/release-matrix.json +133 -0
  87. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  88. package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
  89. package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
  90. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  91. package/dist/agentic/shared/ai/harness/validators.json +78 -0
  92. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +1768 -0
  93. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  94. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  95. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  96. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
  97. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
  98. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  99. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
  100. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  101. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  102. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  103. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  104. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  105. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  106. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  107. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  108. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  109. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  110. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
  111. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
  112. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
  113. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
  114. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  115. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
  116. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
  117. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +28 -0
  118. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +89 -0
  119. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  120. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  121. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  122. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  123. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  124. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  125. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +17 -0
  126. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  127. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +15 -156
  128. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +12 -0
  129. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
  130. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  131. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
  132. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  133. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  134. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  135. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
  136. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +107 -0
  137. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
  138. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  139. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
  140. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  141. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  142. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  143. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
  144. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
  145. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +25 -0
  146. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  147. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +38 -0
  148. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  149. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  150. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  151. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
  152. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
  153. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  154. package/dist/agentic/shared/ai/skills/tiers.json +137 -20
  155. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  156. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +174 -0
  157. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2783 -0
  158. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  159. package/dist/agentic/shared/scripts/framework-context.mjs +894 -0
  160. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  161. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  162. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  163. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1761 -0
  164. package/dist/lib/__integration__/TC-INT-008.spec.js +87 -8
  165. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  166. package/dist/lib/agentic-init.js +13 -3
  167. package/dist/lib/agentic-init.js.map +2 -2
  168. package/dist/lib/agentic-setup.js +418 -186
  169. package/dist/lib/agentic-setup.js.map +2 -2
  170. package/dist/lib/generators/module-facts-discovery.js +41 -7
  171. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  172. package/dist/lib/generators/module-facts.js +65 -17
  173. package/dist/lib/generators/module-facts.js.map +2 -2
  174. package/dist/lib/generators/module-registry.js +5 -4
  175. package/dist/lib/generators/module-registry.js.map +2 -2
  176. package/dist/lib/resolver.js +2 -0
  177. package/dist/lib/resolver.js.map +2 -2
  178. package/package.json +5 -5
  179. package/src/lib/__integration__/TC-INT-008.spec.ts +103 -8
  180. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  181. package/src/lib/__tests__/agentic-setup.ownership.test.ts +380 -0
  182. package/src/lib/agentic-init.ts +15 -3
  183. package/src/lib/agentic-setup.ts +526 -224
  184. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +6 -0
  185. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  186. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  187. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  188. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  189. package/src/lib/generators/module-facts-discovery.ts +58 -7
  190. package/src/lib/generators/module-facts.ts +84 -20
  191. package/src/lib/generators/module-registry.ts +8 -4
  192. package/src/lib/resolver.ts +3 -0
  193. package/dist/agentic/guides/cache.md +0 -45
  194. package/dist/agentic/guides/core.md +0 -453
  195. package/dist/agentic/guides/events.md +0 -117
  196. package/dist/agentic/guides/queue.md +0 -53
  197. package/dist/agentic/guides/search.md +0 -115
  198. package/dist/agentic/guides/shared.md +0 -108
  199. package/dist/agentic/guides/ui.md +0 -347
  200. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  201. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  202. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  203. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  204. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  205. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  206. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  207. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  208. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  209. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  210. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  211. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,1761 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import { createHash } from 'node:crypto'
5
+ import fs from 'node:fs'
6
+ import { createRequire } from 'node:module'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import process from 'node:process'
10
+ import { pathToFileURL } from 'node:url'
11
+ import { assertSandboxRuntimeAvailable, sandboxedInvocation, verifyLinuxSandboxIsolation } from './execution-sandbox.mjs'
12
+
13
+ const WRITABLE_KINDS = new Set(['implementation', 'regression'])
14
+ const RELEASE_SUPPORTED_RUNNERS = ['codex', 'claude']
15
+ const RELEASE_VALIDATION_COMMANDS = ['yarn generate', 'yarn typecheck', 'yarn lint', 'yarn build']
16
+ const RELEASE_VALIDATION_OUTPUT_ROOTS = new Map([
17
+ ['yarn generate', ['.mercato/generated']],
18
+ ['yarn typecheck', ['tsconfig.tsbuildinfo']],
19
+ ['yarn lint', []],
20
+ ['yarn build', ['.mercato/generated', '.mercato/next', 'next-env.d.ts', 'tsconfig.tsbuildinfo']],
21
+ ])
22
+ const RUNNERS = new Set(RELEASE_SUPPORTED_RUNNERS)
23
+ const ALLOWED_VALIDATION_COMMANDS = new Set(RELEASE_VALIDATION_COMMANDS)
24
+ const GENERATED_TEST_RUNNERS = new Set(['jest', 'playwright-api', 'playwright-browser'])
25
+ const RESULT_LIMIT = 262_144
26
+ const ERROR_LIMIT = 2_000
27
+ const VIOLATION_LIMIT = 300
28
+ const COPY_EXCLUDED_PREFIXES = [
29
+ '.git', '.next', '.turbo', '.cache', 'build', 'coverage', 'dist', 'node_modules', 'out',
30
+ '.ai/framework-context', '.ai/harness/results', '.ai/reports',
31
+ '.mercato/generated', '.mercato/next', 'next-env.d.ts', 'tsconfig.tsbuildinfo',
32
+ ]
33
+ const SAFE_ENV_TEMPLATES = new Set(['.env.example', '.env.sample', '.env.template'])
34
+ const SENSITIVE_AUTH_FILES = new Set(['.git-credentials', '.netrc', '.npmrc', '.pypirc', '.yarnrc', '.yarnrc.yml', '.yarnrc.yaml'])
35
+ const SENSITIVE_AUTH_DATA_FILE = /^(?:auth|credentials?|secrets?|tokens?)(?:\.(?:conf(?:ig)?|ini|json|toml|txt|ya?ml))?$/
36
+ const SENSITIVE_ENV_KEY = /(?:^|_)(?:api_?key|auth|credential|credentials|password|passwd|private_?key|secret|token)(?:_|$)/i
37
+ const GENERATED_YARN_CONFIG_PATH = '.yarnrc.yml'
38
+ const GENERATED_YARN_CONFIG_LIMIT = 16_384
39
+
40
+ function usage() {
41
+ return `Run the complete standalone agent-harness release gate.
42
+
43
+ Usage:
44
+ yarn harness:release --runner <codex|claude> --prepare-targets /absolute/empty-directory --acknowledge-writes
45
+ yarn harness:release --runner <codex|claude> --writable-targets /absolute/release-targets.json --acknowledge-writes
46
+
47
+ Options:
48
+ --root <absolute-app> Controller/generated app root (default: current directory)
49
+ --runner <codex|claude> Required primary runner for every blocking live lane
50
+ --portability-runner <runner> Optional different runner for the 45-case read-only portability lane
51
+ --prepare-targets <absolute> Clone this fresh scaffold once per writable case under an empty/new directory
52
+ --writable-targets <absolute> JSON map of every writable case to a fresh disposable app
53
+ --case-timeout <ms> Per-model invocation timeout floor (default: 120000; a writable case may raise it)
54
+ --validation-timeout <ms> Timeout for each yarn validation (default: 1800000)
55
+ --acknowledge-writes Required: fixture preparation and validation commands write files
56
+ --help Show this help
57
+
58
+ Exactly one target option is required. Automatic targets require a fresh controller
59
+ without local environment, credential, or private-key files; safe .env.example/sample/template
60
+ files are retained. Targets exclude dependencies, build outputs, harness results, and
61
+ generated framework context, then safely reference the controller's protected dependency
62
+ tree. Externally supplied targets must use the same
63
+ node_modules link to that protected tree; nested dependency copies are rejected.
64
+ The complete release requires trusted Bubblewrap plus working user/network namespaces on Linux;
65
+ native macOS, Windows, untrusted runtimes, and unavailable isolation fail closed. The command preflights complete matrix,
66
+ fixture, review, and target coverage before
67
+ running deterministic validation, selected primary routing, optional portability routing, writable trusted-oracle
68
+ gates, explicit om-code-review, and the release-matrix validation commands. It stores
69
+ only a sanitized aggregate report under .ai/harness/results/.`
70
+ }
71
+
72
+ function parseArgs(argv) {
73
+ const options = {
74
+ root: process.cwd(),
75
+ runner: undefined,
76
+ portabilityRunner: undefined,
77
+ prepareTargets: undefined,
78
+ writableTargets: undefined,
79
+ caseTimeout: 120_000,
80
+ validationTimeout: 1_800_000,
81
+ acknowledgeWrites: false,
82
+ help: false,
83
+ }
84
+ for (let index = 0; index < argv.length; index += 1) {
85
+ const arg = argv[index]
86
+ const value = () => {
87
+ index += 1
88
+ if (index >= argv.length) throw new Error(`${arg} requires a value`)
89
+ return argv[index]
90
+ }
91
+ if (arg === '--help' || arg === '-h') options.help = true
92
+ else if (arg === '--root') options.root = value()
93
+ else if (arg === '--runner') options.runner = value()
94
+ else if (arg === '--portability-runner') options.portabilityRunner = value()
95
+ else if (arg === '--prepare-targets') options.prepareTargets = value()
96
+ else if (arg === '--writable-targets') options.writableTargets = value()
97
+ else if (arg === '--case-timeout') options.caseTimeout = Number(value())
98
+ else if (arg === '--validation-timeout') options.validationTimeout = Number(value())
99
+ else if (arg === '--acknowledge-writes') options.acknowledgeWrites = true
100
+ else throw new Error(`unknown argument: ${arg}`)
101
+ }
102
+ if (options.help) return options
103
+ if (!path.isAbsolute(options.root)) throw new Error('--root must be an absolute path')
104
+ if (!RUNNERS.has(options.runner)) throw new Error('--runner must be codex or claude')
105
+ if (options.portabilityRunner !== undefined && !RUNNERS.has(options.portabilityRunner)) throw new Error('--portability-runner must be codex or claude')
106
+ if (options.portabilityRunner === options.runner) throw new Error('--portability-runner must differ from --runner')
107
+ if (Boolean(options.prepareTargets) === Boolean(options.writableTargets)) throw new Error('pass exactly one of --prepare-targets or --writable-targets')
108
+ if (options.prepareTargets && !path.isAbsolute(options.prepareTargets)) throw new Error('--prepare-targets must be an absolute path')
109
+ if (options.writableTargets && !path.isAbsolute(options.writableTargets)) throw new Error('--writable-targets must be an absolute path')
110
+ if (!options.acknowledgeWrites) throw new Error('the full release gate requires --acknowledge-writes')
111
+ if (!Number.isInteger(options.caseTimeout) || options.caseTimeout < 1_000 || options.caseTimeout > 3_600_000) throw new Error('--case-timeout must be from 1000 to 3600000 milliseconds')
112
+ if (!Number.isInteger(options.validationTimeout) || options.validationTimeout < 1_000 || options.validationTimeout > 7_200_000) throw new Error('--validation-timeout must be from 1000 to 7200000 milliseconds')
113
+ return options
114
+ }
115
+
116
+ export function effectiveCaseTimeout(cases, caseId, fallback) {
117
+ const declared = cases.find((item) => item.id === caseId)?.timeoutMs
118
+ return Math.max(fallback, Number.isInteger(declared) ? declared : 0)
119
+ }
120
+
121
+ function readJson(file) {
122
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
123
+ }
124
+
125
+ function sha256(value) {
126
+ return createHash('sha256').update(value).digest('hex')
127
+ }
128
+
129
+ function isPathInside(parent, child) {
130
+ const relative = path.relative(parent, child)
131
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
132
+ }
133
+
134
+ function validModelSelector(value) {
135
+ return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,99}$/.test(value)
136
+ }
137
+
138
+ function normalizedRelative(root, absolute) {
139
+ return path.relative(root, absolute).replaceAll(path.sep, '/')
140
+ }
141
+
142
+ function copyPathExcluded(relative) {
143
+ return COPY_EXCLUDED_PREFIXES.some((prefix) => relative === prefix || relative.startsWith(`${prefix}/`))
144
+ }
145
+
146
+ function sensitiveScaffoldPath(relative, stat) {
147
+ const basename = path.posix.basename(relative).toLowerCase()
148
+ const lowerRelative = relative.toLowerCase()
149
+ if ((basename === '.env' || basename.startsWith('.env.')) && !SAFE_ENV_TEMPLATES.has(basename)) return true
150
+ // A fresh create-mercato-app scaffold always contains this generated root file.
151
+ // Its contents are checked separately against the exact credential-free shape.
152
+ if (lowerRelative === GENERATED_YARN_CONFIG_PATH) return relative !== GENERATED_YARN_CONFIG_PATH
153
+ if (SENSITIVE_AUTH_FILES.has(basename)) return true
154
+ if (/^(?:\.aws|\.azure|\.config\/gcloud|\.config\/gh|\.docker|\.gnupg|\.kube|\.oci|\.pulumi|\.ssh|\.terraform\.d)(?:\/|$)/.test(lowerRelative)) return true
155
+ if (stat?.isDirectory()) return false
156
+ if (SENSITIVE_AUTH_DATA_FILE.test(basename)) return true
157
+ if (/\.(?:key|pem|p12|pfx|jks|keystore)$/.test(basename)) return true
158
+ if (/^(?:id_rsa|id_dsa|id_ecdsa|id_ed25519)$/.test(basename)) return true
159
+ return /(?:^|[._-])(?:credential|credentials|private[._-]?key|service[._-]?account)(?:[._-]|$)/.test(basename)
160
+ }
161
+
162
+ function parseGeneratedQuotedValue(line, prefix) {
163
+ if (!line.startsWith(prefix)) return undefined
164
+ try {
165
+ const serialized = line.slice(prefix.length)
166
+ const value = JSON.parse(serialized)
167
+ return typeof value === 'string' && value.length > 0 && JSON.stringify(value) === serialized ? value : undefined
168
+ } catch {
169
+ return undefined
170
+ }
171
+ }
172
+
173
+ function validGeneratedYarnConfig(contents) {
174
+ if (Buffer.byteLength(contents, 'utf8') > GENERATED_YARN_CONFIG_LIMIT || contents.includes('\0')) return false
175
+ const lines = contents.replaceAll('\r\n', '\n').split('\n').filter((line) => line !== '')
176
+ const migrated = lines[0] === 'approvedGitRepositories:'
177
+ if (migrated) {
178
+ if (
179
+ lines.shift() !== 'approvedGitRepositories:'
180
+ || lines.shift() !== ' - "**"'
181
+ || lines.shift() !== 'enableScripts: true'
182
+ ) return false
183
+ }
184
+ if (lines.shift() !== 'nodeLinker: node-modules') return false
185
+ if (migrated && lines.shift() !== 'npmMinimalAgeGate: 0') return false
186
+ if (!lines.length) return true
187
+
188
+ const unsafeHttpHosts = []
189
+ const readUnsafeHttpHosts = () => {
190
+ if (lines[0] !== 'unsafeHttpWhitelist:') return true
191
+ lines.shift()
192
+ while (lines[0]?.startsWith(' - ')) {
193
+ const host = parseGeneratedQuotedValue(lines.shift(), ' - ')
194
+ if (!host) return false
195
+ unsafeHttpHosts.push(host)
196
+ }
197
+ return unsafeHttpHosts.length > 0
198
+ }
199
+ if (!migrated && !readUnsafeHttpHosts()) return false
200
+
201
+ if (lines.shift() !== 'npmScopes:' || lines.shift() !== ' open-mercato:') return false
202
+ if (migrated && lines.shift() !== ' npmMinimalAgeGate: 0') return false
203
+ const registryUrl = parseGeneratedQuotedValue(lines.shift() ?? '', ' npmRegistryServer: ')
204
+ if (!registryUrl || (!migrated && lines.shift() !== ' npmMinimalAgeGate: 0')) return false
205
+ if (migrated && !readUnsafeHttpHosts()) return false
206
+ if (lines.length) return false
207
+
208
+ let parsedRegistryUrl
209
+ try {
210
+ parsedRegistryUrl = new URL(registryUrl)
211
+ } catch {
212
+ return false
213
+ }
214
+ if (!['http:', 'https:'].includes(parsedRegistryUrl.protocol)) return false
215
+ if (parsedRegistryUrl.username || parsedRegistryUrl.password || parsedRegistryUrl.search || parsedRegistryUrl.hash) return false
216
+
217
+ const expectedUnsafeHosts = parsedRegistryUrl.protocol === 'http:' ? [parsedRegistryUrl.hostname] : []
218
+ if (parsedRegistryUrl.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(parsedRegistryUrl.hostname)) {
219
+ expectedUnsafeHosts.push('host.docker.internal')
220
+ }
221
+ return JSON.stringify(unsafeHttpHosts) === JSON.stringify(expectedUnsafeHosts)
222
+ }
223
+
224
+ function validateGeneratedYarnConfig(source, relative, stat) {
225
+ if (relative !== GENERATED_YARN_CONFIG_PATH) return
226
+ if (!stat.isFile() || stat.isSymbolicLink() || !validGeneratedYarnConfig(fs.readFileSync(source, 'utf8'))) {
227
+ throw new Error('fresh controller contains an unsafe Yarn registry configuration; use a credential-free fresh scaffold')
228
+ }
229
+ }
230
+
231
+ function validateScaffoldCopySource(root) {
232
+ const realRoot = fs.realpathSync(root)
233
+ const visit = (directory) => {
234
+ for (const name of fs.readdirSync(directory).sort()) {
235
+ const source = path.join(directory, name)
236
+ const relative = normalizedRelative(realRoot, source)
237
+ if (copyPathExcluded(relative)) continue
238
+ const stat = fs.lstatSync(source)
239
+ if (sensitiveScaffoldPath(relative, stat)) {
240
+ throw new Error('fresh controller contains a local environment, credential, or private-key file; use a sanitized fresh scaffold')
241
+ }
242
+ validateGeneratedYarnConfig(source, relative, stat)
243
+ if (stat.isSymbolicLink()) {
244
+ const resolved = fs.realpathSync(source)
245
+ const resolvedRelative = normalizedRelative(realRoot, resolved)
246
+ if (!isPathInside(realRoot, resolved) || copyPathExcluded(resolvedRelative)) {
247
+ throw new Error(`fresh scaffold contains an unsafe copied symlink: ${relative}`)
248
+ }
249
+ } else if (stat.isDirectory()) visit(source)
250
+ else if (!stat.isFile()) throw new Error(`fresh scaffold contains an unsupported entry: ${relative}`)
251
+ }
252
+ }
253
+ visit(realRoot)
254
+ }
255
+
256
+ function copyFreshScaffold(sourceRoot, targetRoot) {
257
+ const realSourceRoot = fs.realpathSync(sourceRoot)
258
+ const visit = (sourceDirectory, targetDirectory) => {
259
+ fs.mkdirSync(targetDirectory, { recursive: true, mode: 0o755 })
260
+ for (const name of fs.readdirSync(sourceDirectory).sort()) {
261
+ const source = path.join(sourceDirectory, name)
262
+ const relative = normalizedRelative(realSourceRoot, source)
263
+ if (copyPathExcluded(relative)) continue
264
+ const stat = fs.lstatSync(source)
265
+ // Fail closed if a sensitive file appears between validation and copy.
266
+ if (sensitiveScaffoldPath(relative, stat)) {
267
+ throw new Error('fresh controller contains a local environment, credential, or private-key file; use a sanitized fresh scaffold')
268
+ }
269
+ const destination = path.join(targetDirectory, name)
270
+ validateGeneratedYarnConfig(source, relative, stat)
271
+ if (stat.isDirectory()) visit(source, destination)
272
+ else if (stat.isFile()) {
273
+ fs.copyFileSync(source, destination, fs.constants.COPYFILE_FICLONE)
274
+ fs.chmodSync(destination, stat.mode & 0o777)
275
+ } else if (stat.isSymbolicLink()) {
276
+ const resolved = fs.realpathSync(source)
277
+ const resolvedRelative = normalizedRelative(realSourceRoot, resolved)
278
+ const target = path.join(targetRoot, resolvedRelative)
279
+ const type = fs.statSync(resolved).isDirectory() ? (process.platform === 'win32' ? 'junction' : 'dir') : 'file'
280
+ fs.symlinkSync(path.relative(path.dirname(destination), target), destination, type)
281
+ }
282
+ }
283
+ }
284
+ visit(realSourceRoot, targetRoot)
285
+ }
286
+
287
+ export function resolvePreparationRoot(requested, controllerRoot) {
288
+ if (!path.isAbsolute(requested)) throw new Error('--prepare-targets must be an absolute path')
289
+ const resolved = path.resolve(requested)
290
+ if (resolved === path.parse(resolved).root) throw new Error('--prepare-targets cannot be a filesystem root')
291
+ let actual
292
+ if (fs.existsSync(resolved)) {
293
+ const stat = fs.lstatSync(resolved)
294
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('--prepare-targets must be a regular directory')
295
+ if (fs.readdirSync(resolved).length) throw new Error('--prepare-targets must be empty')
296
+ actual = fs.realpathSync(resolved)
297
+ } else {
298
+ const parent = path.dirname(resolved)
299
+ if (!fs.existsSync(parent)) throw new Error('--prepare-targets parent directory must already exist')
300
+ const stat = fs.lstatSync(parent)
301
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('--prepare-targets parent must be a regular directory')
302
+ actual = path.join(fs.realpathSync(parent), path.basename(resolved))
303
+ }
304
+ const controller = fs.realpathSync(controllerRoot)
305
+ if (isPathInside(controller, actual) || isPathInside(actual, controller)) {
306
+ throw new Error('--prepare-targets must be separate from the controller app')
307
+ }
308
+ return actual
309
+ }
310
+
311
+ export function prepareWritableTargets({ root, prepareRoot, caseIds }) {
312
+ const controller = fs.realpathSync(root)
313
+ if (fs.existsSync(path.join(controller, '.ai', 'harness', 'DISPOSABLE'))) {
314
+ throw new Error('automatic target preparation requires an unmodified fresh controller scaffold')
315
+ }
316
+ const targetRoot = resolvePreparationRoot(prepareRoot, controller)
317
+ if (!Array.isArray(caseIds) || new Set(caseIds).size !== caseIds.length || caseIds.some((id) => !/^OMH-[0-9]{3}$/.test(id))) {
318
+ throw new Error('automatic writable target case IDs are invalid or duplicated')
319
+ }
320
+ const dependencyRoot = path.join(controller, 'node_modules')
321
+ let dependencyStat
322
+ try { dependencyStat = fs.lstatSync(dependencyRoot) } catch { throw new Error('controller node_modules is unavailable; run yarn install') }
323
+ if (!dependencyStat.isDirectory() || dependencyStat.isSymbolicLink()) throw new Error('controller node_modules must be a regular installed dependency directory')
324
+ validateScaffoldCopySource(controller)
325
+ if (!fs.existsSync(targetRoot)) fs.mkdirSync(targetRoot, { mode: 0o700 })
326
+ const targetRootStat = fs.lstatSync(targetRoot)
327
+ if (!targetRootStat.isDirectory() || targetRootStat.isSymbolicLink() || fs.readdirSync(targetRoot).length) {
328
+ throw new Error('automatic writable target root stopped being an empty regular directory')
329
+ }
330
+ const targets = {}
331
+ for (const caseId of caseIds) {
332
+ const target = path.join(targetRoot, caseId)
333
+ fs.mkdirSync(target, { mode: 0o700 })
334
+ copyFreshScaffold(controller, target)
335
+ fs.symlinkSync(dependencyRoot, path.join(target, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
336
+ targets[caseId] = fs.realpathSync(target)
337
+ }
338
+ const manifest = { schemaVersion: 1, targets }
339
+ fs.writeFileSync(path.join(targetRoot, 'release-targets.json'), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 })
340
+ return manifest
341
+ }
342
+
343
+ function sortedUnique(values) {
344
+ return [...new Set(values)].sort()
345
+ }
346
+
347
+ function difference(left, right) {
348
+ const rightSet = new Set(right)
349
+ return sortedUnique(left.filter((value) => !rightSet.has(value)))
350
+ }
351
+
352
+ function duplicates(values) {
353
+ const seen = new Set()
354
+ const repeated = new Set()
355
+ for (const value of values) {
356
+ if (seen.has(value)) repeated.add(value)
357
+ seen.add(value)
358
+ }
359
+ return [...repeated].sort()
360
+ }
361
+
362
+ function resolveCaseSelector(selector, allCaseIds) {
363
+ if (selector === 'all') return [...allCaseIds]
364
+ return Array.isArray(selector) ? [...selector] : []
365
+ }
366
+
367
+ function addCoverageViolation(violations, label, ids) {
368
+ if (ids.length) violations.push(`${label}: ${ids.join(', ')}`)
369
+ }
370
+
371
+ function validTargetsManifest(targetsManifest) {
372
+ return targetsManifest?.schemaVersion === 1 && targetsManifest?.targets && typeof targetsManifest.targets === 'object' && !Array.isArray(targetsManifest.targets)
373
+ }
374
+
375
+ export function generatedTestExecutionContract(entry) {
376
+ const cliPackage = entry.runner === 'jest' ? 'jest' : '@playwright/test'
377
+ const argv = entry.runner === 'jest'
378
+ ? ['<resolved-cli>', '--config', 'jest.config.cjs', '--runInBand', '--runTestsByPath', entry.artifact, '--cacheDirectory', '<isolated-temp>/jest-cache', '--json', '--outputFile', '<isolated-temp>/jest-results.json']
379
+ : ['<resolved-cli>', 'test', '--config', '.ai/qa/tests/playwright.config.ts', entry.artifact, '--retries=0', '--workers=1', '--forbid-only', '--reporter=json', '--output', '<isolated-temp>/playwright-output', '--update-snapshots=none']
380
+ return { executor: 'node', cliPackage, argv, command: ['node', ...argv].join(' ') }
381
+ }
382
+
383
+ function readGeneratedTestReport(file, tempRoot, label) {
384
+ const stat = fs.lstatSync(file)
385
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 2 || stat.size > 8 * 1024 * 1024
386
+ || !isPathInside(fs.realpathSync(tempRoot), fs.realpathSync(file))) {
387
+ throw new Error(`${label} report must be a bounded regular file in the isolated output root`)
388
+ }
389
+ return readJson(file)
390
+ }
391
+
392
+ function collectPlaywrightTests(suites, output = []) {
393
+ for (const suite of Array.isArray(suites) ? suites : []) {
394
+ for (const spec of Array.isArray(suite?.specs) ? suite.specs : []) {
395
+ for (const test of Array.isArray(spec?.tests) ? spec.tests : []) output.push(test)
396
+ }
397
+ collectPlaywrightTests(suite?.suites, output)
398
+ }
399
+ return output
400
+ }
401
+
402
+ function generatedTestCounts(testRunner, tempRoot) {
403
+ if (testRunner === 'jest') {
404
+ const report = readGeneratedTestReport(path.join(tempRoot, 'jest-results.json'), tempRoot, 'Jest')
405
+ const counts = {
406
+ passedTests: Number(report.numPassedTests ?? 0),
407
+ skippedTests: Number(report.numPendingTests ?? 0),
408
+ todoTests: Number(report.numTodoTests ?? 0),
409
+ expectedFailureTests: 0,
410
+ }
411
+ const assertions = (Array.isArray(report.testResults) ? report.testResults : [])
412
+ .flatMap((suite) => Array.isArray(suite?.assertionResults) ? suite.assertionResults : [])
413
+ if (report.success !== true || Number(report.numFailedTests ?? 0) !== 0 || counts.passedTests < 1
414
+ || counts.skippedTests !== 0 || counts.todoTests !== 0
415
+ || assertions.some((assertion) => assertion?.status !== 'passed')) {
416
+ throw new Error('generated Jest report must contain at least one passing test and zero failed, skipped, todo, or expected-failure tests')
417
+ }
418
+ return counts
419
+ }
420
+ const report = readGeneratedTestReport(path.join(tempRoot, 'playwright-results.json'), tempRoot, 'Playwright')
421
+ const tests = collectPlaywrightTests(report.suites)
422
+ const skippedTests = tests.filter((test) => test?.expectedStatus === 'skipped' || test?.status === 'skipped').length
423
+ const expectedFailureTests = tests.filter((test) => !['passed', 'skipped'].includes(test?.expectedStatus)).length
424
+ const passedTests = tests.filter((test) => test?.expectedStatus === 'passed' && test?.status === 'expected'
425
+ && Array.isArray(test.results) && test.results.length > 0 && test.results.every((result) => result?.status === 'passed')).length
426
+ const counts = { passedTests, skippedTests, todoTests: 0, expectedFailureTests }
427
+ if (tests.length < 1 || passedTests !== tests.length || skippedTests !== 0 || expectedFailureTests !== 0
428
+ || Number(report?.stats?.skipped ?? 0) !== 0 || Number(report?.stats?.unexpected ?? 0) !== 0
429
+ || Number(report?.stats?.flaky ?? 0) !== 0 || (Array.isArray(report?.errors) && report.errors.length > 0)) {
430
+ throw new Error('generated Playwright report must contain at least one passing test and zero skipped, focused, flaky, unexpected, or expected-failure tests')
431
+ }
432
+ return counts
433
+ }
434
+
435
+ function resolveTargetTestCli(target, runner) {
436
+ const dependencyRoot = fs.realpathSync(path.join(target, 'node_modules'))
437
+ const requireFromTarget = createRequire(path.join(target, 'package.json'))
438
+ const cli = runner === 'jest'
439
+ ? path.join(path.dirname(requireFromTarget.resolve('jest/package.json')), 'bin', 'jest.js')
440
+ : requireFromTarget.resolve('@playwright/test/cli')
441
+ const realCli = fs.realpathSync(cli)
442
+ const stat = fs.lstatSync(cli)
443
+ if (!isPathInside(dependencyRoot, realCli) || !stat.isFile() || stat.isSymbolicLink()) {
444
+ throw new Error(`${runner} CLI must be a regular file in the protected target dependency tree`)
445
+ }
446
+ return realCli
447
+ }
448
+
449
+ function resolveBrowserRuntime(target, isolatedCache) {
450
+ const dependencyRoot = fs.realpathSync(path.join(target, 'node_modules'))
451
+ const requireFromTarget = createRequire(path.join(target, 'package.json'))
452
+ const playwrightCoreEntry = fs.realpathSync(requireFromTarget.resolve('playwright-core'))
453
+ if (!isPathInside(dependencyRoot, playwrightCoreEntry)) throw new Error('playwright-core must resolve from the protected target dependency tree')
454
+ let playwrightCoreRoot = path.dirname(playwrightCoreEntry)
455
+ while (path.dirname(playwrightCoreRoot) !== playwrightCoreRoot && path.basename(playwrightCoreRoot) !== 'playwright-core') playwrightCoreRoot = path.dirname(playwrightCoreRoot)
456
+ const browsersFile = path.join(playwrightCoreRoot, 'browsers.json')
457
+ const browsersStat = fs.lstatSync(browsersFile)
458
+ if (!browsersStat.isFile() || browsersStat.isSymbolicLink() || browsersStat.size > 262_144) throw new Error('playwright-core browsers.json is unsafe')
459
+ const browserDeclaration = readJson(browsersFile)?.browsers?.find((entry) => entry.name === 'chromium-headless-shell')
460
+ if (!/^[0-9]+$/.test(browserDeclaration?.revision ?? '')) throw new Error('Playwright does not declare a bounded Chromium headless-shell revision')
461
+ const chromiumExecutable = requireFromTarget('@playwright/test').chromium.executablePath()
462
+ let chromiumRoot = path.dirname(fs.realpathSync(chromiumExecutable))
463
+ while (path.dirname(chromiumRoot) !== chromiumRoot && !/^chromium-[0-9]+$/.test(path.basename(chromiumRoot))) chromiumRoot = path.dirname(chromiumRoot)
464
+ if (!/^chromium-[0-9]+$/.test(path.basename(chromiumRoot))) throw new Error('the Playwright browser cache root could not be bounded')
465
+ const browserRoot = path.join(path.dirname(chromiumRoot), `chromium_headless_shell-${browserDeclaration.revision}`)
466
+ const browserStat = fs.lstatSync(browserRoot)
467
+ if (!browserStat.isDirectory() || browserStat.isSymbolicLink()) throw new Error('the exact Playwright Chromium headless-shell runtime is unavailable')
468
+ const marker = path.join(browserRoot, 'INSTALLATION_COMPLETE')
469
+ if (!fs.lstatSync(marker).isFile()) throw new Error('the Playwright Chromium headless-shell runtime is incomplete')
470
+ const executableNames = new Set(process.platform === 'win32' ? ['headless_shell.exe'] : ['chrome-headless-shell', 'headless_shell'])
471
+ const executables = []
472
+ const visit = (directory) => {
473
+ for (const name of fs.readdirSync(directory)) {
474
+ const child = path.join(directory, name)
475
+ const entry = fs.lstatSync(child)
476
+ if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) throw new Error('the Playwright Chromium headless-shell runtime contains an unsafe entry')
477
+ if (entry.isDirectory()) visit(child)
478
+ else if (executableNames.has(name)) executables.push(child)
479
+ }
480
+ }
481
+ visit(browserRoot)
482
+ if (executables.length !== 1) throw new Error('the Playwright Chromium headless-shell executable could not be identified exactly')
483
+ const cacheLink = path.join(isolatedCache, path.basename(browserRoot))
484
+ fs.symlinkSync(browserRoot, cacheLink, process.platform === 'win32' ? 'junction' : 'dir')
485
+ return { browserRoot: fs.realpathSync(browserRoot), isolatedCache, executable: fs.realpathSync(executables[0]) }
486
+ }
487
+
488
+ export function buildReleasePlan({ cases, registry, releaseMatrix, fixtures, seeds, targetsManifest, root, primaryRunner, portabilityRunner, targetsMayNotExist = false }) {
489
+ const allCaseIds = cases.map((item) => item.id)
490
+ const writableCases = cases.filter((item) => WRITABLE_KINDS.has(item.evaluationKind))
491
+ const writableCaseIds = writableCases.map((item) => item.id)
492
+ const reviewEligibleCaseIds = [...writableCaseIds]
493
+ const violations = []
494
+ const release = releaseMatrix?.releaseSuite ?? {}
495
+ const deterministicIds = resolveCaseSelector(releaseMatrix?.deterministic?.caseIds, allCaseIds)
496
+ const deterministicMissingCaseIds = difference(allCaseIds, deterministicIds)
497
+ const deterministicUnexpectedCaseIds = difference(deterministicIds, allCaseIds)
498
+ addCoverageViolation(violations, 'deterministic matrix is missing cases', deterministicMissingCaseIds)
499
+ addCoverageViolation(violations, 'deterministic matrix contains unknown cases', deterministicUnexpectedCaseIds)
500
+ if (releaseMatrix?.deterministic?.caseIds !== 'all') violations.push('deterministic matrix must use the exact all-cases selector')
501
+
502
+ const supportedRunners = Array.isArray(release.supportedRunners) ? release.supportedRunners : []
503
+ if (JSON.stringify(supportedRunners) !== JSON.stringify(RELEASE_SUPPORTED_RUNNERS)) {
504
+ violations.push(`release supported runners must be exactly: ${RELEASE_SUPPORTED_RUNNERS.join(', ')}`)
505
+ }
506
+ if (!RUNNERS.has(primaryRunner)) violations.push('release requires one valid primary runner')
507
+ if (portabilityRunner !== undefined && !RUNNERS.has(portabilityRunner)) violations.push('release portability runner is invalid')
508
+ if (portabilityRunner !== undefined && portabilityRunner === primaryRunner) violations.push('release portability runner must differ from the primary runner')
509
+ const configuredRunnerNames = Object.keys(releaseMatrix?.routing?.runners ?? {})
510
+ if (JSON.stringify(configuredRunnerNames) !== JSON.stringify(RELEASE_SUPPORTED_RUNNERS)) {
511
+ violations.push(`routing matrix must configure exactly: ${RELEASE_SUPPORTED_RUNNERS.join(', ')}`)
512
+ }
513
+ for (const runner of RELEASE_SUPPORTED_RUNNERS) {
514
+ if (!validModelSelector(releaseMatrix?.routing?.runners?.[runner]?.modelSelector)) violations.push(`routing matrix lacks a valid bounded ${runner} model selector`)
515
+ }
516
+ const requiredCaseIds = resolveCaseSelector(releaseMatrix?.routing?.required?.caseIds, allCaseIds)
517
+ const requiredUnknownCaseIds = difference(requiredCaseIds, allCaseIds)
518
+ addCoverageViolation(violations, 'primary routing matrix contains unknown cases', requiredUnknownCaseIds)
519
+ if (releaseMatrix?.routing?.required?.caseIds !== 'all') violations.push('primary routing matrix must use the exact all-cases selector')
520
+ const portabilityCaseIds = resolveCaseSelector(releaseMatrix?.routing?.portability?.caseIds, allCaseIds)
521
+ const portabilityUnknownCaseIds = difference(portabilityCaseIds, allCaseIds)
522
+ addCoverageViolation(violations, 'portability routing matrix contains unknown cases', portabilityUnknownCaseIds)
523
+ if (JSON.stringify(portabilityCaseIds) !== JSON.stringify(writableCaseIds)) {
524
+ violations.push('portability routing matrix must match the exact writable case set in catalog order')
525
+ }
526
+ const routing = []
527
+ if (RUNNERS.has(primaryRunner)) routing.push({ lane: 'primary', runner: primaryRunner, expectedCaseIds: requiredCaseIds, unknownCaseIds: requiredUnknownCaseIds })
528
+ if (RUNNERS.has(portabilityRunner) && portabilityRunner !== primaryRunner) {
529
+ routing.push({ lane: 'portability', runner: portabilityRunner, expectedCaseIds: portabilityCaseIds, unknownCaseIds: portabilityUnknownCaseIds })
530
+ }
531
+
532
+ const writableEntries = Array.isArray(releaseMatrix?.writable) ? releaseMatrix.writable : []
533
+ const assignedWritableIds = writableEntries.map((entry) => entry?.caseId)
534
+ const missingWritableMatrixCaseIds = difference(writableCaseIds, assignedWritableIds)
535
+ const unexpectedWritableMatrixCaseIds = difference(assignedWritableIds, writableCaseIds)
536
+ const duplicateWritableMatrixCaseIds = duplicates(assignedWritableIds)
537
+ addCoverageViolation(violations, 'writable release matrix is missing cases', missingWritableMatrixCaseIds)
538
+ addCoverageViolation(violations, 'writable release matrix contains non-writable cases', unexpectedWritableMatrixCaseIds)
539
+ addCoverageViolation(violations, 'writable release matrix contains duplicate cases', duplicateWritableMatrixCaseIds)
540
+ for (const entry of writableEntries) {
541
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).length !== 1 || typeof entry.caseId !== 'string') violations.push(`writable release assignment must be runner-neutral: ${String(entry?.caseId)}`)
542
+ }
543
+
544
+ const configuredReviewIds = Array.isArray(releaseMatrix?.generatedCodeReview?.caseIds)
545
+ ? releaseMatrix.generatedCodeReview.caseIds
546
+ : []
547
+ const missingReviewMatrixCaseIds = difference(reviewEligibleCaseIds, configuredReviewIds)
548
+ const unexpectedReviewMatrixCaseIds = difference(configuredReviewIds, reviewEligibleCaseIds)
549
+ const duplicateReviewMatrixCaseIds = duplicates(configuredReviewIds)
550
+ addCoverageViolation(violations, 'generated-code review matrix is missing eligible cases', missingReviewMatrixCaseIds)
551
+ addCoverageViolation(violations, 'generated-code review matrix contains ineligible cases', unexpectedReviewMatrixCaseIds)
552
+ addCoverageViolation(violations, 'generated-code review matrix contains duplicate cases', duplicateReviewMatrixCaseIds)
553
+ if (JSON.stringify(configuredReviewIds) !== JSON.stringify(reviewEligibleCaseIds)) {
554
+ violations.push('generated-code review matrix must list every writable case exactly once in catalog order')
555
+ }
556
+ if (release.requireGeneratedCodeReview !== true) violations.push('release suite must require generated-code review')
557
+ if (releaseMatrix?.generatedCodeReview?.skill !== 'om-code-review') violations.push('generated-code review must explicitly use om-code-review')
558
+ if (releaseMatrix?.generatedCodeReview?.required !== true) violations.push('generated-code review must be mandatory for every writable case')
559
+ const reviewRunners = releaseMatrix?.generatedCodeReview?.runners ?? {}
560
+ for (const runner of RELEASE_SUPPORTED_RUNNERS) {
561
+ if (!validModelSelector(reviewRunners?.[runner]?.modelSelector)) violations.push(`generated-code review lacks a valid bounded ${runner} model selector`)
562
+ }
563
+ for (const runner of Object.keys(reviewRunners)) {
564
+ if (!RUNNERS.has(runner)) violations.push(`generated-code review declares an invalid runner: ${runner}`)
565
+ }
566
+ const reviewSkillFiles = [
567
+ '.agents/skills/om-code-review/SKILL.md',
568
+ '.agents/skills/om-code-review/references/agentic-setup.md',
569
+ '.agents/skills/om-code-review/references/output-format.md',
570
+ '.agents/skills/om-code-review/references/review-checklist.md',
571
+ '.agents/skills/om-code-review/references/rules.md',
572
+ '.agents/skills/.om-external-ownership.json',
573
+ ]
574
+ const reviewSkillReady = !root || reviewSkillFiles.every((relative) => fs.existsSync(path.join(root, relative)))
575
+ if (!reviewSkillReady) violations.push('generated-code review skill is not installed with ownership evidence')
576
+
577
+ const registryCaseCountMatches = registry?.catalog?.expectedCaseCount === cases.length
578
+ if (!registryCaseCountMatches) violations.push(`validator registry expects ${String(registry?.catalog?.expectedCaseCount)} cases but catalog contains ${cases.length}`)
579
+ const registryWritableIds = Array.isArray(registry?.catalog?.writableCaseIds) ? registry.catalog.writableCaseIds : []
580
+ const registryMissingWritableCaseIds = difference(writableCaseIds, registryWritableIds)
581
+ const registryUnexpectedWritableCaseIds = difference(registryWritableIds, writableCaseIds)
582
+ addCoverageViolation(violations, 'validator registry is missing writable cases', registryMissingWritableCaseIds)
583
+ addCoverageViolation(violations, 'validator registry contains non-writable cases', registryUnexpectedWritableCaseIds)
584
+
585
+ const missingFixtureCaseIds = []
586
+ const missingOracleCaseIds = []
587
+ for (const item of writableCases) {
588
+ const setupEntries = Array.isArray(item.fixture?.setup) ? item.fixture.setup : []
589
+ const setupIds = setupEntries
590
+ .filter((entry) => typeof entry === 'string' && /^fixture:[a-z0-9-]+$/.test(entry))
591
+ .map((entry) => entry.slice('fixture:'.length))
592
+ const fixtureComplete = item.fixture?.scaffold === 'fresh-standalone'
593
+ && setupIds.length > 0
594
+ && setupIds.length === setupEntries.length
595
+ && setupIds.every((id) => fixtures?.fixtures?.[id] && seeds?.fixtures?.[id])
596
+ if (!fixtureComplete) missingFixtureCaseIds.push(item.id)
597
+ const oracleIds = Array.isArray(item.oracle?.validatorIds) ? item.oracle.validatorIds : []
598
+ const oracleComplete = oracleIds.some((id) => {
599
+ const declaration = registry?.validators?.[id]
600
+ return declaration?.implementation === 'trusted-executable' && Array.isArray(declaration.runners) && declaration.runners.length > 0
601
+ }) && Array.isArray(item.oracle?.expectedArtifacts) && item.oracle.expectedArtifacts.length > 0
602
+ if (!oracleComplete) missingOracleCaseIds.push(item.id)
603
+ }
604
+ addCoverageViolation(violations, 'writable cases lack complete fixture coverage', missingFixtureCaseIds)
605
+ addCoverageViolation(violations, 'writable cases lack trusted oracle coverage', missingOracleCaseIds)
606
+
607
+ const targetEntries = validTargetsManifest(targetsManifest) ? targetsManifest.targets : {}
608
+ if (!validTargetsManifest(targetsManifest)) violations.push('writable target manifest must use schemaVersion 1 and a targets object')
609
+ const targetIds = Object.keys(targetEntries)
610
+ const missingTargetCaseIds = writableCaseIds.filter((id) => typeof targetEntries[id] !== 'string' || !path.isAbsolute(targetEntries[id]))
611
+ const unexpectedTargetCaseIds = difference(targetIds, writableCaseIds)
612
+ const duplicateTargetCaseIds = []
613
+ const invalidTargetCaseIds = []
614
+ const targetOwners = new Map()
615
+ const controllerRoot = root ? fs.realpathSync(root) : undefined
616
+ for (const id of writableCaseIds) {
617
+ const target = targetEntries[id]
618
+ if (typeof target !== 'string' || !path.isAbsolute(target)) continue
619
+ let normalized = path.resolve(target)
620
+ if (root && !targetsMayNotExist) {
621
+ try {
622
+ const stat = fs.lstatSync(target)
623
+ normalized = fs.realpathSync(target)
624
+ const required = ['package.json', 'src/modules.ts', '.ai/harness/cases.json']
625
+ const requiredAreSafe = required.every((relative) => {
626
+ const absolute = path.join(normalized, relative)
627
+ try {
628
+ const entry = fs.lstatSync(absolute)
629
+ return entry.isFile() && !entry.isSymbolicLink() && isPathInside(normalized, fs.realpathSync(absolute))
630
+ } catch { return false }
631
+ })
632
+ const dependencyPath = path.join(normalized, 'node_modules')
633
+ let dependenciesAreSafe = false
634
+ try {
635
+ const dependencyEntry = fs.lstatSync(dependencyPath)
636
+ dependenciesAreSafe = dependencyEntry.isSymbolicLink()
637
+ && fs.statSync(dependencyPath).isDirectory()
638
+ && root
639
+ && fs.realpathSync(dependencyPath) === fs.realpathSync(path.join(root, 'node_modules'))
640
+ } catch { /* invalid target below */ }
641
+ if (!stat.isDirectory() || stat.isSymbolicLink() || normalized === root
642
+ || !requiredAreSafe || !dependenciesAreSafe
643
+ || fs.existsSync(path.join(normalized, '.ai', 'harness', 'DISPOSABLE'))) invalidTargetCaseIds.push(id)
644
+ } catch { invalidTargetCaseIds.push(id) }
645
+ }
646
+ if (controllerRoot && (isPathInside(controllerRoot, normalized) || isPathInside(normalized, controllerRoot))) invalidTargetCaseIds.push(id)
647
+ for (const [ownedRoot, ownerId] of targetOwners) {
648
+ if (isPathInside(ownedRoot, normalized) || isPathInside(normalized, ownedRoot)) duplicateTargetCaseIds.push(id, ownerId)
649
+ }
650
+ targetOwners.set(normalized, id)
651
+ }
652
+ addCoverageViolation(violations, 'writable target manifest is missing absolute targets', sortedUnique(missingTargetCaseIds))
653
+ addCoverageViolation(violations, 'writable target manifest contains unknown cases', unexpectedTargetCaseIds)
654
+ addCoverageViolation(violations, 'writable target manifest reuses targets', sortedUnique(duplicateTargetCaseIds))
655
+ addCoverageViolation(violations, 'writable target manifest contains invalid or reused app roots', sortedUnique(invalidTargetCaseIds))
656
+
657
+ const validationCommands = Array.isArray(release.validationCommands) ? release.validationCommands : []
658
+ if (validationCommands.length === 0) violations.push('release suite must declare validation commands')
659
+ for (const command of validationCommands) {
660
+ if (!ALLOWED_VALIDATION_COMMANDS.has(command)) violations.push(`release validation command is not allowed: ${String(command)}`)
661
+ }
662
+ const missingValidationCommands = difference([...ALLOWED_VALIDATION_COMMANDS], validationCommands)
663
+ addCoverageViolation(violations, 'release suite is missing validation commands', missingValidationCommands)
664
+ if (JSON.stringify(validationCommands) !== JSON.stringify(RELEASE_VALIDATION_COMMANDS)) {
665
+ violations.push('release validation commands must be the exact ordered four-command gate')
666
+ }
667
+
668
+ const generatedTestCases = writableCases.filter((item) => item.tags?.some((tag) => ['unit-tests', 'api-tests', 'browser-tests'].includes(tag)))
669
+ const expectedGeneratedTestIds = generatedTestCases.map((item) => item.id)
670
+ const generatedTestEntries = Array.isArray(releaseMatrix?.generatedTests?.entries) ? releaseMatrix.generatedTests.entries : []
671
+ const configuredGeneratedTestIds = generatedTestEntries.map((entry) => entry.caseId)
672
+ const missingGeneratedTestIds = difference(expectedGeneratedTestIds, configuredGeneratedTestIds)
673
+ const unexpectedGeneratedTestIds = difference(configuredGeneratedTestIds, expectedGeneratedTestIds)
674
+ const duplicateGeneratedTestIds = duplicates(configuredGeneratedTestIds)
675
+ if (releaseMatrix?.generatedTests?.required !== true) violations.push('generated test execution must be required')
676
+ addCoverageViolation(violations, 'generated test execution matrix is missing cases', missingGeneratedTestIds)
677
+ addCoverageViolation(violations, 'generated test execution matrix contains ineligible cases', unexpectedGeneratedTestIds)
678
+ addCoverageViolation(violations, 'generated test execution matrix contains duplicate cases', duplicateGeneratedTestIds)
679
+ for (const entry of generatedTestEntries) {
680
+ const item = generatedTestCases.find((candidate) => candidate.id === entry.caseId)
681
+ const expectedRunner = item?.tags?.includes('unit-tests') ? 'jest'
682
+ : item?.tags?.includes('api-tests') ? 'playwright-api'
683
+ : item?.tags?.includes('browser-tests') ? 'playwright-browser' : undefined
684
+ const expectedNetwork = expectedRunner === 'jest' ? 'none' : 'loopback'
685
+ const canonicalPath = expectedRunner === 'jest'
686
+ ? /^src\/modules\/[a-z0-9_]+\/(?:[^/]+\/)*__tests__\/[A-Za-z0-9._-]+\.(?:test|spec)\.tsx?$/
687
+ : /^src\/modules\/[a-z0-9_]+\/(?:[^/]+\/)*__integration__\/[A-Za-z0-9._-]+\.spec\.tsx?$/
688
+ if (!item || !GENERATED_TEST_RUNNERS.has(entry.runner) || entry.runner !== expectedRunner
689
+ || entry.network !== expectedNetwork || typeof entry.artifact !== 'string'
690
+ || !canonicalPath.test(entry.artifact) || !item.oracle?.expectedArtifacts?.includes(entry.artifact)
691
+ || !item.allowedWrites?.includes(entry.artifact)) {
692
+ violations.push(`generated test execution assignment is invalid: ${String(entry.caseId)}`)
693
+ }
694
+ }
695
+
696
+ const steps = [
697
+ { id: 'deterministic:all', kind: 'deterministic', expectedCaseIds: deterministicIds },
698
+ ...validationCommands.map((command) => ({ id: `validation:${command.slice('yarn '.length)}`, kind: 'validation', command })),
699
+ ...routing.map((entry) => ({ id: `routing:${entry.lane}:${entry.runner}`, kind: 'routing', lane: entry.lane, runner: entry.runner, modelSelector: releaseMatrix.routing.runners[entry.runner].modelSelector, expectedCaseIds: entry.expectedCaseIds })),
700
+ ]
701
+ for (const caseId of writableCaseIds) {
702
+ const assignment = writableEntries.find((entry) => entry?.caseId === caseId)
703
+ if (!assignment || !RUNNERS.has(primaryRunner)) continue
704
+ steps.push({ id: `fixture:${caseId}`, kind: 'fixture', caseId })
705
+ steps.push({ id: `writable:${caseId}`, kind: 'writable', caseId, runner: primaryRunner, modelSelector: releaseMatrix.routing.runners[primaryRunner].modelSelector })
706
+ for (const command of validationCommands) {
707
+ steps.push({ id: `target-validation:${caseId}:${command.slice('yarn '.length)}`, kind: 'target-validation', caseId, command })
708
+ }
709
+ const generatedTest = generatedTestEntries.find((entry) => entry.caseId === caseId)
710
+ if (generatedTest) {
711
+ const executionContract = generatedTestExecutionContract(generatedTest)
712
+ steps.push({
713
+ id: `generated-test:${caseId}:${generatedTest.runner}`,
714
+ kind: 'generated-test',
715
+ caseId,
716
+ testRunner: generatedTest.runner,
717
+ artifact: generatedTest.artifact,
718
+ network: generatedTest.network,
719
+ ...executionContract,
720
+ })
721
+ }
722
+ if (configuredReviewIds.includes(caseId)) {
723
+ const modelSelector = releaseMatrix.generatedCodeReview?.runners?.[primaryRunner]?.modelSelector
724
+ if (!validModelSelector(modelSelector)) violations.push(`generated-code review lacks a valid bounded ${primaryRunner} model selector for ${caseId}`)
725
+ steps.push({ id: `review:${caseId}`, kind: 'review', caseId, runner: primaryRunner, modelSelector })
726
+ }
727
+ }
728
+
729
+ return {
730
+ runnerPolicy: { primaryRunner: RUNNERS.has(primaryRunner) ? primaryRunner : null, portabilityRunner: RUNNERS.has(portabilityRunner) ? portabilityRunner : null },
731
+ catalog: {
732
+ caseCount: cases.length,
733
+ writableCaseCount: writableCaseIds.length,
734
+ reviewEligibleCaseCount: reviewEligibleCaseIds.length,
735
+ },
736
+ coverage: {
737
+ deterministic: { expectedCaseIds: allCaseIds, configuredCaseIds: deterministicIds, missingMatrixCaseIds: deterministicMissingCaseIds, unexpectedMatrixCaseIds: deterministicUnexpectedCaseIds },
738
+ routing,
739
+ writable: {
740
+ expectedCaseIds: writableCaseIds,
741
+ configuredCaseIds: assignedWritableIds,
742
+ missingMatrixCaseIds: missingWritableMatrixCaseIds,
743
+ unexpectedMatrixCaseIds: unexpectedWritableMatrixCaseIds,
744
+ duplicateMatrixCaseIds: duplicateWritableMatrixCaseIds,
745
+ missingTargetCaseIds: sortedUnique(missingTargetCaseIds),
746
+ unexpectedTargetCaseIds,
747
+ duplicateTargetCaseIds: sortedUnique(duplicateTargetCaseIds),
748
+ invalidTargetCaseIds: sortedUnique(invalidTargetCaseIds),
749
+ missingFixtureCaseIds: sortedUnique(missingFixtureCaseIds),
750
+ missingOracleCaseIds: sortedUnique(missingOracleCaseIds),
751
+ },
752
+ review: { expectedCaseIds: reviewEligibleCaseIds, configuredCaseIds: configuredReviewIds, missingMatrixCaseIds: missingReviewMatrixCaseIds, unexpectedMatrixCaseIds: unexpectedReviewMatrixCaseIds, skillReady: reviewSkillReady },
753
+ registry: { caseCountMatches: registryCaseCountMatches, missingWritableCaseIds: registryMissingWritableCaseIds, unexpectedWritableCaseIds: registryUnexpectedWritableCaseIds },
754
+ validation: { expectedCommands: [...ALLOWED_VALIDATION_COMMANDS], configuredCommands: validationCommands, missingCommands: missingValidationCommands },
755
+ targetValidation: { expectedCaseIds: writableCaseIds, commands: validationCommands },
756
+ generatedTests: {
757
+ expectedCaseIds: expectedGeneratedTestIds,
758
+ configuredCaseIds: configuredGeneratedTestIds,
759
+ missingMatrixCaseIds: missingGeneratedTestIds,
760
+ unexpectedMatrixCaseIds: unexpectedGeneratedTestIds,
761
+ duplicateMatrixCaseIds: duplicateGeneratedTestIds,
762
+ },
763
+ },
764
+ targets: targetEntries,
765
+ steps,
766
+ violations: sortedUnique(violations),
767
+ }
768
+ }
769
+
770
+ export function releaseHostPrerequisiteViolations(releaseMatrix, platform = process.platform, env = process.env) {
771
+ const entries = Array.isArray(releaseMatrix?.generatedTests?.entries) ? releaseMatrix.generatedTests.entries : []
772
+ const requiresLoopback = entries.some((entry) => entry?.network === 'loopback')
773
+ const modes = ['none', ...(requiresLoopback ? ['loopback'] : [])]
774
+ const violations = []
775
+ for (const mode of modes) {
776
+ try {
777
+ assertSandboxRuntimeAvailable(mode, platform, env)
778
+ } catch (error) {
779
+ violations.push(error instanceof Error ? error.message : String(error))
780
+ }
781
+ }
782
+ if (platform === 'linux' && requiresLoopback && violations.length === 0) {
783
+ try {
784
+ verifyLinuxSandboxIsolation(env)
785
+ } catch (error) {
786
+ violations.push(error instanceof Error ? error.message : String(error))
787
+ }
788
+ }
789
+ return [...new Set(violations)]
790
+ }
791
+
792
+ function percentage(numerator, denominator) {
793
+ return denominator === 0 ? 0 : Math.round((numerator / denominator) * 10_000) / 100
794
+ }
795
+
796
+ function percentile(values, value) {
797
+ if (values.length === 0) return 0
798
+ const sorted = [...values].sort((left, right) => left - right)
799
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * value) - 1)]
800
+ }
801
+
802
+ function violationCategory(value) {
803
+ const text = String(value)
804
+ if (/forbidden|unsafe|out-of-root|outside allowlist|protected roots|inspection|modified (?:target|bundle)|trace unavailable|no observed context/i.test(text)) return 'safety'
805
+ if (/route|skill|context|decision|pattern|budget|missing|required|unexpected|mismatch/i.test(text)) return 'contract'
806
+ if (/process|environment|structured output|timed out|runner/i.test(text)) return 'execution'
807
+ return 'quality'
808
+ }
809
+
810
+ export function aggregateQualityMetrics(results) {
811
+ const contextTokens = []
812
+ const initialContextTokens = []
813
+ let passed = 0
814
+ let corrections = 0
815
+ let resultsWithCorrections = 0
816
+ let firstPassPassed = 0
817
+ let totalViolations = 0
818
+ let resultsWithViolations = 0
819
+ const violationsByCategory = { safety: 0, contract: 0, execution: 0, quality: 0 }
820
+ const verdicts = { approve: 0, requestChanges: 0, error: 0 }
821
+ for (const result of results) {
822
+ if (result.status === 'pass') passed += 1
823
+ const resultCorrections = Number.isInteger(result.corrections) ? result.corrections : Math.max(0, (result.attempts ?? 1) - 1)
824
+ corrections += resultCorrections
825
+ if (resultCorrections > 0) resultsWithCorrections += 1
826
+ if (result.status === 'pass' && resultCorrections === 0) firstPassPassed += 1
827
+ if (Number.isInteger(result.actualContext?.estimatedTokens)) contextTokens.push(result.actualContext.estimatedTokens)
828
+ if (Number.isInteger(result.actualContext?.estimatedInitialTokens)) initialContextTokens.push(result.actualContext.estimatedInitialTokens)
829
+ const resultViolations = Array.isArray(result.violations) ? result.violations : []
830
+ if (resultViolations.length) resultsWithViolations += 1
831
+ totalViolations += resultViolations.length
832
+ for (const violation of resultViolations) violationsByCategory[violationCategory(violation)] += 1
833
+ if (result.verdict === 'approve') verdicts.approve += 1
834
+ else if (result.verdict === 'request changes') verdicts.requestChanges += 1
835
+ else if (result.verdict === 'error') verdicts.error += 1
836
+ }
837
+ const totalContextTokens = contextTokens.reduce((sum, value) => sum + value, 0)
838
+ const totalInitialContextTokens = initialContextTokens.reduce((sum, value) => sum + value, 0)
839
+ return {
840
+ outcomes: { results: results.length, passed, failed: results.length - passed },
841
+ firstPass: { eligibleResults: results.length, passedFirstPass: firstPassPassed, ratePct: percentage(firstPassPassed, results.length) },
842
+ corrections: { total: corrections, resultsWithCorrections, ratePct: percentage(resultsWithCorrections, results.length) },
843
+ context: {
844
+ measuredResults: contextTokens.length,
845
+ totalEstimatedTokens: totalContextTokens,
846
+ averageEstimatedTokens: contextTokens.length ? Math.round(totalContextTokens / contextTokens.length) : 0,
847
+ p95EstimatedTokens: percentile(contextTokens, 0.95),
848
+ maxEstimatedTokens: contextTokens.length ? Math.max(...contextTokens) : 0,
849
+ totalEstimatedInitialTokens: totalInitialContextTokens,
850
+ averageEstimatedInitialTokens: initialContextTokens.length ? Math.round(totalInitialContextTokens / initialContextTokens.length) : 0,
851
+ },
852
+ misuse: { totalViolations, resultsWithViolations, ratePct: percentage(resultsWithViolations, results.length), byCategory: violationsByCategory },
853
+ reviewVerdicts: verdicts,
854
+ }
855
+ }
856
+
857
+ export function sanitizeReportText(value, roots = []) {
858
+ let result = String(value ?? '')
859
+ for (const root of [...roots, os.homedir()].filter(Boolean).sort((left, right) => right.length - left.length)) result = result.split(root).join('<redacted-path>')
860
+ const environmentSecrets = [...new Set(Object.entries(process.env)
861
+ .filter(([key, entry]) => SENSITIVE_ENV_KEY.test(key) && typeof entry === 'string' && entry.length >= 4)
862
+ .map(([, entry]) => entry))]
863
+ .sort((left, right) => right.length - left.length)
864
+ for (const secret of environmentSecrets) result = result.split(secret).join('<redacted-secret>')
865
+ result = result.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, '$1<redacted-secret>@')
866
+ result = result.replace(/\b(?:sk|pk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{10,}\b/g, '<redacted-secret>')
867
+ result = result.replace(/\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, '<redacted-secret>')
868
+ return result.slice(0, ERROR_LIMIT)
869
+ }
870
+
871
+ function sanitizeObject(value, roots) {
872
+ if (typeof value === 'string') return sanitizeReportText(value, roots)
873
+ if (Array.isArray(value)) return value.map((entry) => sanitizeObject(entry, roots))
874
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeObject(entry, roots)]))
875
+ return value
876
+ }
877
+
878
+ function schemaTypeMatches(value, expected) {
879
+ if (expected === 'null') return value === null
880
+ if (expected === 'array') return Array.isArray(value)
881
+ if (expected === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value)
882
+ if (expected === 'integer') return Number.isInteger(value)
883
+ if (expected === 'number') return typeof value === 'number' && Number.isFinite(value)
884
+ return typeof value === expected
885
+ }
886
+
887
+ function resolveSchemaReference(rootSchema, reference) {
888
+ if (!reference.startsWith('#/')) throw new Error(`unsupported release schema reference ${reference}`)
889
+ return reference.slice(2).split('/').reduce((value, key) => value?.[key.replaceAll('~1', '/').replaceAll('~0', '~')], rootSchema)
890
+ }
891
+
892
+ function validateJsonSchema(value, schema, rootSchema = schema, location = '$') {
893
+ if (schema.$ref) return validateJsonSchema(value, resolveSchemaReference(rootSchema, schema.$ref), rootSchema, location)
894
+ const errors = []
895
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []
896
+ if (types.length && !types.some((type) => schemaTypeMatches(value, type))) return [`${location} has the wrong type`]
897
+ if (Object.hasOwn(schema, 'const') && value !== schema.const) errors.push(`${location} differs from its constant`)
898
+ if (schema.enum && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) errors.push(`${location} is outside its enum`)
899
+ if (typeof value === 'string') {
900
+ if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength) errors.push(`${location} exceeds maxLength`)
901
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) errors.push(`${location} does not match its pattern`)
902
+ }
903
+ if (typeof value === 'number') {
904
+ if (Number.isFinite(schema.minimum) && value < schema.minimum) errors.push(`${location} is below minimum`)
905
+ if (Number.isFinite(schema.maximum) && value > schema.maximum) errors.push(`${location} exceeds maximum`)
906
+ }
907
+ if (Array.isArray(value)) {
908
+ if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems) errors.push(`${location} exceeds maxItems`)
909
+ if (schema.uniqueItems && new Set(value.map((entry) => JSON.stringify(entry))).size !== value.length) errors.push(`${location} must contain unique items`)
910
+ if (schema.items) value.forEach((entry, index) => errors.push(...validateJsonSchema(entry, schema.items, rootSchema, `${location}[${index}]`)))
911
+ }
912
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
913
+ for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) errors.push(`${location}.${required} is required`)
914
+ if (schema.additionalProperties === false) {
915
+ for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties ?? {}, key)) errors.push(`${location}.${key} is not allowed`)
916
+ }
917
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
918
+ if (Object.hasOwn(value, key)) errors.push(...validateJsonSchema(value[key], childSchema, rootSchema, `${location}.${key}`))
919
+ }
920
+ }
921
+ return errors
922
+ }
923
+
924
+ function resultFiles(root) {
925
+ const directory = path.join(root, '.ai', 'harness', 'results')
926
+ if (!fs.existsSync(directory)) return new Set()
927
+ return new Set(fs.readdirSync(directory).filter((file) => file.endsWith('.json')))
928
+ }
929
+
930
+ function readNewResults(root, before) {
931
+ const directory = path.join(root, '.ai', 'harness', 'results')
932
+ if (!fs.existsSync(directory)) return []
933
+ const results = []
934
+ for (const file of fs.readdirSync(directory).filter((entry) => entry.endsWith('.json') && !before.has(entry)).sort()) {
935
+ const absolute = path.join(directory, file)
936
+ const stat = fs.lstatSync(absolute)
937
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > RESULT_LIMIT || file.includes('-release-')) continue
938
+ try { results.push({ path: path.relative(root, absolute).replaceAll(path.sep, '/'), value: readJson(absolute) }) } catch { /* evaluator owns schema validation */ }
939
+ }
940
+ return results
941
+ }
942
+
943
+ function execute(command, args, cwd, timeout, env = process.env) {
944
+ const started = Date.now()
945
+ const result = spawnSync(command, args, { cwd, encoding: 'utf8', timeout, maxBuffer: 8 * 1024 * 1024, env })
946
+ return { ...result, durationMs: Date.now() - started }
947
+ }
948
+
949
+ function protectedTreeFingerprint(target) {
950
+ if (!fs.existsSync(target)) return '<missing>'
951
+ const hash = createHash('sha256')
952
+ const visit = (absolute, relative = '.') => {
953
+ let stat
954
+ try { stat = fs.lstatSync(absolute) } catch { hash.update(`${relative}:<unreadable>\n`); return }
955
+ hash.update(`${relative}:${stat.mode}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}\n`)
956
+ if (stat.isSymbolicLink()) {
957
+ try { hash.update(`link:${fs.readlinkSync(absolute)}\n`) } catch { /* metadata still detects the entry */ }
958
+ return
959
+ }
960
+ if (!stat.isDirectory()) return
961
+ for (const entry of fs.readdirSync(absolute).sort()) visit(path.join(absolute, entry), relative === '.' ? entry : `${relative}/${entry}`)
962
+ }
963
+ visit(target)
964
+ return hash.digest('hex')
965
+ }
966
+
967
+ export function dependencyContentFingerprint(appRoot) {
968
+ const dependencyRoot = path.join(appRoot, 'node_modules')
969
+ const stat = fs.lstatSync(dependencyRoot)
970
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('controller node_modules must remain a regular directory')
971
+ const hash = createHash('sha256')
972
+ const ownershipFiles = ['package.json', 'yarn.lock']
973
+ for (const relative of ownershipFiles) {
974
+ const absolute = path.join(appRoot, relative)
975
+ if (!fs.existsSync(absolute)) continue
976
+ const fileStat = fs.lstatSync(absolute)
977
+ if (!fileStat.isFile() || fileStat.isSymbolicLink() || fileStat.size > 8 * 1024 * 1024) throw new Error(`dependency ownership file is unsafe: ${relative}`)
978
+ hash.update(relative)
979
+ hash.update(fs.readFileSync(absolute))
980
+ }
981
+ let entries = 0
982
+ const visit = (directory, relativeDirectory = '') => {
983
+ for (const name of fs.readdirSync(directory).sort()) {
984
+ entries += 1
985
+ if (entries > 500_000) throw new Error('dependency content fingerprint exceeds its 500000-entry safety bound')
986
+ const absolute = path.join(directory, name)
987
+ const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name
988
+ const entry = fs.lstatSync(absolute)
989
+ hash.update(`${relative}:${entry.mode}:${entry.size}\n`)
990
+ if (entry.isSymbolicLink()) hash.update(`link:${fs.readlinkSync(absolute)}\n`)
991
+ else if (entry.isDirectory()) visit(absolute, relative)
992
+ else if (entry.isFile()) hash.update(fs.readFileSync(absolute))
993
+ else throw new Error(`dependency tree contains an unsupported entry: ${relative}`)
994
+ }
995
+ }
996
+ visit(dependencyRoot)
997
+ return hash.digest('hex')
998
+ }
999
+
1000
+ function targetSnapshot(root) {
1001
+ const result = new Map()
1002
+ const protectedRoots = new Set(['.git', 'node_modules', '.next', 'dist', '.ai/harness/results'])
1003
+ const visit = (directory, relativeDirectory = '') => {
1004
+ for (const name of fs.readdirSync(directory).sort()) {
1005
+ const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name
1006
+ if ([...protectedRoots].some((entry) => relative === entry || relative.startsWith(`${entry}/`))) continue
1007
+ const absolute = path.join(directory, name)
1008
+ let entry
1009
+ try { entry = fs.lstatSync(absolute) } catch { result.set(relative, '<unreadable>'); continue }
1010
+ if (entry.isSymbolicLink()) {
1011
+ try { result.set(relative, `<symlink:${fs.readlinkSync(absolute)}>`) } catch { result.set(relative, '<symlink:unreadable>') }
1012
+ } else if (entry.isDirectory()) {
1013
+ result.set(relative, `<directory:${entry.mode & 0o777}>`)
1014
+ visit(absolute, relative)
1015
+ }
1016
+ else if (entry.isFile()) {
1017
+ try { result.set(relative, `<file:${entry.mode & 0o777}:${sha256(fs.readFileSync(absolute))}>`) } catch { result.set(relative, '<unreadable>') }
1018
+ } else result.set(relative, `<special:${entry.mode & 0o170000}>`)
1019
+ }
1020
+ }
1021
+ visit(root)
1022
+ for (const relative of ['.git', 'node_modules', '.next', 'dist', '.ai/harness/results']) {
1023
+ result.set(relative, protectedTreeFingerprint(path.join(root, relative)))
1024
+ }
1025
+ return result
1026
+ }
1027
+
1028
+ function containedValidationSymlink(relative, fingerprint, command, after) {
1029
+ if (!validationOutputRoot(relative, command, after)) return false
1030
+ const target = fingerprint.slice('<symlink:'.length, -1)
1031
+ if (!target || path.isAbsolute(target)) return false
1032
+ const resolved = path.normalize(path.join(path.dirname(relative), target))
1033
+ return resolved !== '..' && !resolved.startsWith(`..${path.sep}`) && !path.isAbsolute(resolved)
1034
+ }
1035
+
1036
+ function unsafeChangedTargetEntries(after, changed, command) {
1037
+ const protectedRoots = new Set(['.git', 'node_modules', '.next', 'dist', '.ai/harness/results'])
1038
+ return changed.flatMap((relative) => {
1039
+ if (protectedRoots.has(relative)) return []
1040
+ const fingerprint = after.get(relative)
1041
+ if (typeof fingerprint !== 'string') return []
1042
+ if (fingerprint.startsWith('<symlink:') && !containedValidationSymlink(relative, fingerprint, command, after)) return [`${relative} (symbolic link)`]
1043
+ if (fingerprint.startsWith('<special:')) return [`${relative} (special file)`]
1044
+ if (fingerprint === '<unreadable>') return [`${relative} (unreadable)`]
1045
+ return []
1046
+ })
1047
+ }
1048
+
1049
+ function targetSnapshotFingerprint(snapshot) {
1050
+ const hash = createHash('sha256')
1051
+ for (const [relative, fingerprint] of [...snapshot.entries()].sort(([left], [right]) => left.localeCompare(right))) {
1052
+ hash.update(relative)
1053
+ hash.update('\0')
1054
+ hash.update(fingerprint)
1055
+ hash.update('\0')
1056
+ }
1057
+ return hash.digest('hex')
1058
+ }
1059
+
1060
+ export function targetContentFingerprint(root) {
1061
+ return targetSnapshotFingerprint(targetSnapshot(root))
1062
+ }
1063
+
1064
+ function stepResult(step, execution, artifacts, roots, expectedArtifacts) {
1065
+ const artifactMismatch = Number.isInteger(expectedArtifacts) && artifacts.length !== expectedArtifacts
1066
+ const status = execution.status === 0 && !execution.error && !artifactMismatch ? 'pass' : 'fail'
1067
+ const diagnostic = artifactMismatch
1068
+ ? `expected ${expectedArtifacts} result artifacts, found ${artifacts.length}`
1069
+ : execution.error?.message || (status === 'fail'
1070
+ ? [execution.stderr, execution.stdout].filter(Boolean).join('\n')
1071
+ : execution.stderr)
1072
+ return {
1073
+ id: step.id,
1074
+ kind: step.kind,
1075
+ ...(step.caseId ? { caseId: step.caseId } : {}),
1076
+ ...(step.runner ? { runner: step.runner } : {}),
1077
+ ...(step.testRunner ? { testRunner: step.testRunner } : {}),
1078
+ ...(step.artifact ? { artifact: step.artifact } : {}),
1079
+ ...(step.network ? { network: step.network } : {}),
1080
+ ...(step.executor ? { executor: step.executor } : {}),
1081
+ ...(step.cliPackage ? { cliPackage: step.cliPackage } : {}),
1082
+ ...(step.cliSha256 ? { cliSha256: step.cliSha256 } : {}),
1083
+ ...(step.argv ? { argv: step.argv } : {}),
1084
+ ...(step.command ? { command: step.command } : {}),
1085
+ status,
1086
+ exitStatus: execution.status,
1087
+ durationMs: execution.durationMs,
1088
+ resultPaths: artifacts.map((entry) => entry.path),
1089
+ ...(diagnostic ? { sanitizedError: sanitizeReportText(diagnostic, roots) } : {}),
1090
+ }
1091
+ }
1092
+
1093
+ function skippedStep(step, reason) {
1094
+ return {
1095
+ id: step.id,
1096
+ kind: step.kind,
1097
+ ...(step.caseId ? { caseId: step.caseId } : {}),
1098
+ ...(step.runner ? { runner: step.runner } : {}),
1099
+ ...(step.testRunner ? { testRunner: step.testRunner } : {}),
1100
+ ...(step.artifact ? { artifact: step.artifact } : {}),
1101
+ ...(step.network ? { network: step.network } : {}),
1102
+ ...(step.executor ? { executor: step.executor } : {}),
1103
+ ...(step.cliPackage ? { cliPackage: step.cliPackage } : {}),
1104
+ ...(step.cliSha256 ? { cliSha256: step.cliSha256 } : {}),
1105
+ ...(step.argv ? { argv: step.argv } : {}),
1106
+ ...(step.command ? { command: step.command } : {}),
1107
+ status: 'skipped',
1108
+ exitStatus: null,
1109
+ durationMs: 0,
1110
+ resultPaths: [],
1111
+ sanitizedError: reason,
1112
+ }
1113
+ }
1114
+
1115
+ export function createMinimalValidationEnvironment(tempRoot, pathValue = process.env.PATH ?? '') {
1116
+ const home = path.join(tempRoot, 'home')
1117
+ const temporary = path.join(tempRoot, 'tmp')
1118
+ const cache = path.join(tempRoot, 'cache')
1119
+ for (const directory of [home, temporary, cache]) fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
1120
+ const env = {
1121
+ PATH: pathValue,
1122
+ HOME: home,
1123
+ TMPDIR: temporary,
1124
+ TEMP: temporary,
1125
+ TMP: temporary,
1126
+ XDG_CONFIG_HOME: path.join(home, '.config'),
1127
+ XDG_CACHE_HOME: cache,
1128
+ XDG_DATA_HOME: path.join(home, '.local', 'share'),
1129
+ CI: '1',
1130
+ NO_COLOR: '1',
1131
+ NEXT_TELEMETRY_DISABLED: '1',
1132
+ YARN_ENABLE_TELEMETRY: '0',
1133
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: '0',
1134
+ TZ: 'UTC',
1135
+ __CF_USER_TEXT_ENCODING: '0x1F5:0x0:0x0',
1136
+ }
1137
+ const configuredCorepackHome = process.env.COREPACK_HOME || path.join(os.homedir(), '.cache', 'node', 'corepack')
1138
+ const corepackHome = fs.existsSync(configuredCorepackHome) && fs.statSync(configuredCorepackHome).isDirectory()
1139
+ ? fs.realpathSync(configuredCorepackHome)
1140
+ : undefined
1141
+ if (corepackHome) env.COREPACK_HOME = corepackHome
1142
+ for (const key of ['SystemRoot', 'WINDIR', 'PATHEXT', 'ComSpec']) {
1143
+ if (typeof process.env[key] === 'string') env[key] = process.env[key]
1144
+ }
1145
+ return { env, toolReadRoots: corepackHome ? [corepackHome] : [] }
1146
+ }
1147
+
1148
+ function validationOutputRoot(relative, command, after) {
1149
+ if (
1150
+ relative === '.env'
1151
+ && (command === 'yarn generate' || command === 'yarn build')
1152
+ && after.get('.env') === after.get('.env.example')
1153
+ ) return '.env'
1154
+ return (RELEASE_VALIDATION_OUTPUT_ROOTS.get(command) ?? []).find((root) => relative === root
1155
+ || relative.startsWith(`${root}/`)
1156
+ || (root.startsWith(`${relative}/`) && after.get(relative)?.startsWith('<directory:')))
1157
+ }
1158
+
1159
+ function cloneValidationDependencies(source, destination) {
1160
+ const sourceRoot = fs.realpathSync(source)
1161
+ const visit = (directory) => {
1162
+ for (const name of fs.readdirSync(directory)) {
1163
+ const absolute = path.join(directory, name)
1164
+ const entry = fs.lstatSync(absolute)
1165
+ if (entry.isSymbolicLink()) {
1166
+ const resolved = fs.realpathSync(absolute)
1167
+ if (resolved !== sourceRoot && !isPathInside(sourceRoot, resolved)) {
1168
+ throw new Error(`dependency tree contains an escaping symbolic link: ${path.relative(sourceRoot, absolute)}`)
1169
+ }
1170
+ } else if (entry.isDirectory()) visit(absolute)
1171
+ else if (!entry.isFile()) throw new Error(`dependency tree contains an unsupported entry: ${path.relative(sourceRoot, absolute)}`)
1172
+ }
1173
+ }
1174
+ visit(sourceRoot)
1175
+ fs.cpSync(sourceRoot, destination, {
1176
+ recursive: true,
1177
+ dereference: false,
1178
+ verbatimSymlinks: true,
1179
+ mode: fs.constants.COPYFILE_FICLONE,
1180
+ })
1181
+ }
1182
+
1183
+ function prepareValidationWorkspace(target, tempRoot, { allowContainedDependencies = false } = {}) {
1184
+ validateScaffoldCopySource(target)
1185
+ const workspace = path.join(tempRoot, 'target')
1186
+ copyFreshScaffold(target, workspace)
1187
+ const dependencyPath = path.join(target, 'node_modules')
1188
+ let dependencyRoot
1189
+ if (fs.existsSync(dependencyPath)) {
1190
+ const sourceDependencyRoot = fs.realpathSync(dependencyPath)
1191
+ if (!allowContainedDependencies && isPathInside(fs.realpathSync(target), sourceDependencyRoot)) {
1192
+ throw new Error('writable target node_modules must resolve outside the writable target')
1193
+ }
1194
+ cloneValidationDependencies(sourceDependencyRoot, path.join(workspace, 'node_modules'))
1195
+ dependencyRoot = fs.realpathSync(path.join(workspace, 'node_modules'))
1196
+ }
1197
+ return { workspace: fs.realpathSync(workspace), dependencyRoot }
1198
+ }
1199
+
1200
+ export function runTargetValidationSteps({ steps, target, timeout, roots, yarnCommand = process.platform === 'win32' ? 'yarn.cmd' : 'yarn', pathValue, allowContainedDependencies = false }) {
1201
+ const results = []
1202
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-harness-validation-'))
1203
+ const reportRoots = [...roots, tempRoot, fs.realpathSync(tempRoot)]
1204
+ const { env, toolReadRoots } = createMinimalValidationEnvironment(tempRoot, pathValue)
1205
+ const originalFingerprint = targetContentFingerprint(target)
1206
+ let workspace
1207
+ let dependencyRoot
1208
+ let workspaceError
1209
+ try {
1210
+ ({ workspace, dependencyRoot } = prepareValidationWorkspace(target, tempRoot, { allowContainedDependencies }))
1211
+ } catch (error) {
1212
+ workspaceError = error
1213
+ }
1214
+ let before = workspace ? targetSnapshot(workspace) : new Map()
1215
+ let workspaceTainted = false
1216
+ try {
1217
+ for (const step of steps) {
1218
+ if (workspaceTainted) {
1219
+ results.push(skippedStep(step, 'validation workspace was tainted by an out-of-contract mutation'))
1220
+ continue
1221
+ }
1222
+ let execution
1223
+ if (workspaceError) {
1224
+ execution = { status: null, error: workspaceError, stdout: '', stderr: '', durationMs: 0 }
1225
+ } else try {
1226
+ const readOnlyRoots = [...toolReadRoots]
1227
+ if (dependencyRoot) readOnlyRoots.unshift(dependencyRoot)
1228
+ const invocation = sandboxedInvocation({
1229
+ command: yarnCommand,
1230
+ args: [step.command.slice('yarn '.length)],
1231
+ cwd: workspace,
1232
+ writableRoots: [workspace, tempRoot],
1233
+ readOnlyRoots,
1234
+ networkMode: 'none',
1235
+ env,
1236
+ })
1237
+ execution = execute(invocation.command, invocation.args, invocation.cwd, timeout, invocation.env)
1238
+ } catch (error) {
1239
+ execution = { status: null, error, stdout: '', stderr: '', durationMs: 0 }
1240
+ }
1241
+ const after = workspace ? targetSnapshot(workspace) : before
1242
+ const changed = [...new Set([...before.keys(), ...after.keys()])].filter((key) => before.get(key) !== after.get(key)).sort()
1243
+ const unsafeEntries = unsafeChangedTargetEntries(after, changed, step.command)
1244
+ const disallowedEntries = changed.filter((relative) => !validationOutputRoot(relative, step.command, after))
1245
+ const touchedOutputRoots = [...new Set(changed.map((relative) => validationOutputRoot(relative, step.command, after)).filter(Boolean))].sort()
1246
+ const result = stepResult(step, execution, [], reportRoots)
1247
+ result.resultPaths = touchedOutputRoots
1248
+ if (disallowedEntries.length || unsafeEntries.length) {
1249
+ result.status = 'fail'
1250
+ const diagnostics = [
1251
+ ...(disallowedEntries.length ? [`validation mutated paths outside explicit command outputs: ${disallowedEntries.join(', ')}`] : []),
1252
+ ...(unsafeEntries.length ? [`validation created unsafe filesystem entries: ${unsafeEntries.join(', ')}`] : []),
1253
+ ]
1254
+ result.sanitizedError = sanitizeReportText(diagnostics.join('; '), reportRoots)
1255
+ workspaceTainted = true
1256
+ }
1257
+ if (targetContentFingerprint(target) !== originalFingerprint) {
1258
+ result.status = 'fail'
1259
+ result.sanitizedError = sanitizeReportText('original writable target changed during isolated validation', roots)
1260
+ workspaceTainted = true
1261
+ }
1262
+ results.push(result)
1263
+ before = after
1264
+ }
1265
+ return results
1266
+ } finally {
1267
+ fs.rmSync(tempRoot, { recursive: true, force: true })
1268
+ }
1269
+ }
1270
+
1271
+ export function runGeneratedTestStep({ step, target, timeout, roots, browserRuntimeResolver = resolveBrowserRuntime }) {
1272
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-harness-generated-test-'))
1273
+ const { env, toolReadRoots } = createMinimalValidationEnvironment(tempRoot)
1274
+ const dependencyPath = path.join(target, 'node_modules')
1275
+ const readOnlyRoots = [fs.realpathSync(target), ...(fs.existsSync(dependencyPath) ? [fs.realpathSync(dependencyPath)] : []), ...toolReadRoots]
1276
+ let execution
1277
+ let before
1278
+ let cli
1279
+ let testCounts
1280
+ try {
1281
+ const artifact = path.resolve(target, step.artifact)
1282
+ const artifactStat = fs.lstatSync(artifact)
1283
+ if (!isPathInside(fs.realpathSync(target), fs.realpathSync(artifact)) || !artifactStat.isFile() || artifactStat.isSymbolicLink()) {
1284
+ throw new Error('generated test artifact must be a regular file inside the target')
1285
+ }
1286
+ if (!GENERATED_TEST_RUNNERS.has(step.testRunner) || !['none', 'loopback'].includes(step.network)) {
1287
+ throw new Error('generated test runner contract is invalid')
1288
+ }
1289
+ if (step.testRunner === 'jest' && step.network !== 'none') throw new Error('generated Jest tests must run without network access')
1290
+ if (step.testRunner !== 'jest' && step.network !== 'loopback') throw new Error('generated Playwright tests must run with isolated loopback only')
1291
+ cli = resolveTargetTestCli(target, step.testRunner)
1292
+ if (step.testRunner === 'playwright-browser') {
1293
+ const isolatedBrowserCache = path.join(tempRoot, 'browser-cache')
1294
+ fs.mkdirSync(isolatedBrowserCache, { mode: 0o700 })
1295
+ const browser = browserRuntimeResolver(target, isolatedBrowserCache)
1296
+ readOnlyRoots.push(browser.browserRoot)
1297
+ env.PLAYWRIGHT_BROWSERS_PATH = browser.isolatedCache
1298
+ }
1299
+ if (step.testRunner !== 'jest') env.PLAYWRIGHT_JSON_OUTPUT_FILE = path.join(tempRoot, 'playwright-results.json')
1300
+ const contract = generatedTestExecutionContract({ runner: step.testRunner, artifact: step.artifact })
1301
+ if (step.executor !== contract.executor || step.cliPackage !== contract.cliPackage
1302
+ || JSON.stringify(step.argv) !== JSON.stringify(contract.argv) || step.command !== contract.command) {
1303
+ throw new Error('generated test execution differs from the fixed direct controller contract')
1304
+ }
1305
+ const runnerArgs = contract.argv.map((value) => value
1306
+ .replace('<resolved-cli>', cli)
1307
+ .replace('<isolated-temp>', tempRoot))
1308
+ before = targetSnapshot(target)
1309
+ const invocation = sandboxedInvocation({
1310
+ command: process.execPath,
1311
+ args: runnerArgs,
1312
+ cwd: target,
1313
+ writableRoots: [tempRoot],
1314
+ readOnlyRoots,
1315
+ networkMode: step.network,
1316
+ env,
1317
+ })
1318
+ execution = execute(invocation.command, invocation.args, invocation.cwd, timeout, invocation.env)
1319
+ if (execution.status === 0 && !execution.error) testCounts = generatedTestCounts(step.testRunner, tempRoot)
1320
+ } catch (error) {
1321
+ execution = { status: null, error, stdout: '', stderr: '', durationMs: 0 }
1322
+ }
1323
+ try {
1324
+ const after = targetSnapshot(target)
1325
+ const changed = before
1326
+ ? [...new Set([...before.keys(), ...after.keys()])].filter((key) => before.get(key) !== after.get(key)).sort()
1327
+ : []
1328
+ const unsafeEntries = unsafeChangedTargetEntries(after, changed)
1329
+ const result = {
1330
+ ...stepResult(step, execution, [], roots),
1331
+ ...(typeof cli === 'string' && fs.existsSync(cli) ? { cliSha256: sha256(fs.readFileSync(cli)) } : {}),
1332
+ ...(testCounts ? { testCounts } : {}),
1333
+ }
1334
+ if (unsafeEntries.length) {
1335
+ result.status = 'fail'
1336
+ result.sanitizedError = sanitizeReportText(`generated test created unsafe filesystem entries: ${unsafeEntries.join(', ')}`, roots)
1337
+ }
1338
+ return result
1339
+ } finally {
1340
+ fs.rmSync(tempRoot, { recursive: true, force: true })
1341
+ }
1342
+ }
1343
+
1344
+ function executedCoverage(plan, steps, results) {
1345
+ const deterministic = steps.find((step) => step.kind === 'deterministic')?.observedCaseIds ?? []
1346
+ const routing = plan.coverage.routing.map((entry) => {
1347
+ const executedCaseIds = sortedUnique(results.filter((result) => result.runner === entry.runner && result.evaluationKind === 'routing' && !result.sourceResult).map((result) => result.caseId))
1348
+ return { runner: entry.runner, expectedCaseIds: entry.expectedCaseIds, executedCaseIds, missingCaseIds: difference(entry.expectedCaseIds, executedCaseIds) }
1349
+ })
1350
+ const writableExecuted = sortedUnique(results.filter((result) => WRITABLE_KINDS.has(result.evaluationKind) && !result.sourceResult).map((result) => result.caseId))
1351
+ const reviewExecuted = sortedUnique(results.filter((result) => result.sourceResult).map((result) => result.caseId))
1352
+ const validationExecuted = steps.filter((step) => step.kind === 'validation' && step.status !== 'skipped').map((step) => `yarn ${step.id.slice('validation:'.length)}`)
1353
+ const targetValidationExecuted = []
1354
+ const targetValidationPassed = []
1355
+ const targetValidationFailed = []
1356
+ for (const caseId of plan.coverage.targetValidation.expectedCaseIds) {
1357
+ const caseSteps = steps.filter((step) => step.kind === 'target-validation' && step.caseId === caseId)
1358
+ if (caseSteps.length !== plan.coverage.targetValidation.commands.length || caseSteps.some((step) => step.status === 'skipped')) continue
1359
+ targetValidationExecuted.push(caseId)
1360
+ if (caseSteps.every((step) => step.status === 'pass')) targetValidationPassed.push(caseId)
1361
+ else targetValidationFailed.push(caseId)
1362
+ }
1363
+ const generatedTestExecuted = sortedUnique(steps.filter((step) => step.kind === 'generated-test' && step.status !== 'skipped').map((step) => step.caseId))
1364
+ const generatedTestPassed = sortedUnique(steps.filter((step) => step.kind === 'generated-test' && step.status === 'pass').map((step) => step.caseId))
1365
+ const generatedTestFailed = sortedUnique(steps.filter((step) => step.kind === 'generated-test' && step.status === 'fail').map((step) => step.caseId))
1366
+ return {
1367
+ deterministic: { ...plan.coverage.deterministic, executedCaseIds: deterministic, missingCaseIds: difference(plan.coverage.deterministic.expectedCaseIds, deterministic) },
1368
+ routing,
1369
+ writable: { ...plan.coverage.writable, executedCaseIds: writableExecuted, missingCaseIds: difference(plan.coverage.writable.expectedCaseIds, writableExecuted) },
1370
+ review: { ...plan.coverage.review, executedCaseIds: reviewExecuted, missingCaseIds: difference(plan.coverage.review.expectedCaseIds, reviewExecuted) },
1371
+ registry: plan.coverage.registry,
1372
+ validation: { ...plan.coverage.validation, executedCommands: validationExecuted, missingExecutedCommands: difference(plan.coverage.validation.expectedCommands, validationExecuted) },
1373
+ targetValidation: {
1374
+ ...plan.coverage.targetValidation,
1375
+ executedCaseIds: targetValidationExecuted,
1376
+ passedCaseIds: targetValidationPassed,
1377
+ failedCaseIds: targetValidationFailed,
1378
+ missingCaseIds: difference(plan.coverage.targetValidation.expectedCaseIds, targetValidationExecuted),
1379
+ },
1380
+ generatedTests: {
1381
+ ...plan.coverage.generatedTests,
1382
+ executedCaseIds: generatedTestExecuted,
1383
+ passedCaseIds: generatedTestPassed,
1384
+ failedCaseIds: generatedTestFailed,
1385
+ missingCaseIds: difference(plan.coverage.generatedTests.expectedCaseIds, generatedTestExecuted),
1386
+ },
1387
+ }
1388
+ }
1389
+
1390
+ function writeReport(root, report, roots, schema) {
1391
+ const directory = path.join(root, '.ai', 'harness', 'results')
1392
+ fs.mkdirSync(directory, { recursive: true })
1393
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
1394
+ const file = path.join(directory, `${stamp}-release-suite.json`)
1395
+ const sanitized = sanitizeObject(report, roots)
1396
+ const schemaErrors = validateJsonSchema(sanitized, schema)
1397
+ if (schemaErrors.length) throw new Error(`release report schema validation failed: ${schemaErrors.slice(0, 8).join('; ')}`)
1398
+ fs.writeFileSync(file, `${JSON.stringify(sanitized, null, 2)}\n`, { mode: 0o600 })
1399
+ return path.relative(root, file).replaceAll(path.sep, '/')
1400
+ }
1401
+
1402
+ function writeTargetValidationResult(root, target, caseId, sourceArtifact, commandSteps, generatedTestStep, schema) {
1403
+ const sourcePath = path.join(root, sourceArtifact.path)
1404
+ const targetFingerprint = targetContentFingerprint(target)
1405
+ if (targetFingerprint !== sourceArtifact.value.writable.targetFingerprint) {
1406
+ throw new Error('original writable target changed after its trusted oracle; target validation must run only in the isolated copy')
1407
+ }
1408
+ const record = {
1409
+ schemaVersion: 1,
1410
+ caseId,
1411
+ sourceResult: { path: sourceArtifact.path, sha256: sha256(fs.readFileSync(sourcePath)) },
1412
+ beforeValidationFingerprint: sourceArtifact.value.writable.targetFingerprint,
1413
+ targetFingerprint,
1414
+ commands: commandSteps.map((step) => ({
1415
+ command: step.command,
1416
+ status: step.status,
1417
+ exitStatus: step.exitStatus,
1418
+ durationMs: step.durationMs,
1419
+ })),
1420
+ generatedTests: generatedTestStep ? [{
1421
+ runner: generatedTestStep.testRunner,
1422
+ artifact: generatedTestStep.artifact,
1423
+ artifactSha256: sha256(fs.readFileSync(path.join(target, generatedTestStep.artifact))),
1424
+ executor: generatedTestStep.executor,
1425
+ cliPackage: generatedTestStep.cliPackage,
1426
+ cliSha256: generatedTestStep.cliSha256,
1427
+ argv: generatedTestStep.argv,
1428
+ command: generatedTestStep.command,
1429
+ network: generatedTestStep.network,
1430
+ testCounts: generatedTestStep.testCounts,
1431
+ status: generatedTestStep.status,
1432
+ exitStatus: generatedTestStep.exitStatus,
1433
+ durationMs: generatedTestStep.durationMs,
1434
+ }] : [],
1435
+ status: commandSteps.every((step) => step.status === 'pass') && (!generatedTestStep || generatedTestStep.status === 'pass') ? 'pass' : 'fail',
1436
+ }
1437
+ const schemaErrors = validateJsonSchema(record, schema)
1438
+ if (schemaErrors.length) throw new Error(`target validation result schema failed: ${schemaErrors.slice(0, 8).join('; ')}`)
1439
+ const directory = path.join(root, '.ai', 'harness', 'results')
1440
+ fs.mkdirSync(directory, { recursive: true })
1441
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
1442
+ const absolute = path.join(directory, `${stamp}-target-validation-${caseId}.json`)
1443
+ fs.writeFileSync(absolute, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 })
1444
+ return { absolute, relative: path.relative(root, absolute).replaceAll(path.sep, '/'), value: record }
1445
+ }
1446
+
1447
+ function initialReport(plan, startedAt) {
1448
+ return {
1449
+ schemaVersion: 1,
1450
+ status: 'fail',
1451
+ startedAt,
1452
+ finishedAt: new Date().toISOString(),
1453
+ durationMs: 0,
1454
+ runnerPolicy: plan.runnerPolicy,
1455
+ catalog: plan.catalog,
1456
+ coverage: executedCoverage(plan, [], []),
1457
+ steps: [],
1458
+ metrics: aggregateQualityMetrics([]),
1459
+ violations: plan.violations.map((entry) => entry.slice(0, VIOLATION_LIMIT)),
1460
+ }
1461
+ }
1462
+
1463
+ export function main(argv = process.argv.slice(2)) {
1464
+ const wallStarted = Date.now()
1465
+ const startedAt = new Date().toISOString()
1466
+ let options
1467
+ try { options = parseArgs(argv) } catch (error) { console.error(error.message); console.error(usage()); return 2 }
1468
+ if (options.help) { console.log(usage()); return 0 }
1469
+ let root
1470
+ try { root = fs.realpathSync(options.root) } catch { console.error('release root is unavailable'); return 2 }
1471
+ const harnessDir = path.join(root, '.ai', 'harness')
1472
+ if (!fs.existsSync(harnessDir)) { console.error(`harness directory not found under release root`); return 2 }
1473
+ let cases
1474
+ let registry
1475
+ let releaseMatrix
1476
+ let fixtures
1477
+ let seeds
1478
+ let releaseResultSchema
1479
+ let targetValidationResultSchema
1480
+ try {
1481
+ cases = readJson(path.join(harnessDir, 'cases.json'))
1482
+ registry = readJson(path.join(harnessDir, 'validators.json'))
1483
+ releaseMatrix = readJson(path.join(harnessDir, 'release-matrix.json'))
1484
+ fixtures = readJson(path.join(harnessDir, 'fixtures', 'index.json'))
1485
+ seeds = readJson(path.join(harnessDir, 'fixtures', 'seeds.json'))
1486
+ releaseResultSchema = readJson(path.join(harnessDir, 'release-result.schema.json'))
1487
+ targetValidationResultSchema = readJson(path.join(harnessDir, 'target-validation-result.schema.json'))
1488
+ } catch {
1489
+ console.error('release harness inputs are missing or invalid')
1490
+ return 2
1491
+ }
1492
+ const hostPrerequisiteViolations = releaseHostPrerequisiteViolations(releaseMatrix)
1493
+ if (hostPrerequisiteViolations.length) {
1494
+ console.error('release host prerequisites are not satisfied:')
1495
+ for (const violation of hostPrerequisiteViolations) console.error(`- ${violation}`)
1496
+ return 2
1497
+ }
1498
+ let targetsManifest
1499
+ let controllerDependencyFingerprint
1500
+ try { controllerDependencyFingerprint = dependencyContentFingerprint(root) } catch (error) {
1501
+ console.error(`controller dependency verification failed: ${error.message}`)
1502
+ return 2
1503
+ }
1504
+ if (options.writableTargets) {
1505
+ try {
1506
+ const stat = fs.lstatSync(options.writableTargets)
1507
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > RESULT_LIMIT) throw new Error('manifest must be a bounded regular file')
1508
+ targetsManifest = readJson(options.writableTargets)
1509
+ } catch (error) {
1510
+ console.error(`cannot read writable target manifest: ${error.message}`)
1511
+ return 2
1512
+ }
1513
+ } else {
1514
+ let prepareRoot
1515
+ try { prepareRoot = resolvePreparationRoot(options.prepareTargets, root) } catch (error) { console.error(error.message); return 2 }
1516
+ const writableIds = cases.filter((item) => WRITABLE_KINDS.has(item.evaluationKind)).map((item) => item.id)
1517
+ const proposedManifest = { schemaVersion: 1, targets: Object.fromEntries(writableIds.map((id) => [id, path.join(prepareRoot, id)])) }
1518
+ const preliminary = buildReleasePlan({ cases, registry, releaseMatrix, fixtures, seeds, targetsManifest: proposedManifest, root, primaryRunner: options.runner, portabilityRunner: options.portabilityRunner, targetsMayNotExist: true })
1519
+ const proposedRoots = Object.values(proposedManifest.targets)
1520
+ if (preliminary.violations.length) {
1521
+ const report = initialReport(preliminary, startedAt)
1522
+ report.durationMs = Date.now() - wallStarted
1523
+ report.finishedAt = new Date().toISOString()
1524
+ const reportPath = writeReport(root, report, [root, ...proposedRoots], releaseResultSchema)
1525
+ console.error(`Release preflight failed before target preparation; report: ${reportPath}`)
1526
+ return 1
1527
+ }
1528
+ try {
1529
+ targetsManifest = prepareWritableTargets({ root, prepareRoot, caseIds: writableIds })
1530
+ } catch (error) {
1531
+ preliminary.violations = [`automatic writable target preparation failed: ${error.message}`]
1532
+ const report = initialReport(preliminary, startedAt)
1533
+ report.durationMs = Date.now() - wallStarted
1534
+ report.finishedAt = new Date().toISOString()
1535
+ const reportPath = writeReport(root, report, [root, ...proposedRoots], releaseResultSchema)
1536
+ console.error(`Automatic target preparation failed; report: ${reportPath}`)
1537
+ return 1
1538
+ }
1539
+ }
1540
+ const plan = buildReleasePlan({ cases, registry, releaseMatrix, fixtures, seeds, targetsManifest, root, primaryRunner: options.runner, portabilityRunner: options.portabilityRunner })
1541
+ const targetRoots = Object.values(plan.targets).filter((entry) => typeof entry === 'string' && path.isAbsolute(entry)).map((entry) => path.resolve(entry))
1542
+ const roots = [root, ...targetRoots]
1543
+ if (plan.violations.length) {
1544
+ const report = initialReport(plan, startedAt)
1545
+ report.durationMs = Date.now() - wallStarted
1546
+ report.finishedAt = new Date().toISOString()
1547
+ const reportPath = writeReport(root, report, roots, releaseResultSchema)
1548
+ console.error(`Release preflight failed; report: ${reportPath}`)
1549
+ for (const violation of plan.violations) console.error(`- ${violation}`)
1550
+ return 1
1551
+ }
1552
+ console.log(`Runner policy: primary=${plan.runnerPolicy.primaryRunner}; portability=${plan.runnerPolicy.portabilityRunner ?? 'not requested'}`)
1553
+
1554
+ const evaluator = path.join(root, 'scripts', 'evaluate-agent-harness.mjs')
1555
+ const preparer = path.join(root, 'scripts', 'prepare-agent-harness-fixture.mjs')
1556
+ const sharedDependencyBefore = controllerDependencyFingerprint
1557
+ const steps = []
1558
+ const resultArtifacts = []
1559
+ const deterministicStep = plan.steps.find((step) => step.kind === 'deterministic')
1560
+ const validationSteps = plan.steps.filter((step) => step.kind === 'validation')
1561
+ const foundationTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-harness-foundation-'))
1562
+ const { env: foundationEnv } = createMinimalValidationEnvironment(foundationTempRoot)
1563
+ let deterministicResult
1564
+ try {
1565
+ const deterministicExecution = execute(
1566
+ process.execPath,
1567
+ [evaluator, '--root', root, '--all'],
1568
+ root,
1569
+ options.caseTimeout * Math.max(1, plan.catalog.caseCount) + 60_000,
1570
+ foundationEnv,
1571
+ )
1572
+ const deterministicObserved = sortedUnique((deterministicExecution.stdout ?? '').match(/^PASS (OMH-[0-9]{3})/gm)?.map((entry) => entry.slice('PASS '.length)) ?? [])
1573
+ deterministicResult = stepResult(deterministicStep, deterministicExecution, [], roots)
1574
+ deterministicResult.observedCaseIds = deterministicObserved
1575
+ if (deterministicObserved.length !== plan.catalog.caseCount) {
1576
+ deterministicResult.status = 'fail'
1577
+ deterministicResult.sanitizedError = `expected ${plan.catalog.caseCount} deterministic cases, observed ${deterministicObserved.length}`
1578
+ }
1579
+ steps.push(deterministicResult)
1580
+
1581
+ if (deterministicResult.status === 'pass') {
1582
+ steps.push(...runTargetValidationSteps({
1583
+ steps: validationSteps,
1584
+ target: root,
1585
+ timeout: options.validationTimeout,
1586
+ roots,
1587
+ allowContainedDependencies: true,
1588
+ }))
1589
+ } else {
1590
+ for (const step of validationSteps) steps.push(skippedStep(step, 'deterministic gate failed'))
1591
+ }
1592
+ } finally {
1593
+ fs.rmSync(foundationTempRoot, { recursive: true, force: true })
1594
+ }
1595
+
1596
+ const foundationsPassed = deterministicResult.status === 'pass'
1597
+ && steps.filter((step) => step.kind === 'validation').every((step) => step.status === 'pass')
1598
+ const modelSteps = plan.steps.filter((step) => ['routing', 'fixture', 'writable', 'target-validation', 'generated-test', 'review'].includes(step.kind))
1599
+ if (!foundationsPassed) {
1600
+ for (const step of modelSteps) steps.push(skippedStep(step, 'deterministic or validation foundation failed'))
1601
+ } else {
1602
+ for (const step of plan.steps.filter((entry) => entry.kind === 'routing')) {
1603
+ const before = resultFiles(root)
1604
+ const routingArgs = [evaluator, '--root', root, '--runner', step.runner]
1605
+ if (step.lane === 'primary') routingArgs.push('--all')
1606
+ routingArgs.push('--model', step.modelSelector, '--timeout', String(options.caseTimeout))
1607
+ const routingTimeout = step.expectedCaseIds.reduce(
1608
+ (total, caseId) => total + effectiveCaseTimeout(cases, caseId, options.caseTimeout),
1609
+ 60_000,
1610
+ )
1611
+ const execution = execute(process.execPath, routingArgs, root, routingTimeout)
1612
+ const artifacts = readNewResults(root, before)
1613
+ resultArtifacts.push(...artifacts)
1614
+ steps.push(stepResult(step, execution, artifacts, roots, step.expectedCaseIds.length))
1615
+ }
1616
+
1617
+ for (const caseId of plan.coverage.writable.expectedCaseIds) {
1618
+ const fixtureStep = plan.steps.find((step) => step.id === `fixture:${caseId}`)
1619
+ const writableStep = plan.steps.find((step) => step.id === `writable:${caseId}`)
1620
+ const targetValidationSteps = plan.steps.filter((step) => step.kind === 'target-validation' && step.caseId === caseId)
1621
+ const generatedTestStep = plan.steps.find((step) => step.kind === 'generated-test' && step.caseId === caseId)
1622
+ const reviewStep = plan.steps.find((step) => step.id === `review:${caseId}`)
1623
+ const target = plan.targets[caseId]
1624
+ const fixtureExecution = execute(process.execPath, [preparer, '--case', caseId, '--target', target, '--acknowledge-writes'], root, 120_000)
1625
+ const fixtureResult = stepResult(fixtureStep, fixtureExecution, [], roots)
1626
+ steps.push(fixtureResult)
1627
+ if (fixtureResult.status !== 'pass') {
1628
+ steps.push(skippedStep(writableStep, 'fixture preparation failed'))
1629
+ for (const step of targetValidationSteps) steps.push(skippedStep(step, 'writable gate did not pass'))
1630
+ if (generatedTestStep) steps.push(skippedStep(generatedTestStep, 'writable gate did not pass'))
1631
+ if (reviewStep) steps.push(skippedStep(reviewStep, 'writable gate did not pass'))
1632
+ continue
1633
+ }
1634
+ const beforeWritable = resultFiles(root)
1635
+ const caseTimeout = effectiveCaseTimeout(cases, caseId, options.caseTimeout)
1636
+ const writableExecution = execute(process.execPath, [
1637
+ evaluator, '--root', root, '--runner', writableStep.runner, '--case', caseId,
1638
+ '--model', writableStep.modelSelector, '--timeout', String(caseTimeout),
1639
+ '--writable-root', target, '--acknowledge-writes',
1640
+ ], root, caseTimeout + 120_000)
1641
+ const writableArtifacts = readNewResults(root, beforeWritable)
1642
+ resultArtifacts.push(...writableArtifacts)
1643
+ const writableResult = stepResult(writableStep, writableExecution, writableArtifacts, roots, 1)
1644
+ steps.push(writableResult)
1645
+ const sourceArtifact = writableArtifacts.find((entry) => entry.value.caseId === caseId && WRITABLE_KINDS.has(entry.value.evaluationKind))
1646
+ const targetValidationResults = runTargetValidationSteps({
1647
+ steps: targetValidationSteps,
1648
+ target,
1649
+ timeout: options.validationTimeout,
1650
+ roots,
1651
+ })
1652
+ steps.push(...targetValidationResults)
1653
+ const failedTargetCommands = targetValidationResults.filter((step) => step.status !== 'pass')
1654
+ let generatedTestResult
1655
+ if (generatedTestStep) {
1656
+ if (writableResult.status !== 'pass' || !sourceArtifact || sourceArtifact.value.status !== 'pass') {
1657
+ generatedTestResult = skippedStep(generatedTestStep, 'trusted writable oracle did not pass')
1658
+ } else if (failedTargetCommands.length) {
1659
+ generatedTestResult = skippedStep(generatedTestStep, 'target validation did not pass')
1660
+ } else {
1661
+ generatedTestResult = runGeneratedTestStep({
1662
+ step: generatedTestStep,
1663
+ target,
1664
+ timeout: options.validationTimeout,
1665
+ roots,
1666
+ })
1667
+ }
1668
+ steps.push(generatedTestResult)
1669
+ }
1670
+ if (!reviewStep) continue
1671
+ if (writableResult.status !== 'pass' || !sourceArtifact || sourceArtifact.value.status !== 'pass') {
1672
+ steps.push(skippedStep(reviewStep, 'writable gate did not pass'))
1673
+ continue
1674
+ }
1675
+ if (failedTargetCommands.length) {
1676
+ steps.push(skippedStep(reviewStep, `target validation failed: ${failedTargetCommands.map((step) => step.command).join(', ')}`))
1677
+ continue
1678
+ }
1679
+ if (generatedTestResult && generatedTestResult.status !== 'pass') {
1680
+ steps.push(skippedStep(reviewStep, 'generated test execution did not pass'))
1681
+ continue
1682
+ }
1683
+ let targetValidationArtifact
1684
+ try {
1685
+ targetValidationArtifact = writeTargetValidationResult(
1686
+ root, target, caseId, sourceArtifact, targetValidationResults, generatedTestResult, targetValidationResultSchema,
1687
+ )
1688
+ } catch (error) {
1689
+ const finalValidation = targetValidationResults.at(-1)
1690
+ finalValidation.status = 'fail'
1691
+ finalValidation.sanitizedError = sanitizeReportText(`validation attestation failed: ${error.message}`, roots)
1692
+ steps.push(skippedStep(reviewStep, 'target validation attestation failed'))
1693
+ continue
1694
+ }
1695
+ const beforeReview = resultFiles(root)
1696
+ const reviewExecution = execute(process.execPath, [
1697
+ evaluator, '--root', root, '--runner', reviewStep.runner,
1698
+ '--model', reviewStep.modelSelector, '--timeout', String(caseTimeout),
1699
+ '--review-writable-result', path.join(root, sourceArtifact.path), '--writable-root', target,
1700
+ '--review-validation-result', targetValidationArtifact.absolute,
1701
+ ], root, caseTimeout + 60_000)
1702
+ const reviewArtifacts = readNewResults(root, beforeReview)
1703
+ resultArtifacts.push(...reviewArtifacts)
1704
+ steps.push(stepResult(reviewStep, reviewExecution, reviewArtifacts, roots, 1))
1705
+ }
1706
+ }
1707
+
1708
+ if (sharedDependencyBefore) {
1709
+ let sharedDependencyAfter
1710
+ let dependencyError
1711
+ try { sharedDependencyAfter = dependencyContentFingerprint(root) } catch (error) { dependencyError = error }
1712
+ steps.push({
1713
+ id: 'shared-dependencies:unchanged',
1714
+ kind: 'dependency-guard',
1715
+ status: !dependencyError && sharedDependencyAfter === sharedDependencyBefore ? 'pass' : 'fail',
1716
+ exitStatus: null,
1717
+ durationMs: 0,
1718
+ resultPaths: [],
1719
+ ...(!dependencyError && sharedDependencyAfter === sharedDependencyBefore ? {} : {
1720
+ sanitizedError: dependencyError
1721
+ ? sanitizeReportText(`shared dependency verification failed: ${dependencyError.message}`, roots)
1722
+ : 'shared dependency tree changed during release execution; restore dependencies and rerun with fresh targets',
1723
+ }),
1724
+ })
1725
+ }
1726
+
1727
+ const values = resultArtifacts.map((entry) => entry.value)
1728
+ const coverage = executedCoverage(plan, steps, values)
1729
+ const executionViolations = steps
1730
+ .filter((step) => step.status !== 'pass')
1731
+ .map((step) => `${step.id} ${step.status}${step.sanitizedError ? `: ${step.sanitizedError}` : ''}`)
1732
+ .slice(0, 256)
1733
+ const coverageIncomplete = coverage.deterministic.missingCaseIds.length
1734
+ || coverage.routing.some((entry) => entry.missingCaseIds.length)
1735
+ || coverage.writable.missingCaseIds.length
1736
+ || coverage.review.missingCaseIds.length
1737
+ || coverage.validation.missingExecutedCommands.length
1738
+ || coverage.targetValidation.missingCaseIds.length
1739
+ || coverage.targetValidation.failedCaseIds.length
1740
+ || coverage.generatedTests.missingCaseIds.length
1741
+ || coverage.generatedTests.failedCaseIds.length
1742
+ const status = executionViolations.length === 0 && !coverageIncomplete ? 'pass' : 'fail'
1743
+ const report = {
1744
+ schemaVersion: 1,
1745
+ status,
1746
+ startedAt,
1747
+ finishedAt: new Date().toISOString(),
1748
+ durationMs: Date.now() - wallStarted,
1749
+ runnerPolicy: plan.runnerPolicy,
1750
+ catalog: plan.catalog,
1751
+ coverage,
1752
+ steps,
1753
+ metrics: aggregateQualityMetrics(values),
1754
+ violations: executionViolations.map((entry) => entry.slice(0, VIOLATION_LIMIT)),
1755
+ }
1756
+ const reportPath = writeReport(root, report, roots, releaseResultSchema)
1757
+ console.log(`${status === 'pass' ? 'PASS' : 'FAIL'} release suite — ${reportPath}`)
1758
+ return status === 'pass' ? 0 : 1
1759
+ }
1760
+
1761
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main()