@agilno-tech/rivet 0.1.0-alpha.0

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 (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,345 @@
1
+ import * as filesystem from 'node:fs';
2
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+
4
+ import { isExecutionCompatibleCwd } from '../config/commands.js';
5
+ import {discoverPortableProject} from './portable.js';
6
+
7
+ const MAX_DISCOVERY_FILE_BYTES = 256 * 1024;
8
+ const MAX_DISCOVERY_AGGREGATE_BYTES = 2 * 1024 * 1024;
9
+ const MAX_DISCOVERY_ENTRIES = 256;
10
+ const MAX_DISCOVERY_MANIFESTS = 64;
11
+ const LOCK_FILES = Object.freeze([
12
+ ['package-lock.json', 'npm'], ['npm-shrinkwrap.json', 'npm'], ['pnpm-lock.yaml', 'pnpm'],
13
+ ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'],
14
+ ]);
15
+ const IGNORED_CHILD_DIRECTORIES = new Set([
16
+ '.git', '.next', 'node_modules', 'vendor', 'dist', 'build', 'out', 'coverage', 'generated',
17
+ ]);
18
+ const OPTIONAL_FILES = Object.freeze([
19
+ 'next.config.js', 'next.config.mjs', 'next.config.ts',
20
+ 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.ts',
21
+ 'AGENTS.md', 'CLAUDE.md', 'README.md',
22
+ '.storybook/main.js', '.storybook/main.mjs', '.storybook/main.ts',
23
+ '.github/copilot-instructions.md',
24
+ ]);
25
+
26
+ export class ProjectDiscoveryError extends Error {
27
+ constructor(code, details = {}) {
28
+ super(code === 'PACKAGE_MANAGER_CONFLICT'
29
+ ? 'Conflicting package-manager evidence was detected.'
30
+ : 'Project discovery failed safely.');
31
+ this.name = 'ProjectDiscoveryError';
32
+ this.code = code;
33
+ this.details = Object.freeze({ code, ...details });
34
+ }
35
+ }
36
+
37
+ function isWithin(root, candidate) {
38
+ const path = relative(root, candidate);
39
+ return path === '' || (!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`));
40
+ }
41
+
42
+ function safeFile(root, relativePath, fs, required = false) {
43
+ const components = relativePath.split('/');
44
+ let path = root;
45
+ for (let index = 0; index < components.length; index += 1) {
46
+ path = join(path, components[index]);
47
+ const metadata = fs.lstatSync(path, { throwIfNoEntry: false });
48
+ if (!metadata) {
49
+ if (required) throw new Error(`${relativePath} must be a regular bounded file`);
50
+ return null;
51
+ }
52
+ if (metadata.isSymbolicLink()) {
53
+ if (index === components.length - 1) {
54
+ throw new Error(`${relativePath} must be a regular bounded file`);
55
+ }
56
+ throw new Error(`${relativePath} has a symbolic link ancestor in an allowlisted path`);
57
+ }
58
+ const canonical = fs.realpathSync(path);
59
+ const canonicalMetadata = fs.statSync(canonical);
60
+ if (!isWithin(root, canonical) || !sameIdentity(metadata, canonicalMetadata)) {
61
+ throw new Error(`${relativePath} changed identity outside its allowlisted path`);
62
+ }
63
+ if (index < components.length - 1) {
64
+ if (!metadata.isDirectory()) throw new Error(`${relativePath} has a non-directory allowlisted ancestor`);
65
+ continue;
66
+ }
67
+ if (!metadata.isFile() || metadata.size > MAX_DISCOVERY_FILE_BYTES) {
68
+ throw new Error(`${relativePath} must be a regular bounded file`);
69
+ }
70
+ return {
71
+ path,
72
+ relativePath,
73
+ size: metadata.size,
74
+ identity: Object.freeze({ dev: metadata.dev, ino: metadata.ino }),
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function sameIdentity(left, right) {
81
+ return left.dev === right.dev && left.ino === right.ino;
82
+ }
83
+
84
+ export function readStrictBoundedFile(file, fs = filesystem, maxBytes = MAX_DISCOVERY_FILE_BYTES) {
85
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
86
+ let descriptor;
87
+ try {
88
+ descriptor = fs.openSync(file.path, flags);
89
+ const opened = fs.fstatSync(descriptor);
90
+ if (!opened.isFile() || !sameIdentity(opened, file.identity)) {
91
+ throw new Error('Bounded file changed identity before reading');
92
+ }
93
+ const chunks = [];
94
+ let total = 0;
95
+ while (total <= maxBytes) {
96
+ const capacity = Math.min(64 * 1024, maxBytes + 1 - total);
97
+ if (capacity <= 0) break;
98
+ const buffer = Buffer.allocUnsafe(capacity);
99
+ const count = fs.readSync(descriptor, buffer, 0, capacity, null);
100
+ if (count === 0) break;
101
+ chunks.push(buffer.subarray(0, count));
102
+ total += count;
103
+ }
104
+ if (total > maxBytes) throw new Error('Bounded file exceeds the byte limit');
105
+ return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, total));
106
+ } finally {
107
+ if (descriptor !== undefined) fs.closeSync(descriptor);
108
+ }
109
+ }
110
+
111
+ export function inspectBoundedFile(root, relativePath, fs = filesystem, required = true) {
112
+ return safeFile(root, relativePath, fs, required);
113
+ }
114
+
115
+ function slug(value) {
116
+ const normalized = String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
117
+ const candidate = /^[a-z]/.test(normalized) ? normalized : `project-${normalized || 'local'}`;
118
+ return candidate.slice(0, 64).replace(/-+$/g, '');
119
+ }
120
+
121
+ function safeLockfilePresence(root, relativePath, fs) {
122
+ const components = relativePath.split('/');
123
+ let path = root;
124
+ for (let index = 0; index < components.length; index += 1) {
125
+ path = join(path, components[index]);
126
+ const metadata = fs.lstatSync(path, { throwIfNoEntry: false });
127
+ if (!metadata) return false;
128
+ if (metadata.isSymbolicLink()) throw new Error('Package-manager evidence must not use symbolic links');
129
+ const canonical = fs.realpathSync(path);
130
+ if (!isWithin(root, canonical) || !sameIdentity(metadata, fs.statSync(canonical))) {
131
+ throw new Error('Package-manager evidence changed identity');
132
+ }
133
+ if (index < components.length - 1) {
134
+ if (!metadata.isDirectory()) throw new Error('Package-manager evidence has a non-directory ancestor');
135
+ } else if (!metadata.isFile()) {
136
+ throw new Error('Package-manager evidence must be a regular file');
137
+ }
138
+ }
139
+ return true;
140
+ }
141
+
142
+ function declaredManager(value) {
143
+ if (typeof value !== 'string' || value.length > 160) return null;
144
+ const match = /^(npm|pnpm|yarn|bun)@[^\s@]{1,128}$/.exec(value);
145
+ return match?.[1] ?? null;
146
+ }
147
+
148
+ function managerEvidence(root, packages, fs, inspectedFiles) {
149
+ const evidence = [];
150
+ for (const item of packages) {
151
+ const declaration = declaredManager(item.manifest.packageManager);
152
+ if (declaration) {
153
+ evidence.push({
154
+ manager: declaration,
155
+ relativePath: item.path === '.' ? 'package.json#packageManager' : `${item.path}/package.json#packageManager`,
156
+ });
157
+ }
158
+ for (const [filename, manager] of LOCK_FILES) {
159
+ const relativePath = item.path === '.' ? filename : `${item.path}/${filename}`;
160
+ if (safeLockfilePresence(root, relativePath, fs)) {
161
+ evidence.push({ manager, relativePath });
162
+ inspectedFiles.add(relativePath);
163
+ }
164
+ }
165
+ }
166
+ const managers = [...new Set(evidence.map(item => item.manager))];
167
+ if (managers.length > 1) {
168
+ throw new ProjectDiscoveryError('PACKAGE_MANAGER_CONFLICT', { managers: managers.sort() });
169
+ }
170
+ return managers.length === 1
171
+ ? [managers[0], evidence.find(item => item.manager === managers[0]).relativePath]
172
+ : ['npm', 'package.json'];
173
+ }
174
+
175
+ function scriptsFor(manifest) {
176
+ return manifest.scripts && typeof manifest.scripts === 'object' && !Array.isArray(manifest.scripts)
177
+ ? manifest.scripts : {};
178
+ }
179
+
180
+ function scriptFor(scripts, logicalId) {
181
+ if (typeof scripts[logicalId] === 'string') return logicalId;
182
+ if (logicalId === 'typecheck' && typeof scripts['type-check'] === 'string') return 'type-check';
183
+ return null;
184
+ }
185
+
186
+ function commandRegistry(packages, manager) {
187
+ const commands = {};
188
+ const provenance = {};
189
+ const unresolved = [];
190
+ const warnings = [];
191
+ const root = packages[0];
192
+ let structured = false;
193
+ for (const key of ['build', 'test', 'lint', 'typecheck']) {
194
+ const rootScript = scriptFor(root.scripts, key);
195
+ let selected = rootScript ? [{ packagePath: '.', script: rootScript }] : packages.slice(1)
196
+ .map(item => ({ packagePath: item.path, script: scriptFor(item.scripts, key) }))
197
+ .filter(item => item.script !== null);
198
+ if (selected.length === 0 && (key === 'build' || key === 'test')) {
199
+ selected = [{ packagePath: '.', script: key, unresolved: true }];
200
+ unresolved.push({ command: key, reason: 'no-script-in-supported-scope' });
201
+ }
202
+ if (selected.length === 0) continue;
203
+ if (selected.some(item => item.packagePath !== '.')
204
+ || (key === 'typecheck' && selected.some(item => item.script === 'type-check'))) structured = true;
205
+ commands[key] = selected.map(item => ({
206
+ cwd: item.packagePath,
207
+ argv: [manager, 'run', item.script],
208
+ unresolved: item.unresolved === true,
209
+ }));
210
+ }
211
+ for (const item of packages.slice(1)) {
212
+ const hasCoverageCandidate = scriptFor(item.scripts, 'build') || scriptFor(item.scripts, 'typecheck');
213
+ if (hasCoverageCandidate && !scriptFor(item.scripts, 'test')) {
214
+ warnings.push({
215
+ code: 'package-missing-test',
216
+ package: item.path,
217
+ message: `Package '${item.path}' has build/typecheck coverage but no test script.`,
218
+ });
219
+ }
220
+ }
221
+ const serialized = {};
222
+ for (const [key, steps] of Object.entries(commands)) {
223
+ if (structured) {
224
+ serialized[key] = { steps: steps.map(({ cwd, argv }) => ({ cwd, argv })) };
225
+ steps.forEach((step, index) => {
226
+ provenance[`commands.${key}.steps[${index}]`] = step.unresolved
227
+ ? 'safe-default (script availability requires review)'
228
+ : `${step.cwd === '.' ? '' : `${step.cwd}/`}package.json#scripts.${step.argv[2]}`;
229
+ });
230
+ } else {
231
+ serialized[key] = steps[0].argv;
232
+ provenance[`commands.${key}`] = steps[0].unresolved
233
+ ? 'safe-default (script availability requires review)'
234
+ : 'package.json#scripts';
235
+ }
236
+ }
237
+ if (!structured && typeof root.scripts.dev === 'string') {
238
+ serialized.dev = [manager, 'run', 'dev'];
239
+ provenance['commands.dev'] = 'package.json#scripts.dev';
240
+ }
241
+ return { schemaVersion: structured ? 2 : 1, commands: serialized, provenance, unresolved, warnings };
242
+ }
243
+
244
+ export async function discoverProject(projectRoot, options = {}) {
245
+ const fs = options.fs ?? filesystem;
246
+ const lexicalRoot = resolve(projectRoot);
247
+ const rootMetadata = fs.lstatSync(lexicalRoot, { throwIfNoEntry: false });
248
+ if (!rootMetadata?.isDirectory() || rootMetadata.isSymbolicLink()) {
249
+ throw new Error('Project root must be a regular directory');
250
+ }
251
+ const root = fs.realpathSync(lexicalRoot);
252
+ if (!sameIdentity(rootMetadata, fs.statSync(root))) {
253
+ throw new Error('Project root changed identity during validation');
254
+ }
255
+ const manifestFile = safeFile(root, 'package.json', fs, false);
256
+ if (!manifestFile || options.checks !== undefined) return discoverPortableProject(root, {
257
+ fs, inspect: inspectBoundedFile, read: readStrictBoundedFile, checks: options.checks,
258
+ });
259
+ const files = new Map([['package.json', manifestFile]]);
260
+ for (const name of OPTIONAL_FILES) {
261
+ const file = safeFile(root, name, fs);
262
+ if (file) files.set(name, file);
263
+ }
264
+ const entries = fs.readdirSync(root);
265
+ if (!Array.isArray(entries) || entries.length > MAX_DISCOVERY_ENTRIES) {
266
+ throw new Error('Project discovery entry limit exceeded');
267
+ }
268
+ const packageFiles = [{ path: '.', file: manifestFile }];
269
+ for (const name of [...entries].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)) {
270
+ if (name.startsWith('.') || IGNORED_CHILD_DIRECTORIES.has(name) || !isExecutionCompatibleCwd(name)) continue;
271
+ const metadata = fs.lstatSync(join(root, name), { throwIfNoEntry: false });
272
+ if (!metadata || metadata.isSymbolicLink() || !metadata.isDirectory()) continue;
273
+ const canonical = fs.realpathSync(join(root, name));
274
+ if (!isWithin(root, canonical) || !sameIdentity(metadata, fs.statSync(canonical))) {
275
+ throw new Error('Child package directory changed identity');
276
+ }
277
+ const childManifest = safeFile(root, `${name}/package.json`, fs);
278
+ if (childManifest) packageFiles.push({ path: name, file: childManifest });
279
+ }
280
+ if (packageFiles.length > MAX_DISCOVERY_MANIFESTS) throw new Error('Project discovery manifest limit exceeded');
281
+ if (packageFiles.reduce((total, item) => total + item.file.size, 0) > MAX_DISCOVERY_AGGREGATE_BYTES) {
282
+ throw new Error('Project discovery aggregate read limit exceeded');
283
+ }
284
+ const packages = [];
285
+ for (const item of packageFiles) {
286
+ let parsed;
287
+ try { parsed = JSON.parse(readStrictBoundedFile(item.file, fs)); }
288
+ catch { throw new Error(`${item.path === '.' ? '' : `${item.path}/`}package.json must contain valid bounded JSON`); }
289
+ packages.push({ path: item.path, manifest: parsed, scripts: scriptsFor(parsed) });
290
+ files.set(item.path === '.' ? 'package.json' : `${item.path}/package.json`, item.file);
291
+ }
292
+ const manifest = packages[0].manifest;
293
+ const inspectedFiles = new Set(files.keys());
294
+ const [manager, managerSource] = managerEvidence(root, packages, fs, inspectedFiles);
295
+ const dependencies = Object.assign({}, ...packages.map(item => ({
296
+ ...(item.manifest.dependencies ?? {}), ...(item.manifest.devDependencies ?? {}),
297
+ })));
298
+ const framework = Object.hasOwn(dependencies, 'next') ? 'nextjs' : 'other';
299
+ const language = Object.hasOwn(dependencies, 'typescript') || files.has('next.config.ts') ? 'typescript' : 'javascript';
300
+ const registry = commandRegistry(packages, manager);
301
+ const inferredName = typeof manifest.name === 'string' && manifest.name.trim().length > 0
302
+ ? manifest.name.trim() : basename(root);
303
+ const nameSource = typeof manifest.name === 'string' && manifest.name.trim().length > 0
304
+ ? 'package.json#name' : 'project directory basename';
305
+ const storybook = Object.keys(dependencies).some(name => name === 'storybook' || name.startsWith('@storybook/'))
306
+ || [...files.keys()].some(name => name.startsWith('.storybook/'));
307
+ const playwright = Object.hasOwn(dependencies, '@playwright/test')
308
+ || [...files.keys()].some(name => name.startsWith('playwright.config.'));
309
+ const configMetadata = fs.lstatSync(join(root, '.rivet'), { throwIfNoEntry: false });
310
+ if (configMetadata?.isSymbolicLink()) throw new Error('.rivet must not be a symbolic link');
311
+ const existingConfig = configMetadata !== undefined;
312
+ return {
313
+ root,
314
+ inspectedFiles: [...inspectedFiles].sort((left, right) => left < right ? -1 : left > right ? 1 : 0),
315
+ proposal: {
316
+ schemaVersion: registry.schemaVersion,
317
+ id: slug(inferredName),
318
+ name: inferredName.slice(0, 120),
319
+ stack: { framework, language, packageManager: manager },
320
+ commands: registry.commands,
321
+ },
322
+ features: { storybook, playwright },
323
+ architectureHints: files.has('AGENTS.md') ? ['AGENTS.md'] : [],
324
+ existingConfig,
325
+ warnings: registry.warnings,
326
+ unresolved: registry.unresolved,
327
+ provenance: {
328
+ id: nameSource,
329
+ name: nameSource,
330
+ 'stack.framework': 'package.json#dependencies',
331
+ 'stack.language': 'package.json#dependencies',
332
+ 'stack.packageManager': managerSource,
333
+ 'features.storybook': storybook ? 'known Storybook dependency/config filename' : 'known-file allowlist',
334
+ 'features.playwright': playwright ? 'known Playwright dependency/config filename' : 'known-file allowlist',
335
+ ...registry.provenance,
336
+ },
337
+ };
338
+ }
339
+
340
+ export {
341
+ MAX_DISCOVERY_AGGREGATE_BYTES,
342
+ MAX_DISCOVERY_ENTRIES,
343
+ MAX_DISCOVERY_FILE_BYTES,
344
+ MAX_DISCOVERY_MANIFESTS,
345
+ };
@@ -0,0 +1,145 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export const SUPPORTED_NODE_MAJOR = 22;
4
+ export const RECOMMENDED_NODE_MAJOR = 22;
5
+ export const DEFAULT_COMMAND_TIMEOUT_MS = 3_000;
6
+ export const DEFAULT_COMMAND_OUTPUT_BYTES = 16 * 1024;
7
+
8
+ function safeResult(result = {}) {
9
+ return {
10
+ code: Number.isInteger(result.code) ? result.code : 1,
11
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
12
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
13
+ timedOut: result.timedOut === true,
14
+ truncated: {
15
+ stdout: result.truncated?.stdout === true,
16
+ stderr: result.truncated?.stderr === true,
17
+ },
18
+ };
19
+ }
20
+
21
+ function decodeValidUtf8Prefix(buffer) {
22
+ const decoder = new TextDecoder('utf-8', { fatal: true });
23
+ for (let length = buffer.byteLength; length >= 0; length -= 1) {
24
+ try {
25
+ return decoder.decode(buffer.subarray(0, length));
26
+ } catch {}
27
+ }
28
+ return '';
29
+ }
30
+
31
+ export function runArgv(command, args, options = {}) {
32
+ if (typeof command !== 'string' || !Array.isArray(args) || args.some(value => typeof value !== 'string')) {
33
+ throw new TypeError('Command execution requires an executable and argv array');
34
+ }
35
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
36
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_COMMAND_OUTPUT_BYTES;
37
+ const maxStreamOutputBytes = options.maxStreamOutputBytes ?? maxOutputBytes;
38
+ return new Promise(resolvePromise => {
39
+ const output = { stdout: [], stderr: [] };
40
+ const streamBytes = { stdout: 0, stderr: 0 };
41
+ const truncated = { stdout: false, stderr: false };
42
+ let totalBytes = 0;
43
+ let settled = false;
44
+ let timedOut = false;
45
+ const child = spawn(command, args, {
46
+ cwd: options.cwd,
47
+ env: options.env,
48
+ shell: false,
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ windowsHide: true,
51
+ });
52
+ const finish = result => {
53
+ if (settled) return;
54
+ settled = true;
55
+ clearTimeout(timer);
56
+ resolvePromise(safeResult({
57
+ ...result,
58
+ stdout: decodeValidUtf8Prefix(Buffer.concat(output.stdout)),
59
+ stderr: decodeValidUtf8Prefix(Buffer.concat(output.stderr)),
60
+ timedOut,
61
+ truncated,
62
+ }));
63
+ };
64
+ const collect = stream => chunk => {
65
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
66
+ const available = Math.max(0, Math.min(
67
+ maxStreamOutputBytes - streamBytes[stream],
68
+ maxOutputBytes - totalBytes,
69
+ ));
70
+ const accepted = buffer.subarray(0, available);
71
+ if (accepted.byteLength > 0) output[stream].push(accepted);
72
+ streamBytes[stream] += accepted.byteLength;
73
+ totalBytes += accepted.byteLength;
74
+ if (accepted.byteLength < buffer.byteLength) truncated[stream] = true;
75
+ };
76
+ child.stdout?.on('data', collect('stdout'));
77
+ child.stderr?.on('data', collect('stderr'));
78
+ child.once('error', () => finish({ code: 1, stdout: '' }));
79
+ child.once('close', code => finish({ code: code ?? 1, stdout: output }));
80
+ const timer = setTimeout(() => {
81
+ timedOut = true;
82
+ child.kill('SIGKILL');
83
+ }, timeoutMs);
84
+ timer.unref?.();
85
+ });
86
+ }
87
+
88
+ function normalizeVersion(stdout) {
89
+ const match = String(stdout).match(/(?:^|\s)v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
90
+ return match ? `${match[1]}.${match[2] ?? '0'}.${match[3] ?? '0'}` : null;
91
+ }
92
+
93
+ async function inspect(executable, args, runner, cwd) {
94
+ try {
95
+ const result = safeResult(await runner(executable, args, {
96
+ cwd,
97
+ shell: false,
98
+ timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
99
+ maxOutputBytes: DEFAULT_COMMAND_OUTPUT_BYTES,
100
+ }));
101
+ const version = result.code === 0 ? normalizeVersion(result.stdout) : null;
102
+ return { present: result.code === 0 && version !== null, version };
103
+ } catch {
104
+ return { present: false, version: null };
105
+ }
106
+ }
107
+
108
+ export async function discoverTools(context = {}, options = {}) {
109
+ const runner = options.runner ?? runArgv;
110
+ const cwd = options.cwd;
111
+ const packageManager = context.packageManager ?? 'npm';
112
+ const platform = options.platform ?? process.platform;
113
+ const managerExecutable = platform === 'win32'
114
+ ? ({ npm: 'npm.cmd', pnpm: 'pnpm.cmd', yarn: 'yarn.cmd', bun: 'bun.exe' }[packageManager] ?? packageManager)
115
+ : packageManager;
116
+ const [node, manager, git] = await Promise.all([
117
+ inspect('node', ['--version'], runner, cwd),
118
+ context.runtimeOnly === true ? Promise.resolve(null) : inspect(managerExecutable, ['--version'], runner, cwd),
119
+ inspect('git', ['--version'], runner, cwd),
120
+ ]);
121
+ const nodeMajor = node.version ? Number.parseInt(node.version, 10) : null;
122
+ node.supported = node.present && nodeMajor >= SUPPORTED_NODE_MAJOR;
123
+ node.compatible = node.supported;
124
+ node.recommended = node.present && nodeMajor >= RECOMMENDED_NODE_MAJOR;
125
+ if(manager) {manager.supported = manager.present;manager.compatible = manager.supported;}
126
+ git.supported = git.present;
127
+ git.compatible = git.supported;
128
+ return {
129
+ node,
130
+ ...(manager?{[packageManager]: manager}:{}),
131
+ git,
132
+ playwright: {
133
+ relevant: context.playwright === true,
134
+ available: context.playwright === true && manager?.present === true,
135
+ },
136
+ storybook: {
137
+ relevant: context.storybook === true,
138
+ available: context.storybook === true && manager?.present === true,
139
+ },
140
+ thresholds: {
141
+ nodeSupportedMajor: SUPPORTED_NODE_MAJOR,
142
+ nodeRecommendedMajor: RECOMMENDED_NODE_MAJOR,
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,20 @@
1
+ import { LiveEvaluationError } from './cost-policy.js';
2
+
3
+ // A UI callback may ignore cancellation or never settle. Stop waiting on the
4
+ // runner's signal independently; a late answer cannot resume this operation.
5
+ export function waitForApproval(confirm, proposal, signal) {
6
+ return new Promise((resolve,reject)=>{
7
+ let settled=false;
8
+ const finish=(error,value)=>{
9
+ if(settled)return;settled=true;signal.removeEventListener('abort',abort);
10
+ if(error)reject(error);else resolve(value);
11
+ };
12
+ const abort=()=>finish(new LiveEvaluationError('cancelled'));
13
+ if(signal.aborted){abort();return;}
14
+ signal.addEventListener('abort',abort,{once:true});
15
+ Promise.resolve().then(()=>{
16
+ if(signal.aborted)throw new LiveEvaluationError('cancelled');
17
+ return confirm(proposal,{signal});
18
+ }).then(value=>finish(null,value),error=>finish(error));
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ export class LiveEvaluationError extends Error {
2
+ constructor(reason) { super(`Live evaluation stopped: ${reason}.`); this.code=`ERR_LIVE_EVAL_${reason.replaceAll('-','_').toUpperCase()}`; this.reason=reason; }
3
+ }
4
+ export function ensure(value,reason='invalid-profile') { if(!value)throw new LiveEvaluationError(reason); }
5
+ export function capture(value,allowed) {
6
+ ensure(value && typeof value==='object' && !Array.isArray(value) && [Object.prototype,null].includes(Object.getPrototypeOf(value)));
7
+ const result={};
8
+ for(const key of Reflect.ownKeys(value)) {
9
+ const descriptor=Object.getOwnPropertyDescriptor(value,key);
10
+ ensure(typeof key==='string' && allowed.includes(key) && descriptor?.enumerable && Object.hasOwn(descriptor,'value'));
11
+ result[key]=descriptor.value;
12
+ }
13
+ return result;
14
+ }
15
+ export function resolveCostPolicy(input,provider) {
16
+ const value=capture(input,['kind','accountPolicy','estimatedCostUsd']);
17
+ ensure((value.kind==='account-policy'||(value.kind==='local-compute'&&provider==='ollama')) && typeof value.accountPolicy==='string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(value.accountPolicy),'account-policy-required');
18
+ ensure(value.estimatedCostUsd===undefined||value.estimatedCostUsd===null||(Number.isFinite(value.estimatedCostUsd) && value.estimatedCostUsd>0 && value.estimatedCostUsd<=100),'invalid-cost-estimate');
19
+ return Object.freeze({...value,estimatedCostUsd:value.estimatedCostUsd??null,estimateIsLimit:false,hardDollarCapEnforced:false,providerAdvertisedBudgetsUsd:provider==='claude'?Object.freeze({planning:1,worker:2}):null,actualCostUsd:null});
20
+ }
@@ -0,0 +1,113 @@
1
+ import { mkdtemp,mkdir,writeFile,realpath } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname,join,isAbsolute,delimiter } from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { createGitClient } from '../git/client.js';
7
+ import { createFeatureWorkflow } from '../feature/workflow.js';
8
+ import { createFeatureExecutor } from '../feature/runtime-bridge.js';
9
+ import { createAcceptedIntegrationStore } from '../feature/accepted-integration.js';
10
+ import { createVerificationReportStore } from '../feature/verification-report.js';
11
+ import { resolveFeatureRunPaths,acceptedIntegrationPaths,verificationReportPaths } from '../state/paths.js';
12
+ import { discoverHarnesses } from '../runtime/harness-discovery.js';
13
+ import { createClaudeClient,createClaudePlanningClient,CLAUDE_ADAPTER_SYNTAX } from '../clients/claude.js';
14
+ import { createCodexClient,createCodexPlanningClient,CODEX_ADAPTER_SYNTAX } from '../clients/codex.js';
15
+ import { runPublicationProcess } from '../delivery/publication-process.js';
16
+ import { ensure,LiveEvaluationError } from './cost-policy.js';
17
+ import { waitForApproval } from './approval.js';
18
+
19
+ async function command(executable,args,cwd,env,signal,timeoutMs=15000) {
20
+ const result=await runPublicationProcess(executable,args,{cwd,env,signal,timeoutMs,maxOutputBytes:262144});
21
+ ensure(result.code===0&&!result.reason,'fixture-command-failed');return result.stdout.trim();
22
+ }
23
+ async function createFixture(scenario,profile,signal) {
24
+ const workspace=await realpath(await mkdtemp(join(tmpdir(),'rivet-live-eval-')));
25
+ const root=join(workspace,'project'),remote=join(workspace,'origin.git'),empty=join(workspace,'empty-template');
26
+ const env={PATH:`${dirname(process.execPath)}:/usr/bin:/bin`,HOME:workspace,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',GIT_NO_REPLACE_OBJECTS:'1',GIT_TERMINAL_PROMPT:'0',NPM_CONFIG_USERCONFIG:join(workspace,'user.npmrc'),NPM_CONFIG_GLOBALCONFIG:join(workspace,'global.npmrc'),NPM_CONFIG_CACHE:join(workspace,'cache'),NPM_CONFIG_OFFLINE:'true',NPM_CONFIG_IGNORE_SCRIPTS:'true'};
27
+ await mkdir(root);await mkdir(empty);await mkdir(join(root,'.rivet'));
28
+ for(const [path,content]of Object.entries(scenario.files)){await mkdir(dirname(join(root,path)),{recursive:true});await writeFile(join(root,path),content,{flag:'wx'});}
29
+ await writeFile(join(root,'package.json'),JSON.stringify({name:'rivet-evaluation-fixture',private:true,type:'module',scripts:{build:'node --check src/solution.js',test:'node public-check.mjs'}}));
30
+ const config={};
31
+ for(const name of ['project','providers','orchestration','quality'])config[name]=YAML.parse(scenario.configurationTemplates[name]);
32
+ config.project.id='live-evaluation';config.project.name='Live evaluation';config.project.stack={framework:'node',language:'javascript',packageManager:'npm'};
33
+ config.project.repository.defaultBranch='main';config.project.repository.sensitivePaths=['package.json','public-check.mjs','request.md'];config.project.commands={build:['npm','run','build'],test:['npm','run','test']};
34
+ config.orchestration.roles.forEach(role=>{role.budget={timeMinutes:5,tokenLimit:20000,costUsd:3,taskLimit:1};role.capacity=1});
35
+ for(const [name,value]of Object.entries(config))await writeFile(join(root,'.rivet',`${name}.yaml`),YAML.stringify(value));
36
+ const git=(...args)=>command('/usr/bin/git',args,root,env,signal);
37
+ await git('init','--quiet',`--template=${empty}`,'--initial-branch=main');await git('config','user.name','Rivet evaluation');await git('config','user.email','evaluation@example.invalid');
38
+ await git('add','.');await git('commit','--quiet','-m','Fixed evaluation baseline');
39
+ await git('init','--quiet','--bare',`--template=${empty}`,remote);await git('remote','add','origin',remote);await git('push','--quiet','-u','origin','main');
40
+ const baseline=await git('rev-parse','HEAD'),remoteBefore=await git('ls-remote','--refs','origin');
41
+ return {workspace,root,remote,env,git,baseline,remoteBefore};
42
+ }
43
+ async function installedClients(profile,root,environment,signal) {
44
+ const selected=(await discoverHarnesses({env:environment,projectRoot:root,signal})).find(item=>item.kind===profile.id&&item.executable);
45
+ ensure(selected,'harness-unavailable');
46
+ const clientEnvironment=Object.fromEntries(['PATH','LANG','LC_ALL','TZ','TERM','TMPDIR','HOME','USER','LOGNAME','SHELL'].filter(key=>typeof environment[key]==='string').map(key=>[key,environment[key]]));
47
+ const settings={executable:selected.executable,...(selected.interpreter?{interpreter:selected.interpreter}:{}),expectedVersion:selected.version,environment:clientEnvironment,maxOutputBytes:profile.limits.maxOutputBytes};
48
+ const planning=(profile.id==='claude'?createClaudePlanningClient:createCodexPlanningClient)({...settings,worktree:root,timeoutMs:Math.min(profile.limits.timeoutMs,profile.limits.planningTimeoutMs)});
49
+ const args=profile.id==='claude'?[...CLAUDE_ADAPTER_SYNTAX.args.slice(0,-1),'--model','sonnet','--effort','low','--permission-mode','acceptEdits','{stdin}']:[...CODEX_ADAPTER_SYNTAX.args.slice(0,-1),'--sandbox','workspace-write',...(profile.models.worker==='harness-default'?[]:['--model',profile.models.worker]),'{stdin}'];
50
+ const worker=(profile.id==='claude'?createClaudeClient:createCodexClient)({...settings,args,timeoutMs:profile.limits.timeoutMs});
51
+ return {planning,worker,version:selected.version};
52
+ }
53
+ function acceptanceProgram(scenario,path) {
54
+ const checks=scenario.id==='feature'?[
55
+ `["normalization",()=>assert.deepEqual(solution.normalizeTags([' Foo ','BAR']),['foo','bar'])]`,
56
+ `["stable-deduplication",()=>assert.deepEqual(solution.normalizeTags([' B ','a','b','',' A ',' ']),['b','a'])]`,
57
+ `["empty-input",()=>assert.deepEqual(solution.normalizeTags([]),[])]`,
58
+ `["input-preservation",()=>{const input=[' A ','a'];solution.normalizeTags(input);assert.deepEqual(input,[' A ','a'])}]`,
59
+ ]:[
60
+ `["inclusive-boundaries",()=>{assert.equal(solution.countInRange([1,3],1,3),2);assert.equal(solution.countInRange([2,2,3],2,2),2)}]`,
61
+ `["duplicate-matches",()=>assert.equal(solution.countInRange([0,1,1,2,3,4],1,3),4)]`,
62
+ `["finite-numbers",()=>assert.equal(solution.countInRange(['2',NaN,Infinity,-Infinity,2],-Infinity,Infinity),1)]`,
63
+ `["input-preservation",()=>{const input=[3,1,2];assert.equal(solution.countInRange([],1,3),0);solution.countInRange(input,1,3);assert.deepEqual(input,[3,1,2])}]`,
64
+ ];
65
+ return `const assert=require('node:assert/strict');const {pathToFileURL}=require('node:url');(async()=>{const solution=await import(pathToFileURL(${JSON.stringify(join(path,'src/solution.js'))}).href);const checks=[${checks.join(',')}].map(([id,check])=>{try{check();return{id,passed:true}}catch{return{id,passed:false}}});process.stdout.write(JSON.stringify(checks))})().catch(()=>process.exitCode=1);`;
66
+ }
67
+ export async function runHarnessAttempt({profile,scenario,environment,signal,confirmActivation,clients,assertCurrent=()=>{}}) {
68
+ ensure(typeof confirmActivation==='function','activation-approval-required');
69
+ const fixture=await createFixture(scenario,profile,signal);
70
+ const evidence={workspace:fixture.workspace,baselineCommit:fixture.baseline,sourcePreserved:null,remotePreserved:null,changedPaths:[],modelCalls:{planning:0,worker:0}};
71
+ let usage=null,harness=null,gates=[],acceptance={total:scenario.acceptanceIds.length,passed:null,checks:[]};
72
+ const check=()=>{ensure(!signal.aborted,'cancelled');assertCurrent()};
73
+ try {
74
+ check();const selected=clients??await installedClients(profile,fixture.root,environment,signal);
75
+ ensure(selected.planning&&typeof selected.planning.propose==='function'&&selected.worker&&selected.worker.provider===profile.id&&typeof selected.worker.launch==='function','invalid-clients');
76
+ harness={name:profile.id,version:selected.version??null};
77
+ let gitClient;
78
+ for(const directory of String(environment.PATH??'').split(delimiter).filter(isAbsolute)) {
79
+ try{gitClient=await createGitClient({gitExecutable:await realpath(join(directory,'git'))});break}catch{}
80
+ }
81
+ ensure(gitClient,'git-unavailable');
82
+ const npm=await realpath(join(dirname(process.execPath),'npm'));
83
+ const executor=createFeatureExecutor({gitClient,now:()=>new Date().toISOString(),environment:fixture.env,resolveCommandExecutable:async runner=>{ensure(runner==='npm');return npm},clientFor:async()=>({provider:profile.id,async launch(contract,options){check();ensure(evidence.modelCalls.worker===0,'automatic-retry-blocked');evidence.modelCalls.worker++;
84
+ const result=await selected.worker.launch(contract,options);usage=result.usage??null;return result;
85
+ }})});
86
+ const workflow=createFeatureWorkflow({gitClient,protocolsFor:async()=>[],planningClientFor:async()=>({async propose(contract,options){check();ensure(evidence.modelCalls.planning===0,'automatic-retry-blocked');evidence.modelCalls.planning++;return selected.planning.propose(contract,options)}}),executeFeature:executor});
87
+ const proposed=await workflow.propose({project:fixture.root,source:{kind:'file',value:join(fixture.root,'request.md')},client:profile.id},{signal});
88
+ check();const workers=proposed.featurePlan.nodes.filter(node=>node.role==='worker');
89
+ ensure(workers.length===1&&workers[0].ownedPaths.length===1&&workers[0].ownedPaths[0]==='src/solution.js','proposal-outside-fixture-scope');
90
+ ensure(await waitForApproval(confirmActivation,Object.freeze({scenario:scenario.id,runId:proposed.runId,proposalDigest:proposed.proposalDigest,plan:proposed.featurePlan,workspace:fixture.workspace}),signal)===true,'activation-declined');check();
91
+ const approved=await workflow.start({project:fixture.root,runId:proposed.runId,expectedVersion:proposed.version,proposalDigest:proposed.proposalDigest});
92
+ const outcome=await workflow.resume({project:fixture.root,runId:proposed.runId,expectedVersion:approved.version},{signal,confirmDependencyInstall:async()=>false});
93
+ evidence.runId=proposed.runId;evidence.workflowStatus=outcome.status;
94
+ const paths=await resolveFeatureRunPaths(fixture.root,proposed.runId);
95
+ const accepted=await createAcceptedIntegrationStore(await acceptedIntegrationPaths(paths)).readOnly();
96
+ const report=await createVerificationReportStore(await verificationReportPaths(paths)).readOnly();
97
+ if(report)evidence.verificationFailure=report.failure;
98
+ if(report)gates=report.checks.map(({id,status,executionStatus,exitCode})=>({id,status,executionStatus,exitCode}));
99
+ if(accepted){evidence.acceptedCommit=accepted.commitSha;evidence.integrationPath=accepted.path;evidence.changedPaths=await gitClient.changedPaths(accepted.path,fixture.baseline,accepted.commitSha);
100
+ evidence.diffSha256=createHash('sha256').update(await fixture.git('diff','--no-ext-diff','--no-textconv',fixture.baseline,accepted.commitSha,'--')).digest('hex');
101
+ check();const result=await runPublicationProcess(process.execPath,['-e',acceptanceProgram(scenario,accepted.path)],{cwd:accepted.path,env:fixture.env,signal,timeoutMs:5000,maxOutputBytes:16384});
102
+ if(result.code===0&&!result.reason){let checks;try{checks=JSON.parse(result.stdout)}catch{};
103
+ if(Array.isArray(checks)&&checks.length===scenario.acceptanceIds.length&&checks.every((item,index)=>item.id===scenario.acceptanceIds[index]&&typeof item.passed==='boolean'))acceptance={total:checks.length,passed:checks.filter(item=>item.passed).length,checks};
104
+ }
105
+ evidence.acceptanceCleanup=result.cleanup;
106
+ const after=await gitClient.inspectRepository(accepted.path);ensure(!after.dirty&&after.headSha===accepted.commitSha,'integration-changed-during-acceptance');
107
+ }
108
+ evidence.sourcePreserved=(await fixture.git('rev-parse','HEAD'))===fixture.baseline&&(await fixture.git('status','--porcelain'))==='';
109
+ evidence.remotePreserved=(await fixture.git('ls-remote','--refs','origin'))===fixture.remoteBefore;
110
+ const passed=outcome.status==='awaiting-final-approval'&&gates.length===2&&gates.every(item=>item.status==='passed')&&acceptance.passed===acceptance.total&&evidence.sourcePreserved&&evidence.remotePreserved&&JSON.stringify(evidence.changedPaths)==='["src/solution.js"]';
111
+ return {outcome:passed?'passed':'failed',harness,usage:{provenance:usage?'agent-result-self-report':'not-observed',value:usage},gates,acceptance,evidence};
112
+ }catch(error){return {outcome:signal.aborted?'cancelled':'failed',reason:error instanceof LiveEvaluationError?error.reason:typeof error.code==='string'?error.code:'harness-attempt-failed',harness,usage:{provenance:usage?'agent-result-self-report':'not-observed',value:usage},gates,acceptance,evidence};}
113
+ }