@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,266 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, unlink } from 'node:fs/promises';
3
+ import { hostname } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ const MAX_LOCK_BYTES = 4 * 1024;
7
+ const DEFAULT_STALE_AFTER_MS = 5 * 60 * 1000;
8
+ const MAX_INITIALIZATION_GRACE_MS = 5_000;
9
+ const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
10
+
11
+ export class LockBusyError extends Error {
12
+ constructor(message = 'State lock is held by another owner') {
13
+ super(message);
14
+ this.name = 'LockBusyError';
15
+ this.code = 'ERR_STATE_LOCKED';
16
+ }
17
+ }
18
+
19
+ export class StaleLockError extends Error {
20
+ constructor() {
21
+ super('Stale state lock requires an explicit recovery command');
22
+ this.name = 'StaleLockError';
23
+ this.code = 'ERR_STALE_STATE_LOCK';
24
+ }
25
+ }
26
+
27
+ function sameIdentity(left, right) {
28
+ return left.dev === right.dev && left.ino === right.ino;
29
+ }
30
+
31
+ function privateRegularFile(metadata) {
32
+ return metadata.isFile()
33
+ && !metadata.isSymbolicLink()
34
+ && metadata.nlink === 1
35
+ && (metadata.mode & 0o777) === 0o600;
36
+ }
37
+
38
+ function validOwner(record) {
39
+ return record
40
+ && typeof record === 'object'
41
+ && Object.keys(record).length === 4
42
+ && Number.isSafeInteger(record.pid)
43
+ && record.pid > 0
44
+ && typeof record.host === 'string'
45
+ && record.host.length > 0
46
+ && record.host.length <= 255
47
+ && typeof record.timestamp === 'string'
48
+ && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(record.timestamp)
49
+ && Number.isFinite(Date.parse(record.timestamp))
50
+ && typeof record.ownerId === 'string'
51
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(record.ownerId);
52
+ }
53
+
54
+ async function readBoundedUtf8(handle, maximumBytes) {
55
+ const buffer = Buffer.alloc(maximumBytes + 1);
56
+ let offset = 0;
57
+ while (offset < buffer.length) {
58
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
59
+ if (bytesRead === 0) break;
60
+ offset += bytesRead;
61
+ }
62
+ if (offset > maximumBytes) throw new Error('Unsafe state lock file');
63
+ try {
64
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, offset));
65
+ } catch {
66
+ throw new Error('Unsafe state lock file');
67
+ }
68
+ }
69
+
70
+ function validateTiming(options = {}) {
71
+ const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
72
+ const now = options.now ?? Date.now;
73
+ if (!Number.isSafeInteger(staleAfterMs) || staleAfterMs <= 0 || staleAfterMs > 24 * 60 * 60 * 1000) {
74
+ throw new TypeError('Invalid state lock expiry');
75
+ }
76
+ if (typeof now !== 'function') throw new TypeError('Invalid state lock clock');
77
+ const nowMs = now();
78
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) throw new TypeError('Invalid state lock clock');
79
+ return { staleAfterMs, nowMs };
80
+ }
81
+
82
+ async function inspectLock(path) {
83
+ let before;
84
+ try {
85
+ before = await lstat(path);
86
+ } catch (error) {
87
+ if (error.code === 'ENOENT') return null;
88
+ throw error;
89
+ }
90
+ if (!privateRegularFile(before) || before.size <= 0 || before.size > MAX_LOCK_BYTES) {
91
+ throw new Error('Unsafe state lock file');
92
+ }
93
+
94
+ let handle;
95
+ try {
96
+ handle = await open(path, constants.O_RDONLY | NOFOLLOW);
97
+ const opened = await handle.stat();
98
+ if (!sameIdentity(before, opened) || !privateRegularFile(opened)) throw new Error('Unsafe state lock file');
99
+ const source = await readBoundedUtf8(handle, MAX_LOCK_BYTES);
100
+ const after = await lstat(path);
101
+ if (!privateRegularFile(after) || !sameIdentity(opened, after) || after.size !== opened.size) {
102
+ throw new Error('State lock identity changed');
103
+ }
104
+ let owner;
105
+ try {
106
+ owner = JSON.parse(source);
107
+ } catch {
108
+ throw new Error('Unsafe state lock file');
109
+ }
110
+ if (!validOwner(owner)) throw new Error('Unsafe state lock file');
111
+ return { owner, metadata: after };
112
+ } finally {
113
+ await handle?.close();
114
+ }
115
+ }
116
+
117
+ async function initializingLock(path, staleAfterMs) {
118
+ try {
119
+ const metadata = await lstat(path);
120
+ const ageMs = Date.now() - metadata.mtimeMs;
121
+ return privateRegularFile(metadata)
122
+ && metadata.size <= MAX_LOCK_BYTES
123
+ && ageMs >= -1_000
124
+ && ageMs < Math.min(staleAfterMs, MAX_INITIALIZATION_GRACE_MS);
125
+ } catch (error) {
126
+ return error.code === 'ENOENT' ? null : false;
127
+ }
128
+ }
129
+
130
+ function stale(record, nowMs, staleAfterMs) {
131
+ const timestamp = Date.parse(record.timestamp);
132
+ if (timestamp > nowMs + 1_000) throw new Error('Unsafe state lock timestamp');
133
+ return nowMs - timestamp >= staleAfterMs;
134
+ }
135
+
136
+ async function removeOwnedLock(path, expectedOwnerId, expectedMetadata) {
137
+ const inspected = await inspectLock(path);
138
+ if (!inspected
139
+ || inspected.owner.ownerId !== expectedOwnerId
140
+ || !sameIdentity(inspected.metadata, expectedMetadata)) {
141
+ throw new Error('State lock ownership changed');
142
+ }
143
+ await unlink(path);
144
+ }
145
+
146
+ async function removeIfIdentityMatches(path, expectedMetadata) {
147
+ try {
148
+ const metadata = await lstat(path);
149
+ if (privateRegularFile(metadata) && sameIdentity(metadata, expectedMetadata)) await unlink(path);
150
+ } catch {}
151
+ }
152
+
153
+ export async function acquireLock(path, options = {}) {
154
+ const { staleAfterMs, nowMs } = validateTiming(options);
155
+ const owner = {
156
+ pid: process.pid,
157
+ host: hostname().slice(0, 255) || 'unknown-host',
158
+ timestamp: new Date(nowMs).toISOString(),
159
+ ownerId: randomUUID(),
160
+ };
161
+ let handle;
162
+ try {
163
+ handle = await open(
164
+ path,
165
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | NOFOLLOW,
166
+ 0o600,
167
+ );
168
+ } catch (error) {
169
+ if (error.code !== 'EEXIST') throw error;
170
+ let inspected;
171
+ try {
172
+ inspected = await inspectLock(path);
173
+ } catch (inspectionError) {
174
+ const initializing = await initializingLock(path, staleAfterMs);
175
+ if (initializing === null) return acquireLock(path, options);
176
+ if (initializing) throw new LockBusyError('State lock is initializing');
177
+ throw inspectionError;
178
+ }
179
+ if (!inspected) return acquireLock(path, options);
180
+ if (stale(inspected.owner, nowMs, staleAfterMs)) throw new StaleLockError();
181
+ throw new LockBusyError();
182
+ }
183
+
184
+ let created;
185
+ try {
186
+ created = await handle.stat();
187
+ await handle.chmod(0o600);
188
+ await handle.writeFile(`${JSON.stringify(owner)}\n`, { encoding: 'utf8' });
189
+ await handle.sync();
190
+ const metadata = await handle.stat();
191
+ if (!privateRegularFile(metadata)) throw new Error('Unsafe state lock file');
192
+ let released = false;
193
+ return Object.freeze({
194
+ owner: Object.freeze({ ...owner }),
195
+ async release() {
196
+ if (released) return;
197
+ await removeOwnedLock(path, owner.ownerId, metadata);
198
+ released = true;
199
+ },
200
+ });
201
+ } catch (error) {
202
+ if (created) await removeIfIdentityMatches(path, created);
203
+ throw error;
204
+ } finally {
205
+ await handle.close();
206
+ }
207
+ }
208
+
209
+ export async function recoverStaleLock(path, options = {}) {
210
+ const { staleAfterMs, nowMs } = validateTiming(options);
211
+ const inspected = await inspectLock(path);
212
+ if (!inspected) return false;
213
+ if (!stale(inspected.owner, nowMs, staleAfterMs)) throw new LockBusyError();
214
+ await removeOwnedLock(path, inspected.owner.ownerId, inspected.metadata);
215
+ return true;
216
+ }
217
+
218
+ function requireDeadLocalOwner(owner) {
219
+ if (owner.host !== hostname()) throw new LockBusyError('Cannot recover a lock from another host');
220
+ try {
221
+ process.kill(owner.pid, 0);
222
+ } catch (error) {
223
+ if (error.code === 'ESRCH') return;
224
+ throw new LockBusyError('Cannot establish that the lock owner exited');
225
+ }
226
+ throw new LockBusyError('State lock owner is still running');
227
+ }
228
+
229
+ /**
230
+ * Recover only an abandoned local lock. Unlike legacy recoverStaleLock, age alone
231
+ * never grants recovery. The permanent per-owner claim serializes all recoverers
232
+ * that observed that owner, including a recoverer delayed past a new acquisition.
233
+ * An interrupted claim deliberately needs manual investigation; do not remove it
234
+ * automatically. This assumes participants use this recovery protocol (not legacy
235
+ * age-only recovery) and a private directory, not arbitrary external replacement.
236
+ */
237
+ export async function recoverAbandonedLock(path, options = {}) {
238
+ const { nowMs } = validateTiming({ now: options.now });
239
+ const inspected = await inspectLock(path);
240
+ if (!inspected) return false;
241
+ if (!stale(inspected.owner, nowMs, DEFAULT_STALE_AFTER_MS)) throw new LockBusyError();
242
+ requireDeadLocalOwner(inspected.owner);
243
+ const claimPath = `${path}.recovered-${inspected.owner.ownerId}`;
244
+ let claim;
245
+ try {
246
+ claim = await open(claimPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | NOFOLLOW, 0o600);
247
+ } catch (error) {
248
+ if (error.code === 'EEXIST') throw new LockBusyError('Lock recovery was already claimed; manual investigation required');
249
+ throw error;
250
+ }
251
+ try {
252
+ await claim.chmod(0o600);
253
+ await claim.writeFile(`${JSON.stringify(inspected.owner)}\n`, { encoding: 'utf8' });
254
+ await claim.sync();
255
+ } finally {
256
+ await claim.close();
257
+ }
258
+ // Recheck identity, owner, file safety and liveness after winning the claim.
259
+ // No other conforming recoverer can unlink this owner, and its process is dead.
260
+ const current = await inspectLock(path);
261
+ if (!current || current.owner.ownerId !== inspected.owner.ownerId
262
+ || !sameIdentity(current.metadata, inspected.metadata)) throw new Error('State lock ownership changed');
263
+ requireDeadLocalOwner(current.owner);
264
+ await removeOwnedLock(path, inspected.owner.ownerId, inspected.metadata);
265
+ return true;
266
+ }
@@ -0,0 +1,274 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, mkdir, open, realpath, readdir } from 'node:fs/promises';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+
5
+ import { runArgv } from '../discovery/tools.js';
6
+
7
+ const INSTANCE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
+ const resolvedStatePaths = new WeakSet();
9
+ const statePathIdentities = new WeakMap();
10
+ const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
11
+ const DIRECTORY = constants.O_DIRECTORY ?? 0;
12
+
13
+ export class UnsupportedStatePlatformError extends Error {
14
+ constructor(platform) {
15
+ super('Private orchestration state requires enforceable owner-only filesystem permissions');
16
+ this.name = 'UnsupportedStatePlatformError';
17
+ this.code = 'ERR_PRIVATE_STATE_UNSUPPORTED_PLATFORM';
18
+ this.platform = platform;
19
+ }
20
+ }
21
+
22
+ function isWithin(parent, candidate) {
23
+ const path = relative(parent, candidate);
24
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
25
+ }
26
+
27
+ async function createPrivateDirectory(candidate, gitCommonDir) {
28
+ try {
29
+ await mkdir(candidate, { mode: 0o700 });
30
+ } catch (error) {
31
+ if (error.code !== 'EEXIST') throw error;
32
+ }
33
+
34
+ const resolved = await realpath(candidate);
35
+ if (!isWithin(gitCommonDir, resolved)) {
36
+ throw new Error('Private state path resolves outside the Git common directory');
37
+ }
38
+ const metadata = await lstat(candidate);
39
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
40
+ throw new Error('Private state path is unsafe');
41
+ }
42
+ let handle;
43
+ try {
44
+ handle = await open(candidate, constants.O_RDONLY | DIRECTORY | NOFOLLOW);
45
+ const opened = await handle.stat();
46
+ if (opened.dev !== metadata.dev || opened.ino !== metadata.ino || !opened.isDirectory()) {
47
+ throw new Error('Private state path identity changed');
48
+ }
49
+ await handle.chmod(0o700);
50
+ const secured = await handle.stat();
51
+ const after = await lstat(candidate);
52
+ if (
53
+ secured.dev !== opened.dev
54
+ || secured.ino !== opened.ino
55
+ || after.dev !== secured.dev
56
+ || after.ino !== secured.ino
57
+ || (secured.mode & 0o777) !== 0o700
58
+ ) {
59
+ throw new Error('Private state path identity changed');
60
+ }
61
+ } finally {
62
+ await handle?.close();
63
+ }
64
+ return resolved;
65
+ }
66
+
67
+ async function existingPrivateDirectory(candidate, gitCommonDir) {
68
+ let metadata;
69
+ try { metadata = await lstat(candidate); }
70
+ catch (error) { if (error?.code === 'ENOENT') return false; throw error; }
71
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== 0o700
72
+ || !isWithin(gitCommonDir, await realpath(candidate))) {
73
+ throw new Error('Private state path is unsafe');
74
+ }
75
+ return true;
76
+ }
77
+
78
+ async function resolveGitCommonDirectory(projectRoot, options, platformError) {
79
+ const platform = process.platform === 'win32' ? 'win32' : (options.platform ?? process.platform);
80
+ if (platform === 'win32') throw new UnsupportedStatePlatformError(platform);
81
+ const cwd = resolve(projectRoot);
82
+ const runner = options.runner ?? runArgv;
83
+ const result = await runner('git', ['rev-parse', '--git-common-dir'], {
84
+ cwd,
85
+ shell: false,
86
+ timeoutMs: 3_000,
87
+ maxOutputBytes: 4 * 1024,
88
+ });
89
+ const output = typeof result?.stdout === 'string' ? result.stdout.trim() : '';
90
+ if (result?.code !== 0 || result?.timedOut || result?.truncated?.stdout || !output || output.includes('\0') || /[\r\n]/.test(output)) {
91
+ throw new Error(platformError);
92
+ }
93
+ const unresolvedCommonDir = isAbsolute(output) ? output : resolve(cwd, output);
94
+ const gitCommonDir = await realpath(unresolvedCommonDir);
95
+ const commonMetadata = await lstat(gitCommonDir);
96
+ if (!commonMetadata.isDirectory()) throw new Error('Git common directory is unsafe');
97
+ return gitCommonDir;
98
+ }
99
+
100
+ function registerPaths(paths) {
101
+ resolvedStatePaths.add(paths);
102
+ return Promise.all([lstat(paths.stateRoot), lstat(paths.instanceDir)]).then(([stateRoot, instanceDir]) => {
103
+ statePathIdentities.set(paths, { stateRoot, instanceDir });
104
+ return paths;
105
+ });
106
+ }
107
+
108
+ export async function resolveStatePaths(projectRoot, instance, options = {}) {
109
+ if (typeof instance !== 'string' || instance.length > 64 || !INSTANCE_ID.test(instance)) {
110
+ throw new TypeError('Invalid state instance identifier');
111
+ }
112
+ const gitCommonDir = await resolveGitCommonDirectory(projectRoot, options, 'Unable to resolve the Git common directory');
113
+
114
+ const stateRoot = await createPrivateDirectory(join(gitCommonDir, 'rivet'), gitCommonDir);
115
+ const instanceDir = await createPrivateDirectory(join(stateRoot, instance), gitCommonDir);
116
+ if (!isWithin(stateRoot, instanceDir)) throw new Error('Private state path is unsafe');
117
+
118
+ const paths = Object.freeze({
119
+ gitCommonDir,
120
+ stateRoot,
121
+ instanceDir,
122
+ eventsPath: join(instanceDir, 'events.jsonl'),
123
+ snapshotPath: join(instanceDir, 'snapshot.json'),
124
+ lockPath: join(instanceDir, 'state.lock'),
125
+ runtimeLockPath: join(instanceDir, 'runtime.lock'),
126
+ });
127
+ return registerPaths(paths);
128
+ }
129
+
130
+ export async function resolveFeatureRunPaths(projectRoot, runId, options = {}) {
131
+ if (typeof runId !== 'string' || runId.length > 64 || !INSTANCE_ID.test(runId)) {
132
+ throw new TypeError('Invalid feature run identifier');
133
+ }
134
+ const gitCommonDir = await resolveGitCommonDirectory(projectRoot, options, 'Unable to resolve the Git common directory');
135
+ const featureRoot = await createPrivateDirectory(join(gitCommonDir, 'rivet'), gitCommonDir);
136
+ const featureRunsRoot = await createPrivateDirectory(join(featureRoot, 'feature-runs'), gitCommonDir);
137
+ const runDir = await createPrivateDirectory(join(featureRunsRoot, runId), gitCommonDir);
138
+ if (!isWithin(featureRunsRoot, runDir)) throw new Error('Private feature run path is unsafe');
139
+ const paths = Object.freeze({
140
+ gitCommonDir,
141
+ featureRoot,
142
+ featureRunsRoot,
143
+ runId,
144
+ runDir,
145
+ stateRoot: featureRunsRoot,
146
+ instanceDir: runDir,
147
+ snapshotPath: join(runDir, 'run.json'),
148
+ lockPath: join(runDir, 'run.lock'),
149
+ });
150
+ return registerPaths(paths);
151
+ }
152
+
153
+ export async function resolveExistingFeatureRunPaths(projectRoot, runId, options = {}) {
154
+ if (typeof runId !== 'string' || runId.length > 64 || !INSTANCE_ID.test(runId)) {
155
+ throw new TypeError('Invalid feature run identifier');
156
+ }
157
+ const gitCommonDir = await resolveGitCommonDirectory(projectRoot, options, 'Unable to resolve the Git common directory');
158
+ const featureRoot = join(gitCommonDir, 'rivet');
159
+ const featureRunsRoot = join(featureRoot, 'feature-runs');
160
+ const runDir = join(featureRunsRoot, runId);
161
+ for (const path of [featureRoot, featureRunsRoot, runDir]) {
162
+ if (!(await existingPrivateDirectory(path, gitCommonDir))) return null;
163
+ }
164
+ return registerPaths(Object.freeze({
165
+ gitCommonDir, featureRoot, featureRunsRoot, runId, runDir,
166
+ stateRoot: featureRunsRoot, instanceDir: runDir,
167
+ snapshotPath: join(runDir, 'run.json'), lockPath: join(runDir, 'run.lock'),
168
+ }));
169
+ }
170
+
171
+ export async function listExistingFeatureRunPaths(projectRoot, options = {}) {
172
+ const gitCommonDir = await resolveGitCommonDirectory(projectRoot, options, 'Unable to resolve the Git common directory');
173
+ const featureRoot = join(gitCommonDir, 'rivet');
174
+ const featureRunsRoot = join(featureRoot, 'feature-runs');
175
+ if (!(await existingPrivateDirectory(featureRoot, gitCommonDir))
176
+ || !(await existingPrivateDirectory(featureRunsRoot, gitCommonDir))) return Object.freeze([]);
177
+ const entries = await readdir(featureRunsRoot);
178
+ if (entries.length > 256 || entries.some(name => !INSTANCE_ID.test(name) || name.length > 64)) {
179
+ throw new Error('Private feature-run directory contains invalid or excessive entries');
180
+ }
181
+ const paths = [];
182
+ for (const name of entries.sort()) {
183
+ const value = await resolveExistingFeatureRunPaths(projectRoot, name, options);
184
+ if (value === null) throw new Error('Private feature-run state changed during discovery');
185
+ paths.push(value);
186
+ }
187
+ return Object.freeze(paths);
188
+ }
189
+
190
+ export async function resolveExistingStatePaths(projectRoot, instance, options = {}) {
191
+ if (typeof instance !== 'string' || instance.length > 64 || !INSTANCE_ID.test(instance)) {
192
+ throw new TypeError('Invalid state instance identifier');
193
+ }
194
+ const gitCommonDir = await resolveGitCommonDirectory(projectRoot, options, 'Unable to resolve the Git common directory');
195
+ const stateRoot = join(gitCommonDir, 'rivet');
196
+ const instanceDir = join(stateRoot, instance);
197
+ for (const path of [stateRoot, instanceDir]) {
198
+ if (!(await existingPrivateDirectory(path, gitCommonDir))) return null;
199
+ }
200
+ return registerPaths(Object.freeze({
201
+ gitCommonDir, stateRoot, instanceDir,
202
+ eventsPath: join(instanceDir, 'events.jsonl'),
203
+ snapshotPath: join(instanceDir, 'snapshot.json'),
204
+ lockPath: join(instanceDir, 'state.lock'),
205
+ runtimeLockPath: join(instanceDir, 'runtime.lock'),
206
+ }));
207
+ }
208
+
209
+ export function verificationReportPaths(featurePaths) {
210
+ assertResolvedStatePaths(featurePaths);
211
+ if (featurePaths.runDir !== featurePaths.instanceDir) throw new TypeError('Invalid feature run paths');
212
+ return registerPaths(Object.freeze({
213
+ ...featurePaths,
214
+ snapshotPath: join(featurePaths.runDir, 'verification.json'),
215
+ lockPath: join(featurePaths.runDir, 'verification.lock'),
216
+ }));
217
+ }
218
+
219
+ export function deliveryRecordPaths(featurePaths) {
220
+ assertResolvedStatePaths(featurePaths);
221
+ if (featurePaths.runDir !== featurePaths.instanceDir) throw new TypeError('Invalid feature run paths');
222
+ return registerPaths(Object.freeze({
223
+ ...featurePaths,
224
+ snapshotPath: join(featurePaths.runDir, 'delivery.json'),
225
+ lockPath: join(featurePaths.runDir, 'delivery.lock'),
226
+ operationLockPath: join(featurePaths.runDir, 'delivery-operation.lock'),
227
+ }));
228
+ }
229
+
230
+ export function acceptedIntegrationPaths(featurePaths) {
231
+ assertResolvedStatePaths(featurePaths);
232
+ if (featurePaths.runDir !== featurePaths.instanceDir) throw new TypeError('Invalid feature run paths');
233
+ return registerPaths(Object.freeze({
234
+ ...featurePaths,
235
+ snapshotPath: join(featurePaths.runDir, 'accepted-integration.json'),
236
+ lockPath: join(featurePaths.runDir, 'accepted-integration.lock'),
237
+ }));
238
+ }
239
+
240
+ export function assertResolvedStatePaths(paths) {
241
+ if (!paths || typeof paths !== 'object' || !resolvedStatePaths.has(paths)) {
242
+ throw new TypeError('State storage requires verified private state paths');
243
+ }
244
+ }
245
+
246
+ function sameIdentity(left, right) {
247
+ return left.dev === right.dev && left.ino === right.ino;
248
+ }
249
+
250
+ export async function verifyResolvedStatePaths(paths) {
251
+ assertResolvedStatePaths(paths);
252
+ const expected = statePathIdentities.get(paths);
253
+ const [stateRoot, instanceDir, resolvedRoot, resolvedInstance] = await Promise.all([
254
+ lstat(paths.stateRoot),
255
+ lstat(paths.instanceDir),
256
+ realpath(paths.stateRoot),
257
+ realpath(paths.instanceDir),
258
+ ]);
259
+ if (
260
+ !stateRoot.isDirectory()
261
+ || stateRoot.isSymbolicLink()
262
+ || !instanceDir.isDirectory()
263
+ || instanceDir.isSymbolicLink()
264
+ || (stateRoot.mode & 0o777) !== 0o700
265
+ || (instanceDir.mode & 0o777) !== 0o700
266
+ || !sameIdentity(stateRoot, expected.stateRoot)
267
+ || !sameIdentity(instanceDir, expected.instanceDir)
268
+ || resolvedRoot !== paths.stateRoot
269
+ || resolvedInstance !== paths.instanceDir
270
+ || !isWithin(paths.gitCommonDir, resolvedInstance)
271
+ ) {
272
+ throw new Error('Private state path identity changed');
273
+ }
274
+ }
@@ -0,0 +1,106 @@
1
+ export const REDACTED = '[REDACTED]';
2
+
3
+ const SENSITIVE_KEY = /(?:^|[-_])(?:password|passwd|secret|api[-_]?key|access[-_]?key|private[-_]?key|authorization|cookie|credential|access[-_]?token|api[-_]?token|auth[-_]?token|token)(?:$|[-_])/i;
4
+ const SENSITIVE_ENVIRONMENT_KEY = /(?:^|_)(?:API_KEY|ACCESS_KEY|PRIVATE_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH(?:ORIZATION)?)(?:_|$)/i;
5
+ const SAFE_KEYS = new Set(['token_limit', 'api_token_env', 'access_token_env', 'token_env', 'username_env']);
6
+ // Gemini usage metadata is numeric accounting, never credential content. Exact
7
+ // keys and integer values only; configured secret keys always take precedence.
8
+ const NUMERIC_USAGE_KEYS = new Set(['promptTokenCount', 'candidatesTokenCount', 'thoughtsTokenCount']);
9
+ const TOKEN_PATTERNS = [
10
+ /\b(?:Bearer|Basic)\s+[A-Za-z0-9+/_.:=-]+/gi,
11
+ /\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
12
+ /\bgh[pousr]_[A-Za-z0-9]{8,}\b/g,
13
+ /\bxox[baprs]-[A-Za-z0-9-]{8,}\b/g,
14
+ /\bsk-[A-Za-z0-9_-]{8,}\b/g,
15
+ /\bAKIA[0-9A-Z]{16}\b/g,
16
+ ];
17
+
18
+ function normalizedKey(key) {
19
+ return String(key)
20
+ .normalize('NFKC')
21
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
22
+ .toLowerCase();
23
+ }
24
+
25
+ function sensitiveEnvironmentValues(environment) {
26
+ return [...new Set(Object.entries(environment ?? {})
27
+ .filter(([key, value]) => (
28
+ SENSITIVE_ENVIRONMENT_KEY.test(key)
29
+ && typeof value === 'string'
30
+ && value.length > 0
31
+ ))
32
+ .map(([, value]) => value))]
33
+ .sort((left, right) => right.length - left.length);
34
+ }
35
+
36
+ function escapeRegExp(value) {
37
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
+ }
39
+
40
+ function redactString(value, environmentValues) {
41
+ let result = value;
42
+ if (environmentValues.length > 0) {
43
+ result = result.replace(new RegExp(environmentValues.map(escapeRegExp).join('|'), 'g'), REDACTED);
44
+ }
45
+ for (const pattern of TOKEN_PATTERNS) result = result.replace(pattern, REDACTED);
46
+ return result;
47
+ }
48
+
49
+ function isSensitiveKey(key, configuredKeys) {
50
+ const normalized = normalizedKey(key);
51
+ if (configuredKeys.has(normalized)) return true;
52
+ if (SAFE_KEYS.has(normalized)) return false;
53
+ return SENSITIVE_KEY.test(normalized);
54
+ }
55
+
56
+ function redactValue(value, context, active) {
57
+ if (value === null || typeof value === 'boolean') return value;
58
+ if (typeof value === 'string') return redactString(value, context.environmentValues);
59
+ if (typeof value === 'number') {
60
+ if (!Number.isFinite(value)) throw new TypeError('State values must be JSON-compatible');
61
+ return value;
62
+ }
63
+ if (typeof value !== 'object') throw new TypeError('State values must be JSON-compatible');
64
+ if (active.has(value)) throw new TypeError('Cannot redact a cyclic value');
65
+
66
+ const prototype = Object.getPrototypeOf(value);
67
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
68
+ throw new TypeError('State values must be JSON-compatible');
69
+ }
70
+
71
+ active.add(value);
72
+ try {
73
+ if (Array.isArray(value)) return value.map(item => redactValue(item, context, active));
74
+ const result = {};
75
+ for (const key of Reflect.ownKeys(value)) {
76
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
77
+ if (typeof key !== 'string' || !descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) {
78
+ throw new TypeError('State values must be JSON-compatible');
79
+ }
80
+ const safeUsageCount = NUMERIC_USAGE_KEYS.has(key)
81
+ && Number.isSafeInteger(descriptor.value) && descriptor.value >= 0
82
+ && !context.configuredKeys.has(normalizedKey(key));
83
+ const child = isSensitiveKey(key, context.configuredKeys) && !safeUsageCount
84
+ ? REDACTED
85
+ : redactValue(descriptor.value, context, active);
86
+ Object.defineProperty(result, key, {
87
+ value: child,
88
+ enumerable: true,
89
+ configurable: true,
90
+ writable: true,
91
+ });
92
+ }
93
+ return result;
94
+ } finally {
95
+ active.delete(value);
96
+ }
97
+ }
98
+
99
+ export function redactSecrets(value, options = {}) {
100
+ const configuredKeys = new Set((options.secretKeys ?? []).map(normalizedKey));
101
+ const environment = options.environment ?? process.env;
102
+ return redactValue(value, {
103
+ configuredKeys,
104
+ environmentValues: sensitiveEnvironmentValues(environment),
105
+ }, new WeakSet());
106
+ }