@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,353 @@
1
+ import { isExecutionCompatibleCwd, isProjectExecutable } from '../config/commands.js';
2
+ import { createTrackerProviderFactory } from '../adapters/factory.js';
3
+ import { createNodeProviderTransport } from '../adapters/node-transport.js';
4
+ import * as filesystem from 'node:fs';
5
+ import { spawn as nodeSpawn } from 'node:child_process';
6
+ import { lstat as lstatFile, realpath as realpathFile } from 'node:fs/promises';
7
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
8
+
9
+ import {
10
+ createClaudeClient,
11
+ createClaudePlanningClient,
12
+ CLAUDE_ADAPTER_SYNTAX,
13
+ } from '../clients/claude.js';
14
+ import {
15
+ createCodexClient,
16
+ createCodexPlanningClient,
17
+ CODEX_ADAPTER_SYNTAX,
18
+ } from '../clients/codex.js';
19
+ import {
20
+ CLAUDE_FEATURE_PROFILE,
21
+ FEATURE_PLANNING_TIMEOUT_MS,
22
+ clientProfileFor,
23
+ matchesClientProfile,
24
+ } from '../feature/client-profile.js';
25
+ import { collectEvidenceBundle } from '../evidence/collect.js';
26
+ import { createFeatureRunStore } from '../feature/run-store.js';
27
+ import { createFeatureExecutor } from '../feature/runtime-bridge.js';
28
+ import { createHostExecution } from '../feature/host-execution.js';
29
+ import { discoverHarnesses } from './harness-discovery.js';
30
+ import { createFeatureWorkflow } from '../feature/workflow.js';
31
+ import { createGitClient } from '../git/client.js';
32
+ import { createReservedWorktree } from '../git/worktrees.js';
33
+ import { buildLaunchContract } from '../prompts/launch-contract.js';
34
+ import { runQualityGates } from '../quality/runner.js';
35
+ import { resolveFeatureRunPaths } from '../state/paths.js';
36
+ import { createOrchestrator } from './orchestrator.js';
37
+
38
+ const INPUT_KEYS = new Set(['cwd', 'env', 'fs', 'fetch', 'spawn', 'now', 'featureWorkflow', 'providerTransport']);
39
+ const FEATURE_WORKFLOW_METHODS = Object.freeze(['propose', 'start', 'watch', 'status', 'resume', 'cancel']);
40
+
41
+ export class ApplicationConfigurationError extends Error {
42
+ constructor(reason = 'configuration') {
43
+ const messages = {
44
+ configuration: 'Rivet runtime configuration is missing or invalid.',
45
+ 'command-executable-unsafe': 'A configured project executable path is unsafe. Keep its working directory and executable ancestors inside the isolated checkout without directory symlinks.',
46
+ 'command-executable-unavailable': 'A configured quality command executable is unavailable. Activate the project environment, install the required tool on PATH, or correct its RIVET_<TOOL>_EXECUTABLE override, then run rivet doctor.',
47
+ 'git-unavailable': 'No usable Git executable was found. Install Git on PATH or set RIVET_GIT_EXECUTABLE to its absolute canonical executable path.',
48
+ 'git-executable-invalid': 'RIVET_GIT_EXECUTABLE must be an absolute canonical path to a Git executable. Correct this setting and retry.',
49
+ 'git-executable-unusable': 'RIVET_GIT_EXECUTABLE does not identify a usable regular Git executable. Check that the path is canonical and the file exists with execute permission, then retry.',
50
+ };
51
+ const selectedReason = Object.hasOwn(messages, reason) ? reason : 'configuration';
52
+ super(messages[selectedReason]);
53
+ this.name = 'ApplicationConfigurationError';
54
+ this.code = 'ERR_APPLICATION_CONFIGURATION';
55
+ this.safeMessage = this.message;
56
+ this.details = Object.freeze({ reason: selectedReason });
57
+ }
58
+ }
59
+
60
+ function fail(reason) { throw new ApplicationConfigurationError(reason); }
61
+
62
+ function capture(input) {
63
+ try {
64
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail();
65
+ const keys = Reflect.ownKeys(input);
66
+ if (keys.length > INPUT_KEYS.size || keys.some(key => typeof key !== 'string' || !INPUT_KEYS.has(key))) fail();
67
+ const output = Object.create(null);
68
+ for (const key of keys) {
69
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
70
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail();
71
+ output[key] = descriptor.value;
72
+ }
73
+ return output;
74
+ } catch (error) {
75
+ if (error instanceof ApplicationConfigurationError) throw error;
76
+ fail();
77
+ }
78
+ }
79
+
80
+ function environment(input) {
81
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail();
82
+ const output = Object.create(null);
83
+ let keys;
84
+ try { keys = Reflect.ownKeys(input); } catch { fail(); }
85
+ if (keys.length > 512 || keys.some(key => typeof key !== 'string')) fail();
86
+ for (const key of keys) {
87
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
88
+ if (!descriptor?.enumerable || typeof descriptor.value !== 'string' || descriptor.value.includes('\0')) fail();
89
+ output[key] = descriptor.value;
90
+ }
91
+ return Object.freeze(output);
92
+ }
93
+
94
+ function executable(value) {
95
+ if (typeof value !== 'string' || value.length < 2 || value.length > 1024 || !isAbsolute(value)
96
+ || /[\u0000\r\n]/.test(value)) fail();
97
+ return value;
98
+ }
99
+
100
+ function executableCandidates(runner, pathValue) {
101
+ const directories = typeof pathValue === 'string' ? pathValue.split(':').slice(0, 128) : [];
102
+ return [
103
+ ...directories.filter(directory => directory.length > 1 && directory.length <= 1024
104
+ && isAbsolute(directory) && resolve(directory) === directory && !/[\u0000\r\n]/.test(directory))
105
+ .map(directory => join(directory, runner)),
106
+ `/opt/homebrew/bin/${runner}`, `/usr/local/bin/${runner}`, `/usr/bin/${runner}`,
107
+ ];
108
+ }
109
+
110
+ function captureFeatureWorkflow(value) {
111
+ if (!value || typeof value !== 'object' || Array.isArray(value)
112
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail();
113
+ const keys = Reflect.ownKeys(value);
114
+ if (keys.length !== FEATURE_WORKFLOW_METHODS.length
115
+ || keys.some(key => typeof key !== 'string' || !FEATURE_WORKFLOW_METHODS.includes(key))) fail();
116
+ const output = {};
117
+ for (const method of FEATURE_WORKFLOW_METHODS) {
118
+ const descriptor = Object.getOwnPropertyDescriptor(value, method);
119
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value') || typeof descriptor.value !== 'function') fail();
120
+ output[method] = descriptor.value;
121
+ }
122
+ return Object.freeze(output);
123
+ }
124
+
125
+ export function createRivetApplication(input = {}) {
126
+ const configured = capture(input);
127
+ const cwd = configured.cwd ?? (() => process.cwd());
128
+ const env = environment(configured.env ?? process.env);
129
+ const fs = configured.fs ?? filesystem;
130
+ const fetch = configured.fetch ?? globalThis.fetch;
131
+ const spawn = configured.spawn ?? nodeSpawn;
132
+ const now = configured.now ?? (() => new Date().toISOString());
133
+ if (typeof cwd !== 'function' || !fs || typeof fs !== 'object' || typeof fetch !== 'function'
134
+ || typeof spawn !== 'function' || typeof now !== 'function') fail();
135
+ let gitClientPromise;
136
+ const gitClient = () => {
137
+ gitClientPromise ??= (async () => {
138
+ if (env.RIVET_GIT_EXECUTABLE !== undefined) {
139
+ let configuredGit;
140
+ try {
141
+ configuredGit = executable(env.RIVET_GIT_EXECUTABLE);
142
+ if (resolve(configuredGit) !== configuredGit) fail();
143
+ } catch { fail('git-executable-invalid'); }
144
+ try { return await createGitClient({ gitExecutable: configuredGit }); }
145
+ catch { fail('git-executable-unusable'); }
146
+ }
147
+ for (const candidate of executableCandidates('git', env.PATH)) {
148
+ try { return await createGitClient({ gitExecutable: executable(await realpathFile(candidate)) }); } catch {}
149
+ }
150
+ fail('git-unavailable');
151
+ })();
152
+ return gitClientPromise;
153
+ };
154
+ let workflowPromise;
155
+ let executorPromise;
156
+ let hostExecutionPromise;
157
+ const selectedHarnesses = new Map();
158
+ const selectedVersion = kind => selectedHarnesses.get(kind)?.version;
159
+ const harnessSettings = kind => {
160
+ const selected = selectedHarnesses.get(kind);
161
+ return selected ? {
162
+ executable: selected.executable,
163
+ ...(selected.interpreter ? { interpreter: selected.interpreter } : {}),
164
+ } : {
165
+ executable: executable(env[`RIVET_${kind.toUpperCase()}_EXECUTABLE`]),
166
+ ...clientInterpreter(kind),
167
+ };
168
+ };
169
+ const harnesses = Object.freeze({
170
+ discover(projectRoot, { signal } = {}) { return discoverHarnesses({ env, projectRoot, signal }); },
171
+ async select(kind, projectRoot, { signal } = {}) {
172
+ if (!['claude', 'codex'].includes(kind)) fail();
173
+ const found = await discoverHarnesses({ env, projectRoot, signal });
174
+ const selected = found.find(item => item.kind === kind && item.executable);
175
+ if (!selected) fail();
176
+ selectedHarnesses.set(kind, selected);
177
+ return selected;
178
+ },
179
+ });
180
+ const clientEnvironment = Object.freeze(Object.fromEntries(
181
+ ['PATH', 'LANG', 'LC_ALL', 'TZ', 'TERM', 'TMPDIR', 'HOME', 'USER', 'LOGNAME', 'SHELL']
182
+ .filter(key => env[key] !== undefined)
183
+ .map(key => [key, env[key]]),
184
+ ));
185
+ const clientInterpreter = kind => {
186
+ const value = env[`RIVET_${kind.toUpperCase()}_INTERPRETER`];
187
+ return value === undefined ? {} : { interpreter: executable(value) };
188
+ };
189
+ const planningClientFor = async request => {
190
+ if (!request || typeof request !== 'object' || Array.isArray(request)
191
+ || Reflect.ownKeys(request).length !== 2
192
+ || !Object.hasOwn(request, 'client') || !Object.hasOwn(request, 'project')) fail();
193
+ if (request.client === 'claude') {
194
+ return createClaudePlanningClient({
195
+ ...harnessSettings('claude'),
196
+ expectedVersion: selectedVersion('claude'),
197
+ worktree: executable(request.project),
198
+ environment: clientEnvironment,
199
+ timeoutMs: FEATURE_PLANNING_TIMEOUT_MS,
200
+ });
201
+ }
202
+ if (request.client === 'codex') {
203
+ return createCodexPlanningClient({
204
+ ...harnessSettings('codex'),
205
+ expectedVersion: selectedVersion('codex'),
206
+ worktree: executable(request.project),
207
+ environment: clientEnvironment,
208
+ timeoutMs: FEATURE_PLANNING_TIMEOUT_MS,
209
+ });
210
+ }
211
+ fail();
212
+ };
213
+ const agentClientFor = (kind, clientProfile) => {
214
+ if (!matchesClientProfile(kind, clientProfile)) fail();
215
+ if (kind === 'claude') return createClaudeClient({
216
+ ...harnessSettings('claude'),
217
+ expectedVersion: selectedVersion('claude'),
218
+ args: [
219
+ ...CLAUDE_ADAPTER_SYNTAX.args.slice(0, -1),
220
+ '--model', CLAUDE_FEATURE_PROFILE.model,
221
+ '--effort', 'low',
222
+ '--permission-mode', 'acceptEdits',
223
+ '{stdin}',
224
+ ],
225
+ environment: clientEnvironment,
226
+ });
227
+ if (kind === 'codex') return createCodexClient({
228
+ ...harnessSettings('codex'),
229
+ expectedVersion: selectedVersion('codex'),
230
+ args: [...CODEX_ADAPTER_SYNTAX.args.slice(0, -1), '--sandbox', 'workspace-write', '{stdin}'],
231
+ environment: clientEnvironment,
232
+ });
233
+ fail();
234
+ };
235
+ const resolveCommandExecutable = async (runner, options = {}) => {
236
+ if (typeof runner !== 'string' || (!/^[a-z][a-z0-9._+-]{0,127}$/i.test(runner) && !isProjectExecutable(runner))
237
+ || !options || typeof options !== 'object' || Array.isArray(options)
238
+ || Object.keys(options).some(key => !['execution', 'worktree', 'cwd'].includes(key))
239
+ || (options.execution !== undefined && options.execution !== 'argv')) fail();
240
+ const direct = options.execution === 'argv';
241
+ if (runner.startsWith('./')) {
242
+ if (!direct || typeof options.worktree !== 'string' || !isAbsolute(options.worktree)
243
+ || resolve(options.worktree) !== options.worktree || !isExecutionCompatibleCwd(options.cwd ?? '.')) fail('command-executable-unsafe');
244
+ try {
245
+ const root = options.worktree;
246
+ const parts = [...((options.cwd ?? '.') === '.' ? [] : options.cwd.split('/')), ...runner.slice(2).split('/')];
247
+ let cursor = root;
248
+ for (const part of [null, ...parts.slice(0, -1)]) {
249
+ if (part !== null) cursor = join(cursor, part);
250
+ const metadata = await lstatFile(cursor);
251
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpathFile(cursor) !== cursor) fail('command-executable-unsafe');
252
+ }
253
+ const invocation = executable(join(cursor, parts.at(-1)));
254
+ await realpathFile(invocation);
255
+ return invocation;
256
+ } catch (error) {
257
+ if (error?.code === 'ERR_APPLICATION_CONFIGURATION') throw error;
258
+ fail(error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? 'command-executable-unavailable' : 'command-executable-unsafe');
259
+ }
260
+ }
261
+
262
+ const resolveExecutable = async candidate => {
263
+ const path = executable(candidate);
264
+ const canonical = executable(await realpathFile(path));
265
+ // Interpreter and tool aliases can select an environment or invocation mode.
266
+ return direct ? executable(join(await realpathFile(dirname(path)), basename(path))) : canonical;
267
+ };
268
+ const configuredPath = env[`RIVET_${runner.toUpperCase().replaceAll('-', '_')}_EXECUTABLE`];
269
+ if (configuredPath !== undefined) {
270
+ try { return await resolveExecutable(configuredPath); }
271
+ catch (error) { if (direct) fail('command-executable-unavailable'); throw error; }
272
+ }
273
+ for (const candidate of executableCandidates(runner, env.PATH)) {
274
+ try { return await resolveExecutable(candidate); } catch {}
275
+ }
276
+ fail(direct ? 'command-executable-unavailable' : undefined);
277
+ };
278
+ const executeFeature = async (request, options) => {
279
+ executorPromise ??= gitClient().then(client => createFeatureExecutor({
280
+ gitClient: client,
281
+ clientFor: async (kind, profile, { project, signal }) => {
282
+ await harnesses.select(kind, project, { signal });
283
+ return agentClientFor(kind, profile);
284
+ },
285
+ resolveCommandExecutable,
286
+ now,
287
+ environment: clientEnvironment,
288
+ }));
289
+ return (await executorPromise)(request, options);
290
+ };
291
+ const resolvedWorkflow = () => {
292
+ if (configured.featureWorkflow !== undefined) return Promise.resolve(captureFeatureWorkflow(configured.featureWorkflow));
293
+ workflowPromise ??= gitClient().then(client => createFeatureWorkflow({
294
+ gitClient: client,
295
+ planningClientFor,
296
+ trackerAdapterFor: async ({ provider, config }) => createTrackerProviderFactory({
297
+ config,
298
+ environment: env,
299
+ transport: configured.providerTransport ?? createNodeProviderTransport(),
300
+ clock: now,
301
+ }).create({ tracker: provider }),
302
+ executeFeature,
303
+ now,
304
+ }));
305
+ return workflowPromise;
306
+ };
307
+ const workflow = configured.featureWorkflow !== undefined
308
+ ? captureFeatureWorkflow(configured.featureWorkflow)
309
+ : Object.freeze(Object.fromEntries(FEATURE_WORKFLOW_METHODS.map(method => [
310
+ method, async (...args) => (await resolvedWorkflow())[method](...args),
311
+ ])));
312
+
313
+ const hostExecution = Object.freeze(Object.fromEntries(
314
+ ['prepare', 'nextAction', 'status', 'submitResult', 'verify', 'recover'].map(method => [
315
+ method,
316
+ async (...args) => {
317
+ hostExecutionPromise ??= gitClient().then(client => createHostExecution({
318
+ gitClient: client,
319
+ now,
320
+ resolveCommandExecutable,
321
+ environment: clientEnvironment,
322
+ }));
323
+ const service = await hostExecutionPromise;
324
+ if (typeof service[method] !== 'function') fail();
325
+ return service[method](...args);
326
+ },
327
+ ]),
328
+ ));
329
+
330
+ const feature = Object.freeze({
331
+ ...workflow,
332
+ async openRun(projectRoot, runId) {
333
+ const paths = await resolveFeatureRunPaths(projectRoot, runId);
334
+ return Object.freeze({ paths, store: createFeatureRunStore(paths) });
335
+ },
336
+ createAgentClient(kind) {
337
+ return agentClientFor(kind, clientProfileFor(kind));
338
+ },
339
+ async prepareWorktree(request, options = {}) {
340
+ return createReservedWorktree(request, { ...options, gitClient: await gitClient() });
341
+ },
342
+ buildLaunchContract,
343
+ createOrchestrator,
344
+ async runQualityGates(request, options = {}) {
345
+ return runQualityGates(request, { ...options, gitClient: await gitClient() });
346
+ },
347
+ async collectEvidence(request) {
348
+ return collectEvidenceBundle({ ...request, gitClient: await gitClient() });
349
+ },
350
+ });
351
+
352
+ return Object.freeze({ cwd, env, fs, fetch, feature, work: hostExecution, harnesses, resolveCommandExecutable });
353
+ }
@@ -0,0 +1,33 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {lstat,realpath,open} from 'node:fs/promises';
3
+ import {constants} from 'node:fs';
4
+ import {join} from 'node:path';
5
+ const DIRECTORY='.rivet-deps';
6
+ const MARKER='.rivet-owner.json';
7
+ const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
8
+ const identity=stat=>`${stat.dev}:${stat.ino}`;
9
+ const fail=()=>{throw new Error('Managed dependency directory is unsafe.');};
10
+ export async function readDependencyFile(path,maxBytes) {
11
+ const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
12
+ try {
13
+ const before=await file.stat({bigint:true});
14
+ if(!before.isFile()||before.nlink!==1n||before.size>BigInt(maxBytes)||await realpath(path)!==path)fail();
15
+ const buffer=Buffer.alloc(maxBytes+1);let count=0;
16
+ while(count<buffer.length) {const result=await file.read(buffer,count,buffer.length-count,null);if(!result.bytesRead)break;count+=result.bytesRead;}
17
+ const after=await file.stat({bigint:true}),entry=await lstat(path,{bigint:true});
18
+ if(count>maxBytes||!entry.isFile()||entry.isSymbolicLink()||identity(before)!==identity(after)||identity(before)!==identity(entry)||before.size!==after.size||before.mtimeNs!==after.mtimeNs||before.ctimeNs!==after.ctimeNs||BigInt(count)!==after.size)fail();
19
+ return buffer.subarray(0,count);
20
+ }finally {await file.close();}
21
+ }
22
+ export async function inspectDependencyDirectory(root) {
23
+ const rootStat=await lstat(root,{bigint:true});
24
+ if(!rootStat.isDirectory()||rootStat.isSymbolicLink()||await realpath(root)!==root)fail();
25
+ const path=join(root,DIRECTORY);
26
+ let stat;try {stat=await lstat(path,{bigint:true});}catch(error){if(error.code==='ENOENT')return null;throw error;}
27
+ if(!stat.isDirectory()||stat.isSymbolicLink()||await realpath(path)!==path||(stat.mode&0o077n)!==0n)fail();
28
+ const marker=await readDependencyFile(join(path,MARKER),4096);
29
+ const value=JSON.parse(marker.toString('utf8'));
30
+ if(Object.keys(value).sort().join(',')!=='directoryIdentity,kind,root,rootIdentity,token,version'||value.kind!=='rivet-dependencies'||value.version!==1||value.root!==root||value.rootIdentity!==identity(rootStat)||value.directoryIdentity!==identity(stat)||typeof value.token!=='string'||!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value.token))fail();
31
+ if((await readDependencyFile(join(path,'.gitignore'),32)).toString()!=='*\n')fail();
32
+ return Object.freeze({identity:identity(stat),marker:hash(marker)});
33
+ }
@@ -0,0 +1,121 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, open, realpath, stat } from 'node:fs/promises';
3
+ import { basename, delimiter, isAbsolute, join, relative } from 'node:path';
4
+
5
+ import { missingCapabilities, validVersion } from '../clients/compatibility.js';
6
+ import { CLAUDE_ADAPTER_SYNTAX } from '../clients/claude.js';
7
+ import { CODEX_ADAPTER_SYNTAX } from '../clients/codex.js';
8
+ import { createProcessRunner } from '../clients/process-runner.js';
9
+
10
+ const ADAPTERS = Object.freeze({ claude: CLAUDE_ADAPTER_SYNTAX, codex: CODEX_ADAPTER_SYNTAX });
11
+
12
+ function inside(root, path) {
13
+ const part = relative(root, path);
14
+ return part === '' || (part !== '..' && !part.startsWith('../') && !isAbsolute(part));
15
+ }
16
+
17
+ async function usable(path, projectRoot) {
18
+ if (typeof path !== 'string' || !isAbsolute(path) || path.length > 1024 || /[\u0000\r\n]/.test(path)) return null;
19
+ try {
20
+ const canonical = await realpath(path);
21
+ if (projectRoot && inside(projectRoot, canonical)) return null;
22
+ const metadata = await stat(canonical);
23
+ if (!metadata.isFile()) return null;
24
+ await access(canonical, constants.X_OK);
25
+ return canonical;
26
+ } catch { return null; }
27
+ }
28
+
29
+ async function launchForm(executable, interpreter) {
30
+ let handle;
31
+ try {
32
+ handle = await open(executable, 'r');
33
+ const header = Buffer.alloc(512);
34
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
35
+ const magic = header.subarray(0, 4).toString('hex');
36
+ const native = process.platform === 'linux' ? magic === '7f454c46'
37
+ : ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca'].includes(magic);
38
+ if (native) return interpreter === null ? 'eligible' : 'interpreter-unneeded';
39
+ if (interpreter !== null) {
40
+ const binary = await open(interpreter, 'r');
41
+ try {
42
+ const interpreterHeader = Buffer.alloc(4);
43
+ await binary.read(interpreterHeader, 0, 4, 0);
44
+ const interpreterMagic = interpreterHeader.toString('hex');
45
+ const nativeInterpreter = process.platform === 'linux' ? interpreterMagic === '7f454c46'
46
+ : ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca'].includes(interpreterMagic);
47
+ if (!nativeInterpreter) return 'interpreter-incompatible';
48
+ } finally { await binary.close(); }
49
+ }
50
+ const newline = header.indexOf(0x0a);
51
+ if (newline < 3 || newline > 256 || newline >= bytesRead) return 'executable-unsafe';
52
+ const firstLine = new TextDecoder('utf-8', { fatal: true }).decode(header.subarray(0, newline));
53
+ if (interpreter === null) return firstLine === '#!/usr/bin/env node' ? 'node-interpreter-required' : 'interpreter-required';
54
+ if (firstLine === `#!${interpreter}`
55
+ || (firstLine === '#!/usr/bin/env node' && basename(interpreter) === 'node')) return 'eligible';
56
+ return 'interpreter-incompatible';
57
+ } catch { return 'executable-unsafe'; }
58
+ finally { await handle?.close(); }
59
+ }
60
+
61
+ async function candidates(kind, env, projectRoot) {
62
+ const configured = env[`RIVET_${kind.toUpperCase()}_EXECUTABLE`];
63
+ const entries = configured === undefined
64
+ ? String(env.PATH ?? '').split(delimiter).filter(isAbsolute).map(directory => join(directory, kind))
65
+ : [configured];
66
+ const found = [];
67
+ for (const entry of entries.slice(0, 128)) {
68
+ const canonical = await usable(entry, projectRoot);
69
+ if (canonical !== null && !found.includes(canonical)) found.push(canonical);
70
+ }
71
+ return found;
72
+ }
73
+
74
+ export async function discoverHarnesses({ env, projectRoot, runner, signal }) {
75
+ const result = [];
76
+ for (const [kind, syntax] of Object.entries(ADAPTERS)) {
77
+ const paths = await candidates(kind, env, projectRoot);
78
+ let selected = null;
79
+ let reason = paths.length === 0 ? 'not-installed' : 'capability-probe-failed';
80
+ for (const executable of paths) {
81
+ const interpreterName = env[`RIVET_${kind.toUpperCase()}_INTERPRETER`];
82
+ let interpreter = interpreterName === undefined ? null : await usable(interpreterName, projectRoot);
83
+ if (interpreterName !== undefined && interpreter === null) { reason = 'interpreter-unavailable'; continue; }
84
+ let form = await launchForm(executable, interpreter);
85
+ if (interpreterName === undefined && form === 'node-interpreter-required') {
86
+ interpreter = await usable(process.execPath, projectRoot);
87
+ if (interpreter === null) { reason = 'interpreter-unavailable'; continue; }
88
+ form = await launchForm(executable, interpreter);
89
+ }
90
+ if (form !== 'eligible') { reason = form; continue; }
91
+ try {
92
+ const processRunner = runner ? null : await createProcessRunner({
93
+ executable, ...(interpreter ? { interpreter } : {}), worktree: projectRoot,
94
+ environment: Object.fromEntries(
95
+ ['PATH', 'LANG', 'LC_ALL', 'TZ', 'TERM', 'TMPDIR', 'HOME', 'USER', 'LOGNAME', 'SHELL']
96
+ .filter(key => env[key] !== undefined).map(key => [key, env[key]]),
97
+ ),
98
+ timeoutMs: 3_000, launchTimeoutMs: 3_000, maxOutputBytes: 64 * 1024, signal,
99
+ });
100
+ const probe = async (args, help = false) => {
101
+ if (!runner) return help ? processRunner.probeHelp(kind) : processRunner.probeVersion();
102
+ const output = await runner(interpreter ?? executable, interpreter ? [executable, ...args] : args,
103
+ { cwd: projectRoot, shell: false, timeoutMs: 3_000, maxOutputBytes: help ? 65536 : 4096, signal });
104
+ if (output.code !== 0 || output.timedOut || output.truncated?.stdout || output.truncated?.stderr) throw new Error('probe failed');
105
+ const text = String(output.stdout ?? '').trim();
106
+ if (Buffer.byteLength(text) > (help ? 65536 : 4096)) throw new Error('probe oversized');
107
+ return text;
108
+ };
109
+ const observed = await probe([...syntax.versionArgs]);
110
+ if (!validVersion(observed)) continue;
111
+ const help = await probe(kind === 'codex' ? ['exec', '--help'] : ['--help'], true);
112
+ const missing = missingCapabilities(help, syntax.requiredOptions);
113
+ if (missing.length) { reason = `missing-options: ${missing.join(', ')}`; continue; }
114
+ selected = Object.freeze({ kind, executable, ...(interpreter ? { interpreter } : {}), version: observed });
115
+ break;
116
+ } catch { if (signal?.aborted) throw signal.reason; }
117
+ }
118
+ result.push(Object.freeze(selected ?? { kind, available: false, reason }));
119
+ }
120
+ return Object.freeze(result);
121
+ }
@@ -0,0 +1,47 @@
1
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
2
+
3
+ export class HeartbeatError extends Error {
4
+ constructor(reason = 'invalid-heartbeat') {
5
+ super('Runtime heartbeat is invalid.');
6
+ this.name = 'HeartbeatError';
7
+ this.code = 'ERR_RUNTIME_HEARTBEAT';
8
+ this.safeMessage = this.message;
9
+ this.details = Object.freeze({ reason });
10
+ }
11
+ }
12
+
13
+ function fail(reason) { throw new HeartbeatError(reason); }
14
+ function id(value) { if (typeof value !== 'string' || value.length > 64 || !ID.test(value)) fail('invalid-heartbeat'); return value; }
15
+
16
+ function capture(input, keys) {
17
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-heartbeat');
18
+ const own = Reflect.ownKeys(input);
19
+ if (own.length !== keys.length || own.some(key => typeof key !== 'string' || !keys.includes(key))) fail('invalid-heartbeat');
20
+ const result = {};
21
+ try { for (const key of own) result[key] = input[key]; } catch { fail('invalid-heartbeat'); }
22
+ return result;
23
+ }
24
+
25
+ export function createHeartbeat(input) {
26
+ try {
27
+ const value = capture(input, ['version', 'instanceId', 'nodeId', 'actorId', 'leaseId', 'sequence', 'timestampMs', 'intervalMs']);
28
+ if (value.version !== 1 || !Number.isSafeInteger(value.sequence) || value.sequence < 1
29
+ || !Number.isSafeInteger(value.timestampMs) || value.timestampMs < 0
30
+ || !Number.isSafeInteger(value.intervalMs) || value.intervalMs < 100 || value.intervalMs > 3_600_000) fail('invalid-heartbeat');
31
+ return Object.freeze({ version: 1, instanceId: id(value.instanceId), nodeId: id(value.nodeId), actorId: id(value.actorId), leaseId: id(value.leaseId), sequence: value.sequence, timestampMs: value.timestampMs, intervalMs: value.intervalMs });
32
+ } catch (error) { if (error instanceof HeartbeatError) throw error; fail('invalid-heartbeat'); }
33
+ }
34
+
35
+ export function heartbeatStatus(heartbeat, input) {
36
+ try {
37
+ const value = capture(input, ['nowMs', 'expected']);
38
+ const stable = createHeartbeat(heartbeat);
39
+ const expected = capture(value.expected, ['instanceId', 'nodeId', 'actorId', 'leaseId']);
40
+ if (!Number.isSafeInteger(value.nowMs) || value.nowMs < stable.timestampMs || value.nowMs > Number.MAX_SAFE_INTEGER
41
+ || stable.instanceId !== id(expected.instanceId) || stable.nodeId !== id(expected.nodeId)
42
+ || stable.actorId !== id(expected.actorId) || stable.leaseId !== id(expected.leaseId)) fail('heartbeat-binding');
43
+ const maximumAge = stable.intervalMs * 2;
44
+ if (!Number.isSafeInteger(maximumAge)) fail('invalid-heartbeat');
45
+ return Object.freeze({ status: value.nowMs - stable.timestampMs > maximumAge ? 'stalled' : 'alive', ageMs: value.nowMs - stable.timestampMs });
46
+ } catch (error) { if (error instanceof HeartbeatError) throw error; fail('invalid-heartbeat'); }
47
+ }
@@ -0,0 +1,112 @@
1
+ import { basename } from 'node:path';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+
4
+ import { immutableJson } from '../clients/contract.js';
5
+ import { acquireLock, LockBusyError } from '../state/lock.js';
6
+ import { assertResolvedStatePaths } from '../state/paths.js';
7
+ import { createSnapshotStore } from '../state/snapshot-store.js';
8
+
9
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
10
+
11
+ export class RuntimeInstanceStoreError extends Error {
12
+ constructor(reason = 'invalid-instance') {
13
+ const messages = {
14
+ 'invalid-instance': 'Runtime instance configuration is invalid.',
15
+ 'lock-required': 'Runtime instance operation requires the acquired outer lock.',
16
+ 'version-conflict': 'Runtime instance state version changed concurrently.',
17
+ };
18
+ super(messages[reason] ?? messages['invalid-instance']);
19
+ this.name = 'RuntimeInstanceStoreError';
20
+ this.code = reason === 'version-conflict' ? 'ERR_STATE_VERSION_CONFLICT' : `ERR_RUNTIME_INSTANCE_${reason.replaceAll('-', '_').toUpperCase()}`;
21
+ this.safeMessage = this.message;
22
+ this.details = Object.freeze({ reason });
23
+ }
24
+ }
25
+
26
+ function fail(reason) { throw new RuntimeInstanceStoreError(reason); }
27
+
28
+ function capture(input) {
29
+ try {
30
+ if (!input || typeof input !== 'object' || Array.isArray(input)
31
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail('invalid-instance');
32
+ const allowed = new Set(['id', 'paths', 'initialState']);
33
+ const keys = Reflect.ownKeys(input);
34
+ if (keys.length !== 3 || keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-instance');
35
+ const result = Object.create(null);
36
+ for (const key of keys) {
37
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
38
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-instance');
39
+ result[key] = descriptor.value;
40
+ }
41
+ return result;
42
+ } catch (error) {
43
+ if (error instanceof RuntimeInstanceStoreError) throw error;
44
+ fail('invalid-instance');
45
+ }
46
+ }
47
+
48
+ async function acquireOuter(path) {
49
+ const deadline = Date.now() + 2_000;
50
+ while (true) {
51
+ try { return await acquireLock(path); } catch (error) {
52
+ if (!(error instanceof LockBusyError) || Date.now() >= deadline) throw error;
53
+ await delay(5);
54
+ }
55
+ }
56
+ }
57
+
58
+ export function createRuntimeInstance(input) {
59
+ const value = capture(input);
60
+ if (typeof value.id !== 'string' || value.id.length > 64 || !ID.test(value.id)) fail('invalid-instance');
61
+ try { assertResolvedStatePaths(value.paths); } catch { fail('invalid-instance'); }
62
+ if (basename(value.paths.instanceDir) !== value.id
63
+ || typeof value.paths.runtimeLockPath !== 'string') fail('invalid-instance');
64
+ const initialState = immutableJson(value.initialState, 'invalid-contract');
65
+ if (!initialState || typeof initialState !== 'object' || Array.isArray(initialState)
66
+ || initialState.version !== 0) fail('invalid-instance');
67
+ const snapshots = createSnapshotStore(value.paths, { environment: {} });
68
+ let held = false;
69
+
70
+ async function rawSnapshot() {
71
+ let snapshot = await snapshots.read();
72
+ if (snapshot === null) snapshot = await snapshots.write(initialState, { expectedVersion: 0, environment: {} });
73
+ return snapshot;
74
+ }
75
+
76
+ return Object.freeze({
77
+ id: value.id,
78
+ async acquire() {
79
+ const outer = await acquireOuter(value.paths.runtimeLockPath);
80
+ held = true;
81
+ let released = false;
82
+ return Object.freeze({
83
+ async release() {
84
+ if (released) return;
85
+ await outer.release();
86
+ released = true;
87
+ held = false;
88
+ },
89
+ });
90
+ },
91
+ async read() {
92
+ if (!held) fail('lock-required');
93
+ return immutableJson((await rawSnapshot()).data, 'invalid-contract');
94
+ },
95
+ async commit(expectedVersion, nextState) {
96
+ if (!held) fail('lock-required');
97
+ if (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0) fail('invalid-instance');
98
+ const next = immutableJson(nextState, 'invalid-contract');
99
+ if (!next || typeof next !== 'object' || Array.isArray(next)
100
+ || next.version !== expectedVersion + 1) fail('invalid-instance');
101
+ const current = await rawSnapshot();
102
+ if (current.data?.version !== expectedVersion) fail('version-conflict');
103
+ try {
104
+ const persisted = await snapshots.write(next, { expectedVersion: current.version, environment: {} });
105
+ return immutableJson(persisted.data, 'invalid-contract');
106
+ } catch (error) {
107
+ if (/state version conflict/i.test(error?.message ?? '')) fail('version-conflict');
108
+ throw error;
109
+ }
110
+ },
111
+ });
112
+ }