@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,509 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat, realpath } from 'node:fs/promises';
4
+ import { isAbsolute, resolve } from 'node:path';
5
+ import { inspectDependencyDirectory } from '../runtime/dependency-directory.js';
6
+
7
+ const MAX_OUTPUT_BYTES = 256 * 1024;
8
+ const TIMEOUT_MS = 15_000;
9
+ const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
10
+ const BRANCH = /^(?![./])(?!.*(?:\.\.|\/\/|@\{|\\|[~^:?*\[\]\u0000-\u0020\u007f]))(?!.*\/$)(?!.*\.lock(?:\/|$))[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
11
+ const clients = new WeakSet();
12
+
13
+ export class GitClientError extends Error {
14
+ constructor(reason = 'git-operation-failed') {
15
+ const messages = {
16
+ 'invalid-client': 'Git client configuration is invalid.',
17
+ 'unsafe-executable': 'Git executable is unsafe.',
18
+ 'invalid-input': 'Git operation input is invalid.',
19
+ 'git-operation-failed': 'Git operation failed safely.',
20
+ 'git-timeout': 'Git operation timed out.',
21
+ 'git-output-invalid': 'Git returned invalid or excessive output.',
22
+ 'review-diff-unavailable': 'The patch cannot be displayed safely within the 64 KiB review limit. Inspect the integration checkout in your editor before approval.',
23
+ 'executable-config': 'Repository Git configuration may execute local code.',
24
+ 'repository-path-unsafe': 'Repository contains a noncanonical or ambiguous path.',
25
+ 'repository-unsafe': 'Git repository identity is unsafe.',
26
+ 'repository-changed': 'Git repository identity changed.',
27
+ 'non-fast-forward': 'Integration is not a fast-forward.',
28
+ };
29
+ super(messages[reason] ?? messages['git-operation-failed']);
30
+ this.name = 'GitClientError';
31
+ this.code = `ERR_GIT_${reason.replaceAll('-', '_').toUpperCase()}`;
32
+ this.safeMessage = this.message;
33
+ this.details = Object.freeze({ reason });
34
+ }
35
+ }
36
+
37
+ function fail(reason) { throw new GitClientError(reason); }
38
+
39
+ function sameIdentity(left, right) {
40
+ return left.dev === right.dev && left.ino === right.ino;
41
+ }
42
+
43
+ function validPath(value) {
44
+ return typeof value === 'string' && value.length > 1 && value.length <= 1_024
45
+ && isAbsolute(value) && !/[\u0000\r\n]/.test(value);
46
+ }
47
+
48
+ function absoluteInput(value) {
49
+ if (!validPath(value) || resolve(value) !== value) fail('invalid-input');
50
+ return value;
51
+ }
52
+
53
+ function validBranch(value) {
54
+ return typeof value === 'string' && value.length <= 200 && BRANCH.test(value)
55
+ && value.normalize('NFKC') === value;
56
+ }
57
+
58
+ function validSha(value) {
59
+ return typeof value === 'string' && SHA.test(value);
60
+ }
61
+
62
+ function captureRecord(value, allowed, required = []) {
63
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-input');
64
+ let keys;
65
+ try { keys = Reflect.ownKeys(value); } catch { fail('invalid-input'); }
66
+ if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-input');
67
+ const result = Object.create(null);
68
+ try {
69
+ for (const key of keys) {
70
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
71
+ if (!descriptor?.enumerable) fail('invalid-input');
72
+ result[key] = value[key];
73
+ }
74
+ } catch (error) {
75
+ if (error instanceof GitClientError) throw error;
76
+ fail('invalid-input');
77
+ }
78
+ if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-input');
79
+ return Object.freeze(result);
80
+ }
81
+
82
+ function decode(buffer) {
83
+ try {
84
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
85
+ } catch {
86
+ fail('git-output-invalid');
87
+ }
88
+ }
89
+
90
+ function execute(executable, args, cwd, options = {}) {
91
+ const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
92
+ const maxOutputBytes = options.maxOutputBytes ?? MAX_OUTPUT_BYTES;
93
+ if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string' || arg.includes('\0'))
94
+ || !validPath(cwd) || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000
95
+ || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0 || maxOutputBytes > 1024 * 1024) {
96
+ fail('invalid-input');
97
+ }
98
+ return new Promise((resolvePromise, rejectPromise) => {
99
+ const chunks = [];
100
+ let bytes = 0;
101
+ let excessive = false;
102
+ let settled = false;
103
+ let timedOut = false;
104
+ const child = spawn(executable, [
105
+ '-c', 'core.hooksPath=/dev/null',
106
+ '-c', 'credential.helper=',
107
+ '-c', 'protocol.file.allow=never',
108
+ ...args,
109
+ ], {
110
+ cwd,
111
+ shell: false,
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ windowsHide: true,
114
+ env: {
115
+ PATH: process.env.PATH ?? '',
116
+ HOME: process.env.HOME ?? '',
117
+ TMPDIR: process.env.TMPDIR ?? '',
118
+ GIT_CONFIG_NOSYSTEM: '1',
119
+ GIT_CONFIG_GLOBAL: '/dev/null',
120
+ GIT_NO_REPLACE_OBJECTS: '1',
121
+ GIT_OPTIONAL_LOCKS: '0',
122
+ GIT_TERMINAL_PROMPT: '0',
123
+ GIT_ASKPASS: '/bin/false',
124
+ LC_ALL: 'C',
125
+ },
126
+ });
127
+ const finish = (error, result) => {
128
+ if (settled) return;
129
+ settled = true;
130
+ clearTimeout(timer);
131
+ if (error) rejectPromise(error);
132
+ else resolvePromise(result);
133
+ };
134
+ const collect = chunk => {
135
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
136
+ const available = Math.max(0, maxOutputBytes - bytes);
137
+ if (available > 0) chunks.push(buffer.subarray(0, available));
138
+ bytes += Math.min(buffer.byteLength, available);
139
+ if (buffer.byteLength > available) excessive = true;
140
+ };
141
+ child.stdout.on('data', collect);
142
+ child.stderr.on('data', collect);
143
+ child.once('error', () => finish(new GitClientError('git-operation-failed')));
144
+ child.once('close', code => {
145
+ if (timedOut) return finish(new GitClientError('git-timeout'));
146
+ if (excessive) return finish(new GitClientError('git-output-invalid'));
147
+ finish(null, Object.freeze({ code: code ?? 1, output: decode(Buffer.concat(chunks)) }));
148
+ });
149
+ const timer = setTimeout(() => {
150
+ timedOut = true;
151
+ child.kill('SIGKILL');
152
+ }, timeoutMs);
153
+ timer.unref?.();
154
+ });
155
+ }
156
+
157
+ async function verifiedDirectory(path, reason = 'repository-unsafe') {
158
+ if (!validPath(path) || resolve(path) !== path) fail(reason);
159
+ let before;
160
+ try { before = await lstat(path, { bigint: true }); } catch { fail(reason); }
161
+ if (!before.isDirectory() || before.isSymbolicLink()) fail(reason);
162
+ let canonical;
163
+ try { canonical = await realpath(path); } catch { fail(reason); }
164
+ const after = await lstat(path, { bigint: true });
165
+ if (canonical !== path || !after.isDirectory() || after.isSymbolicLink() || !sameIdentity(before, after)) fail(reason);
166
+ return Object.freeze({
167
+ path,
168
+ dev: after.dev.toString(),
169
+ ino: after.ino.toString(),
170
+ });
171
+ }
172
+
173
+ async function verifyExecutable(path) {
174
+ if (!validPath(path) || resolve(path) !== path) fail('unsafe-executable');
175
+ let before;
176
+ try { before = await lstat(path, { bigint: true }); } catch { fail('unsafe-executable'); }
177
+ // Installed Git binaries may have hardlinked aliases. This path is executed,
178
+ // never mutated; canonical path and inode checks still pin its identity.
179
+ if (!before.isFile() || before.isSymbolicLink() || (before.mode & 0o111n) === 0n) {
180
+ fail('unsafe-executable');
181
+ }
182
+ const canonical = await realpath(path);
183
+ const after = await lstat(path, { bigint: true });
184
+ if (canonical !== path || !after.isFile() || after.isSymbolicLink() || !sameIdentity(before, after)) {
185
+ fail('unsafe-executable');
186
+ }
187
+ return Object.freeze({ path, dev: after.dev.toString(), ino: after.ino.toString() });
188
+ }
189
+
190
+ function oneLine(output) {
191
+ const value = output.endsWith('\n') ? output.slice(0, -1) : output;
192
+ if (!value || /[\u0000\r\n]/.test(value)) fail('git-output-invalid');
193
+ return value;
194
+ }
195
+
196
+ function repositoryPath(value) {
197
+ if (typeof value !== 'string' || value.length === 0 || value.length > 1_024
198
+ || isAbsolute(value) || value.normalize('NFKC') !== value
199
+ || /[\u0000-\u001f\u007f\\:]/.test(value) || value.startsWith('/') || value.endsWith('/')
200
+ || value.includes('//')) fail('repository-path-unsafe');
201
+ const parts = value.split('/');
202
+ if (parts.some(part => (
203
+ !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
204
+ || /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i.test(part)
205
+ || part.toUpperCase().toLowerCase().normalize('NFKC') === '.git'
206
+ ))) fail('repository-path-unsafe');
207
+ return value;
208
+ }
209
+
210
+ function diffPaths(output) {
211
+ if (output === '') return Object.freeze([]);
212
+ if (!output.endsWith('\0')) fail('git-output-invalid');
213
+ const fields = output.slice(0, -1).split('\0');
214
+ const paths = [];
215
+ for (let index = 0; index < fields.length;) {
216
+ const status = fields[index];
217
+ index += 1;
218
+ if (!/^[ACDMRTUXB](?:[0-9]{1,3})?$/.test(status)) fail('git-output-invalid');
219
+ const endpointCount = status[0] === 'R' || status[0] === 'C' ? 2 : 1;
220
+ for (let endpoint = 0; endpoint < endpointCount; endpoint += 1) {
221
+ const path = fields[index];
222
+ index += 1;
223
+ paths.push(repositoryPath(path));
224
+ }
225
+ }
226
+ if (paths.length > 8_192) fail('git-output-invalid');
227
+ return Object.freeze([...new Set(paths)].sort());
228
+ }
229
+
230
+ function parseStatusPaths(output, managedDependencies = false) {
231
+ if (output === '') return Object.freeze([]);
232
+ if (!output.endsWith('\0')) fail('git-output-invalid');
233
+ const records = output.slice(0, -1).split('\0');
234
+ const paths = [];
235
+ for (let index = 0; index < records.length; index += 1) {
236
+ const record = records[index];
237
+ if (record.length < 4 || record[2] !== ' ') fail('git-output-invalid');
238
+ const path = record.slice(3);
239
+ // Git represents an ignored directory as a synthetic path with a trailing
240
+ // slash (for example, `!! node_modules/`). It is a directory marker, not
241
+ // an evidence path; ignored files inside it are still reported separately.
242
+ if (!(record[0] === '!' && record[1] === '!'
243
+ && (path.endsWith('/') || (managedDependencies && path.startsWith('.rivet-deps/'))))) {
244
+ paths.push(repositoryPath(path));
245
+ }
246
+ if (record[0] === 'R' || record[0] === 'C' || record[1] === 'R' || record[1] === 'C') {
247
+ index += 1;
248
+ if (!records[index]) fail('git-output-invalid');
249
+ paths.push(repositoryPath(records[index]));
250
+ }
251
+ }
252
+ if (paths.length > 4_096) fail('git-output-invalid');
253
+ return Object.freeze([...new Set(paths)].sort());
254
+ }
255
+
256
+ function parseWorktrees(output) {
257
+ if (!output.endsWith('\0')) fail('git-output-invalid');
258
+ const fields = output.split('\0');
259
+ const records = [];
260
+ let record = null;
261
+ for (const field of fields) {
262
+ if (field === '') continue;
263
+ const space = field.indexOf(' ');
264
+ const key = space === -1 ? field : field.slice(0, space);
265
+ const value = space === -1 ? true : field.slice(space + 1);
266
+ if (key === 'worktree') {
267
+ if (record) records.push(record);
268
+ record = Object.create(null);
269
+ }
270
+ if (!record || Object.hasOwn(record, key)) fail('git-output-invalid');
271
+ record[key] = value;
272
+ }
273
+ if (record) records.push(record);
274
+ if (records.length === 0 || records.length > 1_024) fail('git-output-invalid');
275
+ return Object.freeze(records.map(item => {
276
+ if (!validPath(item.worktree) || resolve(item.worktree) !== item.worktree || !validSha(item.HEAD)) fail('git-output-invalid');
277
+ const branch = typeof item.branch === 'string' && item.branch.startsWith('refs/heads/')
278
+ ? item.branch.slice('refs/heads/'.length) : null;
279
+ if (branch !== null && !validBranch(branch)) fail('git-output-invalid');
280
+ return Object.freeze({ path: item.worktree, head: item.HEAD, branch, detached: item.detached === true });
281
+ }));
282
+ }
283
+
284
+ function repositoryId(common) {
285
+ return createHash('sha256').update(`${common.path}\0${common.dev}\0${common.ino}`).digest('hex');
286
+ }
287
+
288
+ export async function createGitClient(options = {}) {
289
+ let captured;
290
+ try { captured = captureRecord(options, new Set(['gitExecutable', 'timeoutMs', 'maxOutputBytes']), ['gitExecutable']); }
291
+ catch { fail('invalid-client'); }
292
+ const executable = await verifyExecutable(captured.gitExecutable);
293
+ const timeoutMs = captured.timeoutMs ?? TIMEOUT_MS;
294
+ const maxOutputBytes = captured.maxOutputBytes ?? MAX_OUTPUT_BYTES;
295
+
296
+ async function assertNoExecutableConfig(cwd) {
297
+ const pattern = '^(alias\\.|filter\\.|diff\\.|merge\\.|credential\\.|include\\.|includeif\\.|pager\\.|gpg\\.|sequence\\.editor$|core\\.(fsmonitor|hookspath|editor|pager|sshcommand|worktree)$)';
298
+ const inspectScope = async scope => {
299
+ const result = await execute(executable.path, [
300
+ 'config', scope, '--no-includes', '--null', '--name-only', '--get-regexp', pattern,
301
+ ], cwd, { timeoutMs, maxOutputBytes });
302
+ if (result.code !== 0 && result.code !== 1) throw new GitClientError('git-operation-failed');
303
+ if (result.code === 0) {
304
+ if (!result.output.endsWith('\0')) fail('git-output-invalid');
305
+ const keys = result.output.slice(0, -1).split('\0');
306
+ if (keys.length === 0 || keys.some(key => !key || key.length > 500 || /[\u0000\r\n]/.test(key))) fail('git-output-invalid');
307
+ fail('executable-config');
308
+ }
309
+ if (result.output !== '') fail('git-output-invalid');
310
+ };
311
+ await inspectScope('--local');
312
+ const extension = await execute(executable.path, [
313
+ 'config', '--local', '--no-includes', '--type=bool', '--get', 'extensions.worktreeConfig',
314
+ ], cwd, { timeoutMs, maxOutputBytes });
315
+ if (extension.code !== 0 && extension.code !== 1) throw new GitClientError('git-operation-failed');
316
+ if (extension.code === 0) {
317
+ if (extension.output === 'true\n') await inspectScope('--worktree');
318
+ else if (extension.output !== 'false\n') fail('git-output-invalid');
319
+ }
320
+ }
321
+
322
+ async function run(cwd, args, allowedCodes = [0], outputLimit = maxOutputBytes) {
323
+ const current = await verifyExecutable(executable.path);
324
+ if (current.dev !== executable.dev || current.ino !== executable.ino) fail('unsafe-executable');
325
+ await assertNoExecutableConfig(cwd);
326
+ const result = await execute(executable.path, args, cwd, { timeoutMs, maxOutputBytes: Math.min(maxOutputBytes, outputLimit) });
327
+ if (!allowedCodes.includes(result.code)) throw new GitClientError('git-operation-failed');
328
+ return result;
329
+ }
330
+
331
+ const client = {
332
+ async inspectRepository(cwdInput) {
333
+ const cwd = absoluteInput(cwdInput);
334
+ const rootResult = await run(cwd, ['rev-parse', '--path-format=absolute', '--show-toplevel']);
335
+ const commonResult = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
336
+ const root = await verifiedDirectory(oneLine(rootResult.output));
337
+ const common = await verifiedDirectory(oneLine(commonResult.output));
338
+ const [head, branch, status] = await Promise.all([
339
+ run(root.path, ['rev-parse', '--verify', 'HEAD^{commit}']),
340
+ run(root.path, ['symbolic-ref', '--quiet', '--short', 'HEAD'], [0, 1]),
341
+ run(root.path, ['status', '--porcelain=v1', '-z', '--untracked-files=all']),
342
+ ]);
343
+ const headSha = oneLine(head.output);
344
+ if (!validSha(headSha)) fail('git-output-invalid');
345
+ const branchName = branch.code === 0 ? oneLine(branch.output) : null;
346
+ if (branchName !== null && !validBranch(branchName)) fail('git-output-invalid');
347
+ const dirtyPaths = parseStatusPaths(status.output);
348
+ return Object.freeze({
349
+ root: root.path,
350
+ rootIdentity: Object.freeze({ dev: root.dev, ino: root.ino }),
351
+ gitCommonDir: common.path,
352
+ repositoryId: repositoryId(common),
353
+ headSha,
354
+ branch: branchName,
355
+ detached: branchName === null,
356
+ dirty: dirtyPaths.length > 0,
357
+ dirtyPaths,
358
+ });
359
+ },
360
+
361
+ async listWorktrees(repoRoot) {
362
+ const root = absoluteInput(repoRoot);
363
+ const result = await run(root, ['worktree', 'list', '--porcelain', '-z']);
364
+ return parseWorktrees(result.output);
365
+ },
366
+
367
+ async branchTip(repoRoot, branch) {
368
+ if (!validBranch(branch)) fail('invalid-input');
369
+ const result = await run(absoluteInput(repoRoot), ['rev-parse', '--verify', `refs/heads/${branch}^{commit}`], [0, 128]);
370
+ if (result.code !== 0) return null;
371
+ const sha = oneLine(result.output);
372
+ if (!validSha(sha)) fail('git-output-invalid');
373
+ return sha;
374
+ },
375
+
376
+ async createWorktree(repoRoot, input) {
377
+ const value = captureRecord(input, new Set(['path', 'branch', 'baseSha']), ['path', 'branch', 'baseSha']);
378
+ if (!validPath(value.path) || !validBranch(value.branch) || !validSha(value.baseSha)) fail('invalid-input');
379
+ await run(absoluteInput(repoRoot), ['worktree', 'add', '--quiet', '-b', value.branch, value.path, value.baseSha]);
380
+ },
381
+
382
+ async rollbackWorktreeCreation(repoRoot, input) {
383
+ const value = captureRecord(input, new Set(['path', 'branch', 'baseSha']), ['path', 'branch', 'baseSha']);
384
+ if (!validPath(value.path) || !validBranch(value.branch) || !validSha(value.baseSha)) fail('invalid-input');
385
+ const root = absoluteInput(repoRoot);
386
+ const topology = await client.listWorktrees(root);
387
+ const atPath = topology.filter(item => item.path === value.path);
388
+ const onBranch = topology.filter(item => item.branch === value.branch);
389
+ if (atPath.length > 1 || onBranch.length > 1
390
+ || atPath.some(item => item.branch !== value.branch || item.head !== value.baseSha)
391
+ || onBranch.some(item => item.path !== value.path || item.head !== value.baseSha)) fail('repository-changed');
392
+ if (atPath.length === 1) {
393
+ const worker = await client.inspectRepository(value.path);
394
+ if (worker.dirty || worker.branch !== value.branch || worker.headSha !== value.baseSha) fail('repository-changed');
395
+ await run(root, ['worktree', 'remove', value.path]);
396
+ }
397
+ const tip = await client.branchTip(root, value.branch);
398
+ if (tip === null) return;
399
+ if (tip !== value.baseSha) fail('repository-changed');
400
+ await run(root, ['branch', '-d', value.branch]);
401
+ },
402
+
403
+ async changedPaths(cwd, fromSha, toSha) {
404
+ if (!validSha(fromSha) || !validSha(toSha)) fail('invalid-input');
405
+ const result = await run(absoluteInput(cwd), [
406
+ 'diff', '--name-status', '-z', '--find-renames', '--find-copies-harder',
407
+ '--diff-filter=ACDMRTUXB', fromSha, toSha, '--',
408
+ ]);
409
+ return diffPaths(result.output);
410
+ },
411
+
412
+ async reviewDiff(cwd, input) {
413
+ const value = captureRecord(input, new Set(['fromSha', 'toSha']), ['fromSha', 'toSha']);
414
+ if (!validSha(value.fromSha) || !validSha(value.toSha)) fail('invalid-input');
415
+ try {
416
+ const result = await run(absoluteInput(cwd), [
417
+ '--no-pager', 'diff', '--no-ext-diff', '--no-textconv', '--no-color',
418
+ '--no-renames', '--submodule=short', '--src-prefix=a/', '--dst-prefix=b/',
419
+ value.fromSha, value.toSha, '--',
420
+ ], [0], 64 * 1024);
421
+ return Object.freeze({ patch: result.output, truncated: false });
422
+ } catch (error) {
423
+ if (error?.code === 'ERR_GIT_GIT_OUTPUT_INVALID') fail('review-diff-unavailable');
424
+ throw error;
425
+ }
426
+ },
427
+
428
+ async inspectTrackedFile(cwd, commit, path) {
429
+ if (!validSha(commit)) fail('invalid-input');
430
+ repositoryPath(path);
431
+ const result = await run(absoluteInput(cwd), ['ls-tree', '-z', commit, '--', path]);
432
+ const match = /^(100644|100755) blob ([0-9a-f]{40}|[0-9a-f]{64})\t([^\0]+)\0$/.exec(result.output);
433
+ if (!match || match[3] !== path) fail('repository-path-unsafe');
434
+ return Object.freeze({ path, blob: match[2] });
435
+ },
436
+
437
+ async statusPaths(cwd) {
438
+ const result = await run(absoluteInput(cwd), [
439
+ 'status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=matching',
440
+ ]);
441
+ // Only ignored records may be omitted, and only for this checkout's
442
+ // validated owned directory. Foreign or damaged metadata remains evidence.
443
+ let managedDependencies = false;
444
+ try { managedDependencies = (await inspectDependencyDirectory(absoluteInput(cwd))) !== null; } catch {}
445
+ return parseStatusPaths(result.output, managedDependencies);
446
+ },
447
+
448
+ async commitPaths(cwdInput, input) {
449
+ const value = captureRecord(input, new Set(['paths', 'expectedHeadSha', 'branch', 'message']), [
450
+ 'paths', 'expectedHeadSha', 'branch', 'message',
451
+ ]);
452
+ if (!Array.isArray(value.paths) || value.paths.length === 0 || value.paths.length > 256
453
+ || value.paths.some(path => typeof path !== 'string' || path !== repositoryPath(path))
454
+ || !validSha(value.expectedHeadSha) || !validBranch(value.branch)
455
+ || typeof value.message !== 'string' || value.message.length === 0 || value.message.length > 200
456
+ || /[\u0000\r\n]/.test(value.message)) fail('invalid-input');
457
+ const cwd = absoluteInput(cwdInput);
458
+ const before = await client.inspectRepository(cwd);
459
+ if (before.branch !== value.branch || before.headSha !== value.expectedHeadSha) fail('repository-changed');
460
+ if (!before.dirty) return before;
461
+ await run(before.root, ['add', '--', ...value.paths]);
462
+ const staged = await run(before.root, [
463
+ 'diff', '--cached', '--name-status', '-z', '--find-renames', '--find-copies-harder',
464
+ '--diff-filter=ACDMRTUXB', '--',
465
+ ]);
466
+ const stagedPaths = diffPaths(staged.output);
467
+ if (stagedPaths.length === 0) return client.inspectRepository(before.root);
468
+ if (stagedPaths.some(path => !value.paths.includes(path))) fail('repository-changed');
469
+ await run(before.root, [
470
+ '-c', 'user.name=Rivet Worker',
471
+ '-c', 'user.email=rivet-worker@invalid',
472
+ 'commit', '--quiet', '-m', value.message,
473
+ ]);
474
+ const after = await client.inspectRepository(before.root);
475
+ if (after.branch !== value.branch || after.headSha === before.headSha || after.dirty) fail('repository-changed');
476
+ return after;
477
+ },
478
+
479
+ async isAncestor(cwd, ancestorSha, descendantSha) {
480
+ if (!validSha(ancestorSha) || !validSha(descendantSha)) fail('invalid-input');
481
+ const result = await run(absoluteInput(cwd), ['merge-base', '--is-ancestor', ancestorSha, descendantSha], [0, 1]);
482
+ return result.code === 0;
483
+ },
484
+
485
+ async fastForward(cwd, input) {
486
+ const value = captureRecord(input, new Set(['branch', 'expectedTip', 'newTip']), ['branch', 'expectedTip', 'newTip']);
487
+ if (!validBranch(value.branch) || !validSha(value.expectedTip) || !validSha(value.newTip)) fail('invalid-input');
488
+ const before = await client.inspectRepository(absoluteInput(cwd));
489
+ if (before.dirty || before.detached || before.branch !== value.branch || before.headSha !== value.expectedTip) {
490
+ fail('repository-changed');
491
+ }
492
+ if (!(await client.isAncestor(before.root, value.expectedTip, value.newTip))) fail('non-fast-forward');
493
+ await run(before.root, ['merge', '--ff-only', '--no-edit', '--no-overwrite-ignore', value.newTip]);
494
+ const after = await client.inspectRepository(before.root);
495
+ if (after.branch !== value.branch || after.headSha !== value.newTip || after.dirty) fail('repository-changed');
496
+ return after;
497
+ },
498
+ };
499
+ // Avoid allowing forged objects into higher-level worktree APIs.
500
+ Object.defineProperty(client, 'isTrustedGitClient', { value: true, enumerable: false });
501
+ Object.freeze(client);
502
+ clients.add(client);
503
+ return client;
504
+ }
505
+
506
+ // Kept internal to this module's trust boundary; consumers receive only a boolean.
507
+ export function assertGitClient(client) {
508
+ if (!clients.has(client)) throw new GitClientError('invalid-client');
509
+ }
@@ -0,0 +1,153 @@
1
+ import { lstat, mkdir, realpath } from 'node:fs/promises';
2
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
3
+
4
+ import { assertGitClient } from './client.js';
5
+ import { assertResolvedStatePaths, verifyResolvedStatePaths } from '../state/paths.js';
6
+
7
+ const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
8
+ const BRANCH = /^(?![./])(?!.*(?:\.\.|\/\/|@\{|\\|[~^:?*\[\]\u0000-\u0020\u007f]))(?!.*\/$)(?!.*\.lock(?:\/|$))[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
9
+ const PROTECTED = /^(?:main|master|trunk|develop|development|production|release(?:\/.*)?)$/i;
10
+
11
+ export class IntegrationWorktreeError extends Error {
12
+ constructor(reason = 'invalid-input') {
13
+ const messages = {
14
+ 'invalid-input': 'Integration worktree request is invalid.',
15
+ 'unsafe-path': 'Integration worktree path is unsafe.',
16
+ 'baseline-drift': 'Approved integration baseline no longer matches the source repository.',
17
+ 'dirty-source': 'Source checkout must be clean before feature execution.',
18
+ 'dirty-integration': 'Integration worktree must be clean before feature execution resumes.',
19
+ 'topology-drift': 'Integration worktree topology changed and requires review.',
20
+ 'branch-collision': 'Integration branch already exists at another worktree.',
21
+ 'path-collision': 'Integration worktree path is already in use.',
22
+ 'create-failed': 'Integration worktree could not be created safely.',
23
+ };
24
+ super(messages[reason] ?? messages['invalid-input']);
25
+ this.name = 'IntegrationWorktreeError';
26
+ this.code = `ERR_INTEGRATION_WORKTREE_${reason.replaceAll('-', '_').toUpperCase()}`;
27
+ this.safeMessage = this.message;
28
+ this.details = Object.freeze({ reason });
29
+ }
30
+ }
31
+
32
+ function fail(reason) { throw new IntegrationWorktreeError(reason); }
33
+
34
+ function capture(input) {
35
+ try {
36
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-input');
37
+ const allowed = new Set(['projectRoot', 'statePaths', 'branch', 'worktreePath', 'baseSha']);
38
+ const keys = Reflect.ownKeys(input);
39
+ if (keys.length !== allowed.size || keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-input');
40
+ const value = Object.create(null);
41
+ for (const key of keys) {
42
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
43
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-input');
44
+ value[key] = descriptor.value;
45
+ }
46
+ if (typeof value.projectRoot !== 'string' || !isAbsolute(value.projectRoot) || resolve(value.projectRoot) !== value.projectRoot
47
+ || typeof value.worktreePath !== 'string' || !isAbsolute(value.worktreePath) || resolve(value.worktreePath) !== value.worktreePath
48
+ || typeof value.branch !== 'string' || value.branch.normalize('NFKC') !== value.branch || !BRANCH.test(value.branch)
49
+ || PROTECTED.test(value.branch) || !SHA.test(value.baseSha)) fail('invalid-input');
50
+ assertResolvedStatePaths(value.statePaths);
51
+ return Object.freeze(value);
52
+ } catch (error) {
53
+ if (error instanceof IntegrationWorktreeError) throw error;
54
+ fail('invalid-input');
55
+ }
56
+ }
57
+
58
+ async function secureDirectory(path) {
59
+ try { await mkdir(path, { mode: 0o700 }); } catch (error) { if (error?.code !== 'EEXIST') fail('unsafe-path'); }
60
+ let before;
61
+ let canonical;
62
+ let after;
63
+ try {
64
+ before = await lstat(path, { bigint: true });
65
+ canonical = await realpath(path);
66
+ after = await lstat(path, { bigint: true });
67
+ } catch { fail('unsafe-path'); }
68
+ if (canonical !== path || !before.isDirectory() || before.isSymbolicLink()
69
+ || before.dev !== after.dev || before.ino !== after.ino || (after.mode & 0o777n) !== 0o700n) fail('unsafe-path');
70
+ return path;
71
+ }
72
+
73
+ async function secureParent(value, repositoryId) {
74
+ await verifyResolvedStatePaths(value.statePaths).catch(() => fail('unsafe-path'));
75
+ const workspaceRoot = await secureDirectory(join(dirname(value.projectRoot), '.rivet-worktrees'));
76
+ const repositoryRoot = await secureDirectory(join(workspaceRoot, repositoryId));
77
+ const runRoot = await secureDirectory(join(repositoryRoot, basename(value.statePaths.instanceDir)));
78
+ if (value.worktreePath !== join(runRoot, 'integration')) fail('unsafe-path');
79
+ }
80
+
81
+ function result(value, repository, integration, reused) {
82
+ return Object.freeze({
83
+ branch: value.branch,
84
+ path: value.worktreePath,
85
+ baseSha: value.baseSha,
86
+ headSha: integration.headSha,
87
+ repositoryId: repository.repositoryId,
88
+ reused,
89
+ });
90
+ }
91
+
92
+ export async function prepareIntegrationWorktree(input, options = {}) {
93
+ const value = capture(input);
94
+ let gitClient;
95
+ try {
96
+ if (!options || typeof options !== 'object' || Array.isArray(options)
97
+ || Reflect.ownKeys(options).length !== 1 || !Object.hasOwn(options, 'gitClient')) fail('invalid-input');
98
+ gitClient = options.gitClient;
99
+ assertGitClient(gitClient);
100
+ } catch (error) {
101
+ if (error instanceof IntegrationWorktreeError) throw error;
102
+ fail('invalid-input');
103
+ }
104
+ let repository;
105
+ try { repository = await gitClient.inspectRepository(value.projectRoot); } catch { fail('topology-drift'); }
106
+ if (repository.root !== value.projectRoot || repository.gitCommonDir !== value.statePaths.gitCommonDir
107
+ || repository.detached) fail('topology-drift');
108
+ if (repository.dirty) fail('dirty-source');
109
+ if (repository.headSha !== value.baseSha) fail('baseline-drift');
110
+ await secureParent(value, repository.repositoryId);
111
+
112
+ const topology = await gitClient.listWorktrees(repository.root).catch(() => fail('topology-drift'));
113
+ const atPath = topology.filter(item => item.path === value.worktreePath);
114
+ const onBranch = topology.filter(item => item.branch === value.branch);
115
+ if (atPath.length > 1 || onBranch.length > 1) fail('topology-drift');
116
+ if (atPath.length === 1 || onBranch.length === 1) {
117
+ if (atPath.length !== 1 || onBranch.length !== 1 || atPath[0] !== onBranch[0]) fail(atPath.length ? 'path-collision' : 'branch-collision');
118
+ let integration;
119
+ try { integration = await gitClient.inspectRepository(value.worktreePath); } catch { fail('topology-drift'); }
120
+ if (integration.repositoryId !== repository.repositoryId || integration.gitCommonDir !== repository.gitCommonDir
121
+ || integration.root !== value.worktreePath || integration.branch !== value.branch || integration.detached) fail('topology-drift');
122
+ if (integration.dirty) fail('dirty-integration');
123
+ if (!(await gitClient.isAncestor(integration.root, value.baseSha, integration.headSha))) fail('baseline-drift');
124
+ return result(value, repository, integration, true);
125
+ }
126
+
127
+ if (await gitClient.branchTip(repository.root, value.branch) !== null) fail('branch-collision');
128
+ try {
129
+ await lstat(value.worktreePath);
130
+ fail('path-collision');
131
+ } catch (error) {
132
+ if (error instanceof IntegrationWorktreeError) throw error;
133
+ if (error?.code !== 'ENOENT') fail('unsafe-path');
134
+ }
135
+ try {
136
+ await gitClient.createWorktree(repository.root, {
137
+ path: value.worktreePath,
138
+ branch: value.branch,
139
+ baseSha: value.baseSha,
140
+ });
141
+ const integration = await gitClient.inspectRepository(value.worktreePath);
142
+ const matches = (await gitClient.listWorktrees(repository.root))
143
+ .filter(item => item.path === value.worktreePath && item.branch === value.branch && item.head === value.baseSha);
144
+ if (matches.length !== 1 || integration.repositoryId !== repository.repositoryId
145
+ || integration.gitCommonDir !== repository.gitCommonDir || integration.headSha !== value.baseSha || integration.dirty) {
146
+ fail('topology-drift');
147
+ }
148
+ return result(value, repository, integration, false);
149
+ } catch (error) {
150
+ if (error instanceof IntegrationWorktreeError) throw error;
151
+ fail('create-failed');
152
+ }
153
+ }