@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,848 @@
1
+ import { prepareSetupRemote } from './setup-remote.js';
2
+ import * as filesystem from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import YAML from 'yaml';
8
+
9
+ import { CliError, EXIT_CODES } from '../cli/output.js';
10
+ import { loadProjectConfig } from '../config/load.js';
11
+ import { snapshot } from '../integrations/capabilities.js';
12
+ import { validateProjectConfiguration } from '../config/validate.js';
13
+ import { withPinnedTargetDirectory } from './install.js';
14
+ import { discoverGit } from '../discovery/git.js';
15
+ import {
16
+ ProjectDiscoveryError,
17
+ discoverProject,
18
+ inspectBoundedFile,
19
+ readStrictBoundedFile,
20
+ } from '../discovery/project.js';
21
+ import { discoverTools } from '../discovery/tools.js';
22
+ import {ProjectChecksRequiredError,parseProjectChecks,parseProjectDependencies} from '../discovery/portable.js';
23
+ import {compileProjectDependencies} from '../config/commands.js';
24
+
25
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
26
+ const FILES = Object.freeze(['project.yaml', 'providers.yaml', 'orchestration.yaml', 'quality.yaml']);
27
+ const MAX_TEMPLATE_BYTES = 256 * 1024;
28
+
29
+ function emit(output, json, value, exitCode) {
30
+ if (json) {
31
+ output.json(value, exitCode === EXIT_CODES.SUCCESS ? 'stdout' : 'stderr');
32
+ } else if (exitCode === EXIT_CODES.SUCCESS) {
33
+ output.log(value.message);
34
+ } else {
35
+ output.error(`ERROR: ${value.error.message}`);
36
+ }
37
+ return exitCode;
38
+ }
39
+
40
+ function failure(output, json, code, exitCode, message, details = {}) {
41
+ return emit(output, json, {
42
+ ok: false,
43
+ error: { code, exitCode, message },
44
+ ...details,
45
+ }, exitCode);
46
+ }
47
+
48
+ function boundedTemplate(packageRoot, filename, fs) {
49
+ const lexicalRoot = resolve(packageRoot);
50
+ const rootMetadata = fs.lstatSync(lexicalRoot, { throwIfNoEntry: false });
51
+ if (!rootMetadata?.isDirectory() || rootMetadata.isSymbolicLink()) throw new Error('Project templates are unavailable');
52
+ const root = fs.realpathSync(lexicalRoot);
53
+ if (!sameIdentity(rootMetadata, fs.statSync(root))) throw new Error('Project templates are unavailable');
54
+ const file = inspectBoundedFile(root, `templates/project/.rivet/${filename}`, fs, true);
55
+ return YAML.parse(readStrictBoundedFile(file, fs, MAX_TEMPLATE_BYTES));
56
+ }
57
+
58
+ function provenanceRecord(source, kind, confidence) {
59
+ return Object.freeze({ source, kind, confidence });
60
+ }
61
+
62
+ function collectLeafProvenance(value, prefix, record, target) {
63
+ if (Array.isArray(value)) {
64
+ value.forEach((item, index) => collectLeafProvenance(item, `${prefix}[${index}]`, record, target));
65
+ } else if (value !== null && typeof value === 'object') {
66
+ for (const [key, item] of Object.entries(value)) {
67
+ collectLeafProvenance(item, prefix ? `${prefix}.${key}` : key, record, target);
68
+ }
69
+ } else {
70
+ target[prefix] = record;
71
+ }
72
+ }
73
+
74
+ function replaceProvenance(provenance, prefix, record) {
75
+ for (const path of Object.keys(provenance)) {
76
+ if (path === prefix || path.startsWith(`${prefix}.`) || path.startsWith(`${prefix}[`)) {
77
+ provenance[path] = record;
78
+ }
79
+ }
80
+ }
81
+
82
+ function proposalProvenance(config, discovery, git) {
83
+ const provenance = {};
84
+ for (const [section, value] of Object.entries(config)) {
85
+ collectLeafProvenance(
86
+ value,
87
+ section,
88
+ provenanceRecord(`templates/project/.rivet/${section}.yaml`, 'template', 'exact'),
89
+ provenance,
90
+ );
91
+ }
92
+ for (const field of ['id', 'name', 'stack.framework', 'stack.language', 'stack.packageManager']) {
93
+ const fallbackName = (field === 'id' || field === 'name')
94
+ && discovery.provenance[field] === 'project directory basename';
95
+ replaceProvenance(
96
+ provenance,
97
+ `project.${field}`,
98
+ provenanceRecord(discovery.provenance[field], fallbackName ? 'inferred' : 'detected', fallbackName ? 'low' : 'high'),
99
+ );
100
+ }
101
+ if(config.project.dependencies) {
102
+ replaceProvenance(provenance,'project.dependencies',{});
103
+ collectLeafProvenance(config.project.dependencies,'project.dependencies',provenanceRecord(discovery.provenance.dependencies??'explicit --dependencies-json','inferred','medium'),provenance);
104
+ }
105
+ replaceProvenance(
106
+ provenance,
107
+ 'project.repository.defaultBranch',
108
+ provenanceRecord(
109
+ git.repository === true && git.defaultBranchSource !== 'default'
110
+ ? 'local git metadata' : 'templates/project/.rivet/project.yaml',
111
+ git.repository === true && git.defaultBranchSource !== 'default' ? 'detected' : 'default',
112
+ git.repository === true && git.defaultBranchSource !== 'default' ? 'high' : 'low',
113
+ ),
114
+ );
115
+ for (const command of Object.keys(config.project.commands)) {
116
+ const steps = config.project.schemaVersion === 1
117
+ ? [{ prefix: `project.commands.${command}`, source: discovery.provenance[`commands.${command}`] }]
118
+ : config.project.commands[command].steps.map((_, index) => ({
119
+ prefix: `project.commands.${command}.steps[${index}]`,
120
+ source: discovery.provenance[`commands.${command}.steps[${index}]`],
121
+ }));
122
+ for (const step of steps) {
123
+ replaceProvenance(
124
+ provenance,
125
+ step.prefix,
126
+ provenanceRecord(step.source, step.source.startsWith('safe-default') ? 'default' : 'detected', step.source.startsWith('safe-default') ? 'low' : 'high'),
127
+ );
128
+ }
129
+ }
130
+ for (const [index, gate] of config.quality.commandGates.entries()) {
131
+ replaceProvenance(
132
+ provenance,
133
+ `quality.commandGates[${index}]`,
134
+ provenanceRecord(`project.commands.${gate.command}`, 'inferred', 'high'),
135
+ );
136
+ }
137
+ for (const field of ['storybook', 'playwright']) {
138
+ replaceProvenance(
139
+ provenance,
140
+ `quality.expectations.${field}`,
141
+ provenanceRecord(discovery.provenance[`features.${field}`], 'inferred', 'high'),
142
+ );
143
+ }
144
+ const visualSource = 'Storybook feature inference';
145
+ replaceProvenance(provenance, 'quality.expectations.visual', provenanceRecord(visualSource, 'inferred', 'medium'));
146
+ replaceProvenance(provenance, 'quality.evidence.requireHumanBaseline', provenanceRecord(visualSource, 'inferred', 'medium'));
147
+ return Object.freeze(provenance);
148
+ }
149
+
150
+ function proposalFromTemplates(discovery, git, packageRoot, fs, remote) {
151
+ const config = {
152
+ project: boundedTemplate(packageRoot, 'project.yaml', fs),
153
+ providers: boundedTemplate(packageRoot, 'providers.yaml', fs),
154
+ orchestration: boundedTemplate(packageRoot, 'orchestration.yaml', fs),
155
+ quality: boundedTemplate(packageRoot, 'quality.yaml', fs),
156
+ };
157
+ config.project.id = discovery.proposal.id;
158
+ config.project.name = discovery.proposal.name;
159
+ config.project.schemaVersion = discovery.proposal.schemaVersion;
160
+ config.project.stack = discovery.proposal.stack;
161
+ config.project.repository.defaultBranch = git.defaultBranch ?? 'main';
162
+ if (remote) config.project.repository.remote = remote;
163
+ config.project.commands = discovery.proposal.commands;
164
+ if(discovery.proposal.dependencies)config.project.dependencies=discovery.proposal.dependencies;
165
+ if (config.project.schemaVersion === 3) {
166
+ config.quality.commandGates = Object.keys(config.project.commands).map(command=>({id:command,command,required:['test','check','build'].includes(command)}));
167
+ }
168
+ for (const command of ['lint', 'typecheck']) {
169
+ if (config.project.schemaVersion !== 3 && config.project.commands[command]) {
170
+ config.quality.commandGates.push({ id: command, command, required: false });
171
+ }
172
+ }
173
+ config.quality.expectations.storybook = discovery.features.storybook ? 'required' : 'optional';
174
+ config.quality.expectations.playwright = discovery.features.playwright ? 'required' : 'optional';
175
+ config.quality.expectations.visual = discovery.features.storybook ? 'human-baseline' : 'none';
176
+ config.quality.evidence.requireHumanBaseline = discovery.features.storybook;
177
+ validateProjectConfiguration(config);
178
+ return {
179
+ config,
180
+ provenance: proposalProvenance(config, discovery, git),
181
+ files: Object.fromEntries(FILES.map(filename => {
182
+ const key = filename.replace('.yaml', '');
183
+ return [filename, YAML.stringify(config[key], { lineWidth: 0 })];
184
+ })),
185
+ };
186
+ }
187
+
188
+ function publicGitSummary(git) {
189
+ return {
190
+ repository: git.repository === true,
191
+ currentBranch: git.currentBranch ?? null,
192
+ defaultBranch: git.defaultBranch ?? null,
193
+ detached: git.detached === true,
194
+ dirty: git.dirty === true,
195
+ baseFreshness: git.baseFreshness ?? 'not_checked',
196
+ worktreeCount: Array.isArray(git.worktrees) ? git.worktrees.length : 0,
197
+ occupiedCandidateCount: Array.isArray(git.occupiedCandidatePaths)
198
+ ? git.occupiedCandidatePaths.length : 0,
199
+ };
200
+ }
201
+
202
+ function sameIdentity(left, right) {
203
+ return left.dev === right.dev && left.ino === right.ino;
204
+ }
205
+
206
+ function readBoundedRegular(path, expected, fs) {
207
+ return readStrictBoundedFile({
208
+ path,
209
+ identity: Object.freeze({ dev: expected.dev, ino: expected.ino }),
210
+ }, fs, MAX_TEMPLATE_BYTES);
211
+ }
212
+
213
+ function contentDigest(source) {
214
+ return createHash('sha256').update(source, 'utf8').digest('hex');
215
+ }
216
+
217
+ function targetState(root, files, fs) {
218
+ const directory = join(root, '.rivet');
219
+ const directoryStatus = fs.lstatSync(directory, { throwIfNoEntry: false });
220
+ if (directoryStatus?.isSymbolicLink() || (directoryStatus && !directoryStatus.isDirectory())) {
221
+ throw new Error('Target .rivet path is not a safe directory');
222
+ }
223
+ if (directoryStatus) {
224
+ const unexpected = fs.readdirSync(directory).filter(name => !FILES.includes(name));
225
+ if (unexpected.length > 0) throw new Error('Target .rivet directory contains unexpected entries');
226
+ }
227
+ const existing = [];
228
+ const diffs = {};
229
+ const identities = {};
230
+ const snapshots = {};
231
+ for (const filename of FILES) {
232
+ const path = join(directory, filename);
233
+ const status = fs.lstatSync(path, { throwIfNoEntry: false });
234
+ if (status?.isSymbolicLink() || (status && !status.isFile())) {
235
+ throw new Error('Target configuration contains an unsafe entry');
236
+ }
237
+ if (status) {
238
+ if (status.size > MAX_TEMPLATE_BYTES) {
239
+ throw new Error('Target configuration is too large to compare safely');
240
+ }
241
+ existing.push(filename);
242
+ identities[filename] = Object.freeze({ dev: status.dev, ino: status.ino });
243
+ const prior = readBoundedRegular(path, status, fs);
244
+ snapshots[filename] = Object.freeze({
245
+ dev: status.dev,
246
+ ino: status.ino,
247
+ size: status.size,
248
+ mode: status.mode & 0o7777,
249
+ digest: contentDigest(prior),
250
+ });
251
+ diffs[filename] = {
252
+ action: prior === files[filename] ? 'unchanged' : 'replace',
253
+ beforeBytes: Buffer.byteLength(prior),
254
+ afterBytes: Buffer.byteLength(files[filename]),
255
+ };
256
+ } else {
257
+ identities[filename] = null;
258
+ snapshots[filename] = null;
259
+ diffs[filename] = { action: 'create', beforeBytes: 0, afterBytes: Buffer.byteLength(files[filename]) };
260
+ }
261
+ }
262
+ return {
263
+ directory,
264
+ directoryExists: Boolean(directoryStatus),
265
+ directoryIdentity: directoryStatus
266
+ ? Object.freeze({ dev: directoryStatus.dev, ino: directoryStatus.ino }) : null,
267
+ existing,
268
+ diffs,
269
+ identities,
270
+ snapshots,
271
+ };
272
+ }
273
+
274
+ function assertContained(root, target) {
275
+ const path = relative(resolve(root), resolve(target));
276
+ if (path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path))) return;
277
+ throw new Error('Configuration target escapes the project root');
278
+ }
279
+
280
+ class InitTransactionError extends Error {
281
+ constructor(recovery) {
282
+ super('Project configuration transaction failed safely');
283
+ this.recovery = recovery;
284
+ }
285
+ }
286
+
287
+ function recoveryDetails(residueCount = 0, remediation = 'Retry initialization after resolving the local filesystem condition.') {
288
+ return {
289
+ residueCount,
290
+ recoverable: true,
291
+ remediation,
292
+ };
293
+ }
294
+
295
+ function verifiedRoot(root, fs) {
296
+ const lexical = resolve(root);
297
+ const metadata = fs.lstatSync(lexical, { throwIfNoEntry: false });
298
+ if (!metadata?.isDirectory() || metadata.isSymbolicLink()) throw new InitTransactionError(recoveryDetails());
299
+ const canonical = fs.realpathSync(lexical);
300
+ if (!sameIdentity(metadata, fs.statSync(canonical))) throw new InitTransactionError(recoveryDetails());
301
+ return { path: canonical, identity: Object.freeze({ dev: metadata.dev, ino: metadata.ino }) };
302
+ }
303
+
304
+ function acquireInitLock(root, fs) {
305
+ const anchor = verifiedRoot(root, fs);
306
+ const name = '.rivet-init.lock';
307
+ const path = join(anchor.path, '.rivet-init.lock');
308
+ assertContained(anchor.path, path);
309
+ let created = false;
310
+ try {
311
+ withPinnedTargetDirectory(anchor.path, anchor.identity, fs, () => {
312
+ fs.mkdirSync(name, { mode: 0o700 });
313
+ created = true;
314
+ fs.writeFileSync(join(name, 'owner.json'), JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), {
315
+ encoding: 'utf8', flag: 'wx', mode: 0o600,
316
+ });
317
+ });
318
+ } catch {
319
+ const metadata = fs.lstatSync(path, { throwIfNoEntry: false });
320
+ if (!created && metadata?.isDirectory() && !metadata.isSymbolicLink()) {
321
+ const owner = fs.lstatSync(join(path, 'owner.json'), { throwIfNoEntry: false });
322
+ throw new InitTransactionError({
323
+ ...recoveryDetails(1, 'Inspect and remove the local initialization lock after confirming no writer is active.'),
324
+ lockPresent: true,
325
+ lockAgeMs: Number.isFinite(metadata.mtimeMs) ? Math.max(0, Date.now() - metadata.mtimeMs) : null,
326
+ ownerMetadataPresent: Boolean(owner?.isFile() && !owner.isSymbolicLink()),
327
+ });
328
+ }
329
+ try {
330
+ withPinnedTargetDirectory(anchor.path, anchor.identity, fs, () => {
331
+ try { fs.unlinkSync(join(name, 'owner.json')); } catch {}
332
+ try { fs.rmdirSync(name); } catch {}
333
+ });
334
+ } catch {}
335
+ throw new InitTransactionError({
336
+ ...recoveryDetails(metadata ? 1 : 0),
337
+ lockPresent: Boolean(metadata),
338
+ });
339
+ }
340
+ const metadata = fs.lstatSync(path);
341
+ return { path, name, anchor, identity: Object.freeze({ dev: metadata.dev, ino: metadata.ino }) };
342
+ }
343
+
344
+ function releaseInitLock(lock, fs) {
345
+ let residueCount = 0;
346
+ try {
347
+ withPinnedTargetDirectory(lock.anchor.path, lock.anchor.identity, fs, () => {
348
+ const current = fs.lstatSync(lock.name, { throwIfNoEntry: false });
349
+ if (!current?.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, lock.identity)) {
350
+ residueCount += 1;
351
+ return;
352
+ }
353
+ try { fs.unlinkSync(join(lock.name, 'owner.json')); } catch { residueCount += 1; }
354
+ try { fs.rmdirSync(lock.name); } catch { residueCount += 1; }
355
+ });
356
+ } catch {
357
+ residueCount += 1;
358
+ }
359
+ return residueCount === 0 ? { residueCount: 0 } : recoveryDetails(residueCount,
360
+ 'Inspect and remove the local initialization lock after confirming no writer is active.');
361
+ }
362
+
363
+ function verifyTargetState(state, fs) {
364
+ const unexpected = fs.readdirSync(state.directory).filter(name => !FILES.includes(name));
365
+ if (unexpected.length > 0) throw new Error('Target configuration changed during validation');
366
+ for (const filename of FILES) {
367
+ const current = fs.lstatSync(join(state.directory, filename), { throwIfNoEntry: false });
368
+ if (current?.isSymbolicLink() || (current && !current.isFile())) throw new Error('Target configuration changed during validation');
369
+ const expected = state.identities[filename];
370
+ if ((expected === null && current) || (expected && (!current || !sameIdentity(expected, current)))) {
371
+ throw new Error('Target configuration changed during validation');
372
+ }
373
+ }
374
+ }
375
+
376
+ function directoryMatches(path, identity, fs) {
377
+ try {
378
+ const metadata = fs.lstatSync(path, { throwIfNoEntry: false });
379
+ return metadata?.isDirectory() && !metadata.isSymbolicLink() && sameIdentity(metadata, identity);
380
+ } catch {
381
+ return false;
382
+ }
383
+ }
384
+
385
+ function identityOf(metadata) {
386
+ return Object.freeze({ dev: metadata.dev, ino: metadata.ino });
387
+ }
388
+
389
+ function strictDirectoryEntry(name, expectedIdentity, fs) {
390
+ const metadata = fs.lstatSync(name, { throwIfNoEntry: false });
391
+ return metadata?.isDirectory() && !metadata.isSymbolicLink() && sameIdentity(metadata, expectedIdentity)
392
+ ? metadata : null;
393
+ }
394
+
395
+ function fileMatchesSnapshot(path, metadata, snapshot, fs) {
396
+ if (!metadata?.isFile() || metadata.isSymbolicLink() || !snapshot
397
+ || metadata.dev !== snapshot.dev || metadata.ino !== snapshot.ino
398
+ || metadata.size !== snapshot.size || (metadata.mode & 0o7777) !== snapshot.mode) {
399
+ return false;
400
+ }
401
+ try {
402
+ return contentDigest(readBoundedRegular(path, metadata, fs)) === snapshot.digest;
403
+ } catch {
404
+ return false;
405
+ }
406
+ }
407
+
408
+ function verifyDirectorySnapshot(name, directoryIdentity, snapshots, fs) {
409
+ if (!strictDirectoryEntry(name, directoryIdentity, fs)) return false;
410
+ const expectedEntries = Object.keys(snapshots).filter(filename => snapshots[filename] !== null).sort();
411
+ let entries;
412
+ try { entries = fs.readdirSync(name).sort(); } catch { return false; }
413
+ if (entries.length !== expectedEntries.length || entries.some((entry, index) => entry !== expectedEntries[index])) return false;
414
+ return expectedEntries.every(filename => {
415
+ const path = join(name, filename);
416
+ return fileMatchesSnapshot(path, fs.lstatSync(path, { throwIfNoEntry: false }), snapshots[filename], fs);
417
+ });
418
+ }
419
+
420
+ function verifyStateRelative(state, fs) {
421
+ const directory = fs.lstatSync('.rivet', { throwIfNoEntry: false });
422
+ if (state.directoryIdentity === null) {
423
+ if (directory) throw new Error('Target configuration appeared during initialization');
424
+ return;
425
+ }
426
+ if (!directory?.isDirectory() || directory.isSymbolicLink()
427
+ || !sameIdentity(directory, state.directoryIdentity)) {
428
+ throw new Error('Target configuration directory changed during initialization');
429
+ }
430
+ const unexpected = fs.readdirSync('.rivet').filter(name => !FILES.includes(name));
431
+ if (unexpected.length > 0) throw new Error('Target configuration changed during initialization');
432
+ for (const filename of FILES) {
433
+ const current = fs.lstatSync(join('.rivet', filename), { throwIfNoEntry: false });
434
+ const expected = state.snapshots[filename];
435
+ if (current?.isSymbolicLink() || (current && !current.isFile())
436
+ || (expected === null && current) || (expected && !fileMatchesSnapshot(join('.rivet', filename), current, expected, fs))) {
437
+ throw new Error('Target configuration changed during initialization');
438
+ }
439
+ }
440
+ }
441
+
442
+ function validateStagedDirectory(stagePath, stageIdentity, files, fs) {
443
+ const snapshots = {};
444
+ withPinnedTargetDirectory(stagePath, stageIdentity, fs, () => {
445
+ const entries = fs.readdirSync('.').sort();
446
+ if (entries.length !== FILES.length || entries.some((name, index) => name !== [...FILES].sort()[index])) {
447
+ throw new Error('Staged configuration is incomplete');
448
+ }
449
+ const config = {};
450
+ for (const filename of FILES) {
451
+ const metadata = fs.lstatSync(filename, { throwIfNoEntry: false });
452
+ if (!metadata?.isFile() || metadata.isSymbolicLink()) throw new Error('Staged configuration is unsafe');
453
+ const source = readBoundedRegular(filename, metadata, fs);
454
+ if (source !== files[filename]) throw new Error('Staged configuration content changed');
455
+ snapshots[filename] = Object.freeze({
456
+ dev: metadata.dev,
457
+ ino: metadata.ino,
458
+ size: metadata.size,
459
+ mode: metadata.mode & 0o7777,
460
+ digest: contentDigest(source),
461
+ });
462
+ config[filename.replace('.yaml', '')] = YAML.parse(source);
463
+ }
464
+ validateProjectConfiguration(config);
465
+ });
466
+ return Object.freeze(snapshots);
467
+ }
468
+
469
+ function residueCount(name, fs) {
470
+ try {
471
+ const metadata = fs.lstatSync(name, { throwIfNoEntry: false });
472
+ if (!metadata) return 0;
473
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) return 1;
474
+ return Math.max(1, fs.readdirSync(name).length);
475
+ } catch {
476
+ return 1;
477
+ }
478
+ }
479
+
480
+ function removeOwnedDirectory(name, directoryIdentity, snapshots, fs) {
481
+ if (!verifyDirectorySnapshot(name, directoryIdentity, snapshots, fs)) return false;
482
+ const expectedEntries = Object.keys(snapshots).filter(filename => snapshots[filename] !== null).sort();
483
+ let ok = true;
484
+ for (const filename of expectedEntries) {
485
+ try { fs.unlinkSync(join(name, filename)); } catch { ok = false; }
486
+ }
487
+ if (ok) {
488
+ try { fs.rmdirSync(name); } catch { ok = false; }
489
+ }
490
+ return ok;
491
+ }
492
+
493
+ function rollbackWholeDirectory(context, fs) {
494
+ let errors = 0;
495
+ try {
496
+ withPinnedTargetDirectory(context.anchor.path, context.anchor.identity, fs, () => {
497
+ const live = fs.lstatSync('.rivet', { throwIfNoEntry: false });
498
+ if (context.published) {
499
+ if (live?.isDirectory() && !live.isSymbolicLink() && sameIdentity(live, context.stageIdentity)
500
+ && !fs.lstatSync(context.stageName, { throwIfNoEntry: false })) {
501
+ try {
502
+ fs.renameSync('.rivet', context.stageName);
503
+ if (!strictDirectoryEntry(context.stageName, context.stageIdentity, fs)) errors += 1;
504
+ } catch { errors += 1; }
505
+ } else {
506
+ errors += 1;
507
+ }
508
+ }
509
+ if (context.backupIdentity) {
510
+ const backup = strictDirectoryEntry(context.backupName, context.backupIdentity, fs);
511
+ const target = fs.lstatSync('.rivet', { throwIfNoEntry: false });
512
+ if (backup && !target) {
513
+ try {
514
+ fs.renameSync(context.backupName, '.rivet');
515
+ if (!strictDirectoryEntry('.rivet', context.state.directoryIdentity, fs)) errors += 1;
516
+ } catch { errors += 1; }
517
+ } else if (backup) {
518
+ errors += 1;
519
+ }
520
+ }
521
+ if (strictDirectoryEntry(context.stageName, context.stageIdentity, fs)
522
+ && !removeOwnedDirectory(context.stageName, context.stageIdentity, context.stageSnapshots, fs)) {
523
+ errors += 1;
524
+ }
525
+ });
526
+ } catch {
527
+ errors += 1;
528
+ }
529
+ const residue = residueCount(context.stagePath, fs) + residueCount(context.backupPath, fs);
530
+ return recoveryDetails(Math.max(errors, residue), errors > 0
531
+ ? 'Recover the preserved sibling stage or backup before retrying.'
532
+ : 'Retry initialization after resolving the local filesystem conflict.');
533
+ }
534
+
535
+ function cleanupCommittedBackup(context, fs, options) {
536
+ if (!context.backupIdentity) return { residueCount: 0 };
537
+ let removed = false;
538
+ try {
539
+ withPinnedTargetDirectory(context.anchor.path, context.anchor.identity, fs, () => {
540
+ options.beforeBackupCleanup?.();
541
+ removed = removeOwnedDirectory(context.backupName, context.backupIdentity, context.state.snapshots, fs);
542
+ });
543
+ } catch {}
544
+ const residue = removed ? 0 : residueCount(context.backupPath, fs);
545
+ return residue === 0 ? { residueCount: 0 } : {
546
+ ...recoveryDetails(residue, 'Retry cleanup after resolving the local filesystem condition.'),
547
+ recoveryStored: true,
548
+ warning: 'Committed configuration has private sibling backup cleanup residue.',
549
+ };
550
+ }
551
+
552
+ async function atomicWrite(root, files, state, fs, options = {}) {
553
+ assertContained(root, state.directory);
554
+ const anchor = verifiedRoot(root, fs);
555
+ const marker = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
556
+ const stageName = `.rivet-stage-${marker}`;
557
+ const backupName = `.rivet-backup-${marker}`;
558
+ const context = {
559
+ anchor,
560
+ state,
561
+ stageName,
562
+ stagePath: join(anchor.path, stageName),
563
+ backupName,
564
+ backupPath: join(anchor.path, backupName),
565
+ stageIdentity: null,
566
+ stageSnapshots: {},
567
+ backupIdentity: null,
568
+ published: false,
569
+ };
570
+ try {
571
+ withPinnedTargetDirectory(anchor.path, anchor.identity, fs, () => {
572
+ fs.mkdirSync(stageName, { mode: 0o700 });
573
+ const staged = fs.lstatSync(stageName);
574
+ if (!staged.isDirectory() || staged.isSymbolicLink()) throw new Error('Staged directory is unsafe');
575
+ context.stageIdentity = identityOf(staged);
576
+ });
577
+ withPinnedTargetDirectory(context.stagePath, context.stageIdentity, fs, () => {
578
+ for (const filename of FILES) {
579
+ fs.writeFileSync(filename, files[filename], { encoding: 'utf8', flag: 'wx', mode: 0o600 });
580
+ }
581
+ });
582
+ context.stageSnapshots = validateStagedDirectory(context.stagePath, context.stageIdentity, files, fs);
583
+ options.beforePublish?.();
584
+ await options.beforePublicationCheck?.();
585
+ withPinnedTargetDirectory(anchor.path, anchor.identity, fs, () => {
586
+ verifyStateRelative(state, fs);
587
+ options.beforeFirstMutation?.();
588
+ if (state.directoryIdentity) {
589
+ verifyStateRelative(state, fs);
590
+ if (!verifyDirectorySnapshot(stageName, context.stageIdentity, context.stageSnapshots, fs)) {
591
+ throw new Error('Staged directory changed');
592
+ }
593
+ fs.renameSync('.rivet', backupName);
594
+ const backup = strictDirectoryEntry(backupName, state.directoryIdentity, fs);
595
+ if (!backup || !verifyDirectorySnapshot(backupName, state.directoryIdentity, state.snapshots, fs)) {
596
+ throw new Error('Backup directory identity verification failed');
597
+ }
598
+ context.backupIdentity = identityOf(backup);
599
+ }
600
+ options.beforePublishRename?.();
601
+ if (fs.lstatSync('.rivet', { throwIfNoEntry: false })) throw new Error('Target configuration appeared before publish');
602
+ if (!verifyDirectorySnapshot(stageName, context.stageIdentity, context.stageSnapshots, fs)) {
603
+ throw new Error('Staged directory changed before publish');
604
+ }
605
+ if (context.backupIdentity
606
+ && !verifyDirectorySnapshot(backupName, context.backupIdentity, state.snapshots, fs)) {
607
+ throw new Error('Backup directory changed before publish');
608
+ }
609
+ fs.renameSync(stageName, '.rivet');
610
+ const published = strictDirectoryEntry('.rivet', context.stageIdentity, fs);
611
+ if (!published || !verifyDirectorySnapshot('.rivet', context.stageIdentity, context.stageSnapshots, fs)) {
612
+ throw new Error('Published directory identity verification failed');
613
+ }
614
+ context.published = true;
615
+ });
616
+ const loaded = await (options.configLoader ?? loadProjectConfig)(anchor.path, { fs });
617
+ validateProjectConfiguration(loaded);
618
+ } catch {
619
+ throw new InitTransactionError(rollbackWholeDirectory(context, fs));
620
+ }
621
+ return cleanupCommittedBackup(context, fs, options);
622
+ }
623
+
624
+ export async function init(parsed, dependencies = {}) {
625
+ const fs = dependencies.fs ?? filesystem;
626
+ const output = dependencies.output;
627
+ const json = parsed.flags.json === true;
628
+ const projectRoot = resolve(parsed.flags.project ?? dependencies.cwd?.() ?? process.cwd());
629
+ try {
630
+ const discovery = await (dependencies.projectDiscovery ?? discoverProject)(projectRoot, { fs, checks: parseProjectChecks(parsed.flags['checks-json']) });
631
+ const installation = parseProjectDependencies(parsed.flags['dependencies-json']);
632
+ if(installation !== undefined) {
633
+ discovery.proposal.schemaVersion=3;
634
+ discovery.proposal.commands=Object.fromEntries(Object.entries(discovery.proposal.commands).map(([name,command])=>[name,Array.isArray(command)?{steps:[{cwd:'.',argv:command}]}:command]));
635
+ discovery.proposal.dependencies=installation;
636
+ try {compileProjectDependencies(discovery.proposal);}
637
+ catch {throw new ProjectChecksRequiredError('Dependency installation is invalid. Use bounded inputs, provides, and ordered cwd/argv steps without shell commands.');}
638
+ }
639
+ const git = await (dependencies.gitDiscovery ?? discoverGit)(projectRoot, { runner: dependencies.runner });
640
+ const tools = await (dependencies.toolDiscovery ?? discoverTools)({
641
+ ...(discovery.proposal.schemaVersion===3?{runtimeOnly:true}:{}),
642
+ packageManager: discovery.proposal.stack.packageManager,
643
+ playwright: discovery.features.playwright,
644
+ storybook: discovery.features.storybook,
645
+ }, { cwd: projectRoot, runner: dependencies.runner });
646
+ const remote = dependencies.setupRemoteSelection ?? await prepareSetupRemote(projectRoot, parsed.flags, dependencies);
647
+ const proposal = proposalFromTemplates(discovery, git, dependencies.packageRoot ?? PACKAGE_ROOT, fs, remote.selected);
648
+ if (!parsed.flags.write) {
649
+ const state = targetState(projectRoot, proposal.files, fs);
650
+ const result = {
651
+ ok: true,
652
+ status: 'proposal',
653
+ project: { id: discovery.proposal.id, root: '.' },
654
+ proposal: {
655
+ repository: proposal.config.project.repository,
656
+ remoteSelection: {status:remote.status, choices:remote.choices},
657
+ schemaVersion: discovery.proposal.schemaVersion,
658
+ commands: discovery.proposal.commands,
659
+ ...(discovery.proposal.dependencies?{dependencies:discovery.proposal.dependencies}:{}),
660
+ qualityGates: proposal.config.quality.commandGates,
661
+ files: proposal.files,
662
+ provenance: proposal.provenance,
663
+ diffs: state.diffs,
664
+ },
665
+ discovery: {
666
+ features: discovery.features,
667
+ git: publicGitSummary(git),
668
+ tools,
669
+ warnings: discovery.warnings,
670
+ unresolved: discovery.unresolved,
671
+ },
672
+ checksExecuted: false,
673
+ };
674
+ result.message = `Proposed ${FILES.length} configuration files; no files were written.\n${FILES.map(filename => ` ${filename}: ${state.diffs[filename].action}`).join('\n')}`;
675
+ if (remote.selected) result.message += `\nSelected repository remote: ${remote.selected.name} -> ${remote.selected.url}`;
676
+ else if (remote.status === 'selection-required') result.message += '\nChoose a repository remote with --remote=<name> when applying this configuration.';
677
+ return emit(output, json, result, EXIT_CODES.SUCCESS);
678
+ }
679
+ const lock = acquireInitLock(projectRoot, fs);
680
+ let lockCleanup = { residueCount: 0 };
681
+ let result;
682
+ try {
683
+ const state = targetState(projectRoot, proposal.files, fs);
684
+ result = {
685
+ ok: true,
686
+ status: 'written',
687
+ project: { id: discovery.proposal.id, root: '.' },
688
+ proposal: {
689
+ repository: proposal.config.project.repository,
690
+ remoteSelection: {status:remote.status, choices:remote.choices},
691
+ schemaVersion: discovery.proposal.schemaVersion,
692
+ commands: discovery.proposal.commands,
693
+ ...(discovery.proposal.dependencies?{dependencies:discovery.proposal.dependencies}:{}),
694
+ qualityGates: proposal.config.quality.commandGates,
695
+ files: proposal.files,
696
+ provenance: proposal.provenance,
697
+ diffs: state.diffs,
698
+ },
699
+ discovery: {
700
+ features: discovery.features,
701
+ git: publicGitSummary(git),
702
+ tools,
703
+ warnings: discovery.warnings,
704
+ unresolved: discovery.unresolved,
705
+ },
706
+ checksExecuted: false,
707
+ };
708
+ if (state.existing.length > 0 && !parsed.flags.overwrite) {
709
+ if (json || typeof dependencies.confirmOverwrite !== 'function') {
710
+ return failure(output, json, 'REPOSITORY_CONFLICT', EXIT_CODES.REPOSITORY_CONFLICT,
711
+ 'Existing configuration requires both --write and --overwrite.', { diffs: state.diffs });
712
+ }
713
+ output.log(`Proposed overwrite:\n${FILES.map(filename => ` ${filename}: ${state.diffs[filename].action}`).join('\n')}`);
714
+ const confirmed = await dependencies.confirmOverwrite(state.diffs);
715
+ if (!confirmed) {
716
+ return failure(output, json, 'REPOSITORY_CONFLICT', EXIT_CODES.REPOSITORY_CONFLICT,
717
+ 'Configuration write was cancelled.', { diffs: state.diffs });
718
+ }
719
+ }
720
+ await remote.verify();
721
+ result.cleanup = await atomicWrite(projectRoot, proposal.files, state, fs, {
722
+ beforePublicationCheck: remote.verify,
723
+ beforePublish: dependencies.beforePublish,
724
+ beforeFirstMutation: dependencies.beforeFirstMutation,
725
+ beforePublishRename: dependencies.beforePublishRename,
726
+ beforeBackupCleanup: dependencies.beforeBackupCleanup,
727
+ configLoader: dependencies.configLoader,
728
+ });
729
+ } finally {
730
+ lockCleanup = releaseInitLock(lock, fs);
731
+ }
732
+ if (lockCleanup.residueCount > 0) {
733
+ result.cleanup = {
734
+ ...result.cleanup,
735
+ residueCount: result.cleanup.residueCount + lockCleanup.residueCount,
736
+ warning: lockCleanup.remediation,
737
+ remediation: lockCleanup.remediation,
738
+ };
739
+ }
740
+ result.message = `Wrote ${FILES.length} configuration files to .rivet/.${result.cleanup.residueCount > 0 ? ' Backup cleanup requires attention.' : ''}`;
741
+ if (remote.selected) result.message += `\nSelected repository remote: ${remote.selected.name} -> ${remote.selected.url}`;
742
+ return emit(output, json, result, EXIT_CODES.SUCCESS);
743
+ } catch (error) {
744
+ if (error instanceof ProjectChecksRequiredError) return failure(output,json,'MISSING_CONFIGURATION',EXIT_CODES.MISSING_CONFIGURATION,error.message);
745
+ return failure(output, json, 'REPOSITORY_CONFLICT', EXIT_CODES.REPOSITORY_CONFLICT,
746
+ error instanceof CliError ? error.message : 'Project configuration could not be proposed or written safely.',
747
+ error instanceof InitTransactionError ? { recovery: error.recovery }
748
+ : error instanceof ProjectDiscoveryError ? { discovery: error.details } : {});
749
+ }
750
+ }
751
+
752
+ /** Update one configuration file under the init lock without replacing .rivet.
753
+ * Protocols stay in place; all four configuration snapshots must remain current.
754
+ */
755
+ async function prepareConfigFileUpdate(root, filename, transform, options = {}) {
756
+ const fs = options.fs ?? filesystem;
757
+ const anchor = verifiedRoot(root, fs);
758
+ const directory = join(anchor.path, '.rivet');
759
+ const metadata = fs.lstatSync(directory);
760
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error('Existing configuration is required');
761
+ const directoryIdentity = identityOf(metadata);
762
+ const files = {}, snapshots = {};
763
+ let entries, protocolsIdentity;
764
+ const config = await loadProjectConfig(anchor.path, { fs });
765
+ withPinnedTargetDirectory(directory, directoryIdentity, fs, () => {
766
+ entries = fs.readdirSync('.').sort();
767
+ if (entries.includes('protocols')) protocolsIdentity = identityOf(fs.lstatSync('protocols'));
768
+ for (const name of FILES) {
769
+ const before = fs.lstatSync(name);
770
+ const content = readBoundedRegular(name, before, fs);
771
+ files[name] = content;
772
+ snapshots[name] = { dev: before.dev, ino: before.ino, size: before.size, mode: before.mode & 0o7777, digest: contentDigest(content) };
773
+ }
774
+ });
775
+ const verifyTopology = (temporary) => {
776
+ const current = fs.readdirSync('.').filter(name => name !== temporary).sort();
777
+ if (JSON.stringify(current) !== JSON.stringify(entries)
778
+ || (protocolsIdentity && !directoryMatches('protocols', protocolsIdentity, fs))) throw new Error('Configuration topology changed');
779
+ };
780
+ const verify = () => {
781
+ if (!directoryMatches(anchor.path, anchor.identity, fs) || !directoryMatches(directory, directoryIdentity, fs)) throw new Error('Configuration identity changed');
782
+ withPinnedTargetDirectory(directory, directoryIdentity, fs, () => {
783
+ verifyTopology();
784
+ for (const name of FILES) if (!fileMatchesSnapshot(name, fs.lstatSync(name, { throwIfNoEntry: false }), snapshots[name], fs)) throw new Error('Configuration changed');
785
+ });
786
+ };
787
+ // Bind the parsed values to the byte snapshots, including any edits during load.
788
+ const reloaded = await loadProjectConfig(anchor.path, { fs });
789
+ if (JSON.stringify(config) !== JSON.stringify(reloaded)) throw new Error('Configuration changed');
790
+ verify();
791
+ let committed = false;
792
+ return Object.freeze({ config, propose(additions) {
793
+ const document = YAML.parseDocument(files[filename]);
794
+ const updated = structuredClone(config);
795
+ transform(updated, document, additions);
796
+ validateProjectConfiguration(updated);
797
+ const proposed = String(document);
798
+ if (Buffer.byteLength(proposed, 'utf8') > MAX_TEMPLATE_BYTES) throw new Error('Configuration is too large');
799
+ return Object.freeze({ providersYaml: proposed, yaml: proposed, async commit() {
800
+ if (committed) throw new Error('Configuration proposal has already been used');
801
+ committed = true;
802
+ verify();
803
+ await options.beforeCommit?.();
804
+ verify();
805
+ const lock = acquireInitLock(anchor.path, fs);
806
+ const temporary = `.rivet-providers-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
807
+ let staged = null, cleanup;
808
+ try {
809
+ verify();
810
+ withPinnedTargetDirectory(directory, directoryIdentity, fs, () => {
811
+ fs.writeFileSync(temporary, proposed, { encoding: 'utf8', flag: 'wx', mode: snapshots[filename].mode });
812
+ const metadata = fs.lstatSync(temporary);
813
+ staged = { dev: metadata.dev, ino: metadata.ino, size: metadata.size, mode: metadata.mode & 0o7777, digest: contentDigest(proposed) };
814
+ verifyTopology(temporary);
815
+ for (const name of FILES) if (!fileMatchesSnapshot(name, fs.lstatSync(name, { throwIfNoEntry: false }), snapshots[name], fs)) throw new Error('Configuration changed');
816
+ if (!fileMatchesSnapshot(temporary, metadata, staged, fs)) throw new Error('Staged providers changed');
817
+ fs.renameSync(temporary, filename);
818
+ staged = null;
819
+ });
820
+ } finally {
821
+ if (staged) {
822
+ try { withPinnedTargetDirectory(directory, directoryIdentity, fs, () => {
823
+ if (fileMatchesSnapshot(temporary, fs.lstatSync(temporary, { throwIfNoEntry: false }), staged, fs)) fs.unlinkSync(temporary);
824
+ }); } catch {}
825
+ }
826
+ cleanup = releaseInitLock(lock, fs);
827
+ }
828
+ return cleanup;
829
+ } });
830
+ } });
831
+ }
832
+
833
+ export { atomicWrite };
834
+
835
+ export async function prepareProviderAppend(root, options = {}) {
836
+ return prepareConfigFileUpdate(root, 'providers.yaml', (config, document, additions) => {
837
+ const added = snapshot(additions, MAX_TEMPLATE_BYTES);
838
+ if (!Array.isArray(added) || !added.length) throw new Error('Provider additions are required');
839
+ config.providers.providers.push(...added);
840
+ for (const provider of added) document.get('providers', true).add(provider);
841
+ }, options);
842
+ }
843
+ export async function prepareRepositoryRemoteUpdate(root, options = {}) {
844
+ return prepareConfigFileUpdate(root, 'project.yaml', (config, document, remote) => {
845
+ config.project.repository.remote = snapshot(remote, MAX_TEMPLATE_BYTES);
846
+ document.setIn(['repository', 'remote'], config.project.repository.remote);
847
+ }, options);
848
+ }