@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,400 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { evaluateAuthority } from '../policy/authority.js';
3
+ import {
4
+ ACTIONS,
5
+ candidate,
6
+ digest,
7
+ ensure,
8
+ exact,
9
+ fail,
10
+ factsDigest,
11
+ hash,
12
+ id,
13
+ mergeReceiptFor,
14
+ observation,
15
+ plain,
16
+ ready,
17
+ timestamp,
18
+ validateReceipt,
19
+ } from './contract.js';
20
+ import { isDeliveryStore } from './store.js';
21
+ const executors = new WeakSet();
22
+ const inFlight = new Map();
23
+
24
+ // Only trusted application code may create an executor. A succeeded receipt must
25
+ // describe a provider-verified effect for the exact operation; not-applied means
26
+ // terminal proof that no effect occurred or can later occur. An eventually
27
+ // consistent absence check is not sufficient. Native transports are not qualified
28
+ // by this constructor: mutation guarantees and reconciliation remain obligations
29
+ // of the implementation supplied here. Review creation may explicitly verify
30
+ // creation afterward; tracker transitions may verify the approved desired state.
31
+ // These assurances do not claim atomic compare-and-swap or exclusive causation.
32
+ export function createTrustedDeliveryExecutor(input) {
33
+ ensure(input && typeof input === 'object');
34
+ const { provider, capabilities } = input;
35
+ ensure(['github', 'bitbucket', 'gitlab'].includes(provider));
36
+ const captured = plain(capabilities);
37
+ ensure(Array.isArray(captured) && captured.length <= ACTIONS.length);
38
+ for (const capability of captured) {
39
+ if (capability.action === 'review-request' && capability.conditionalHead === false) {
40
+ exact(capability, ['action', 'conditionalHead', 'verifiesCreatedReview', 'reconcile']);
41
+ ensure(capability.verifiesCreatedReview === true && capability.reconcile === true);
42
+ } else if (['tracker-transition', 'review-update'].includes(capability.action) && capability.conditionalHead === false) {
43
+ exact(capability, ['action', 'conditionalHead', 'verifiesDesiredState', 'reconcile']);
44
+ ensure(capability.verifiesDesiredState === true && capability.reconcile === true);
45
+ } else {
46
+ exact(capability, ['action', 'conditionalHead', 'reconcile']);
47
+ ensure(ACTIONS.includes(capability.action) && capability.conditionalHead === true && capability.reconcile === true);
48
+ }
49
+ }
50
+ ensure(new Set(captured.map((value) => value.action)).size === captured.length);
51
+ const unavailable = () => fail('executor-unavailable');
52
+ for (const method of ['observe', 'dispatch', 'reconcile'])
53
+ ensure(input[method] === undefined || typeof input[method] === 'function');
54
+ if (captured.length)
55
+ ensure(
56
+ typeof input.observe === 'function' &&
57
+ typeof input.dispatch === 'function' &&
58
+ typeof input.reconcile === 'function'
59
+ );
60
+ const executor = Object.freeze({
61
+ provider,
62
+ capabilities: captured,
63
+ observe: input.observe ?? unavailable,
64
+ dispatch: input.dispatch ?? unavailable,
65
+ reconcile: input.reconcile ?? unavailable,
66
+ });
67
+ executors.add(executor);
68
+ return executor;
69
+ }
70
+ export function createDeliveryService(config) {
71
+ const { store, executor, authority, approvalRegistry, expectedApproverId, subjectId, providerId } = config;
72
+ ensure(isDeliveryStore(store) && executors.has(executor));
73
+ id(expectedApproverId);
74
+ id(subjectId);
75
+ id(providerId);
76
+ const clock = config.clock ?? (() => new Date().toISOString());
77
+ ensure(typeof clock === 'function');
78
+ const timeoutMs = config.timeoutMs ?? 10000;
79
+ ensure(Number.isSafeInteger(timeoutMs) && timeoutMs >= 1 && timeoutMs <= 120000);
80
+ const now = () => timestamp(clock());
81
+ const active = (state) =>
82
+ state.operations.some((value) => ['dispatching', 'indeterminate'].includes(value.state));
83
+ function writable(state) {
84
+ ensure(!active(state), 'reconciliation-required');
85
+ }
86
+ async function load(version) {
87
+ const state = await store.read();
88
+ ensure(state !== null, 'missing-state');
89
+ ensure(state.candidate.repository.provider === executor.provider, 'provider-mismatch');
90
+ if (version !== undefined)
91
+ ensure(Number.isSafeInteger(version) && state.version === version, 'version-conflict');
92
+ return state;
93
+ }
94
+ async function save(state, changes) {
95
+ const { version, ...record } = state;
96
+ return store.write({ ...record, ...changes, updatedAt: now() }, { expectedVersion: version });
97
+ }
98
+ async function bounded(operation) {
99
+ let timer;
100
+ try {
101
+ return await Promise.race([
102
+ Promise.resolve().then(operation),
103
+ new Promise((_, reject) => {
104
+ timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
105
+ }),
106
+ ]);
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ async function observe(target) {
112
+ const result = observation(await bounded(() => executor.observe(target)), target);
113
+ ensure(
114
+ Date.parse(result.observedAt) <= Date.parse(now()) &&
115
+ Date.parse(now()) - Date.parse(result.observedAt) <= 300000,
116
+ 'stale-observation'
117
+ );
118
+ return result;
119
+ }
120
+ function observedStage(state, facts) {
121
+ if (['merged', 'deployed', 'tracker-updated', 'tracker-status-confirmed'].includes(state.stage)) return state.stage;
122
+ return ready(facts)
123
+ ? 'checks-passed'
124
+ : facts.review ||
125
+ state.operations.some(
126
+ (operation) => operation.action === 'review-request' && operation.state === 'succeeded'
127
+ )
128
+ ? 'review-requested'
129
+ : state.operations.some(operation => operation.action === 'branch-publish' && operation.state === 'succeeded') ? 'branch-published' : 'locally-verified';
130
+ }
131
+ function allowed(state, action) {
132
+ ensure(
133
+ executor.capabilities.some((value) => value.action === action),
134
+ 'unsupported-action'
135
+ );
136
+ if (action === 'branch-publish') ensure(state.observation?.publication?.remoteSha === null && ['locally-verified', 'branch-published'].includes(state.stage) && !state.operations.some(op => op.action === action && op.state === 'succeeded'), 'not-ready');
137
+ else if (action === 'merge')
138
+ ensure(
139
+ ready(state.observation) &&
140
+ !state.operations.some((value) => value.action === 'merge' && value.state === 'succeeded'),
141
+ 'not-ready'
142
+ );
143
+ else if (action === 'review-update') ensure(state.observation?.review?.state === 'open' && !state.operations.some(op => op.action === 'merge' && op.state === 'succeeded'), 'not-ready');
144
+ else if (action === 'review-request')
145
+ ensure(
146
+ state.observation?.review === null &&
147
+ ['locally-verified', 'branch-published'].includes(state.stage) &&
148
+ !state.operations.some(
149
+ (operation) => operation.action === 'review-request' && operation.state === 'succeeded'
150
+ ),
151
+ 'not-ready'
152
+ );
153
+ else ensure(
154
+ state.operations.some((value) => value.action === 'merge' && value.state === 'succeeded') &&
155
+ !state.operations.some((value) => value.action === action && value.state === 'succeeded'),
156
+ 'not-ready'
157
+ );
158
+ }
159
+ async function initialize(input) {
160
+ const target = candidate(input);
161
+ ensure(target.repository.provider === executor.provider);
162
+ ensure(Date.parse(target.localVerification.verifiedAt) <= Date.parse(now()), 'invalid-verification');
163
+ return store.exclusive(async () => {
164
+ ensure((await store.read()) === null, 'state-exists');
165
+ const at = now();
166
+ return store.write(
167
+ {
168
+ schemaVersion: 1,
169
+ stage: 'locally-verified',
170
+ candidate: target,
171
+ observation: null,
172
+ proposal: null,
173
+ operations: [],
174
+ usedApprovalIds: [],
175
+ createdAt: at,
176
+ updatedAt: at,
177
+ },
178
+ { expectedVersion: 0 }
179
+ );
180
+ });
181
+ }
182
+ async function refresh(input) {
183
+ const request = plain(input);
184
+ exact(request, ['expectedVersion']);
185
+ return store.exclusive(async () => {
186
+ const state = await load(request.expectedVersion);
187
+ writable(state);
188
+ const facts = await observe(state.candidate);
189
+ return save(state, { observation: facts, proposal: null, stage: observedStage(state, facts) });
190
+ });
191
+ }
192
+ async function propose(input) {
193
+ const request = plain(input);
194
+ exact(request, ['expectedVersion', 'action', 'payload', 'expiresAt']);
195
+ ensure(ACTIONS.includes(request.action));
196
+ ensure(request.payload && typeof request.payload === 'object' && !Array.isArray(request.payload));
197
+ timestamp(request.expiresAt);
198
+ ensure(
199
+ Date.parse(request.expiresAt) > Date.parse(now()) &&
200
+ Date.parse(request.expiresAt) - Date.parse(now()) <= 3600000,
201
+ 'expired-proposal'
202
+ );
203
+ return store.exclusive(async () => {
204
+ const state = await load(request.expectedVersion);
205
+ writable(state);
206
+ ensure(state.observation !== null, 'observation-required');
207
+ allowed(state, request.action);
208
+ const facts = await observe(state.candidate);
209
+ const observed = { ...state, observation: facts };
210
+ allowed(observed, request.action);
211
+ const body = {
212
+ candidate: state.candidate,
213
+ action: request.action,
214
+ payload: request.payload,
215
+ expiresAt: request.expiresAt,
216
+ factsDigest: factsDigest(facts),
217
+ nonce: randomUUID(),
218
+ mergeReceipt: mergeReceiptFor(state.operations, request.action),
219
+ providerId,
220
+ };
221
+ const proposalDigest = hash(body);
222
+ const resource = `delivery:${proposalDigest}`;
223
+ return save(state, {
224
+ observation: facts,
225
+ proposal: {
226
+ digest: proposalDigest,
227
+ action: body.action,
228
+ payload: body.payload,
229
+ expiresAt: body.expiresAt,
230
+ factsDigest: body.factsDigest,
231
+ nonce: body.nonce,
232
+ mergeReceipt: body.mergeReceipt,
233
+ providerId,
234
+ resource,
235
+ approvalResource: `${providerId}:${body.action}:${resource}`,
236
+ },
237
+ });
238
+ });
239
+ }
240
+ function validateOutcome(receipt, operation) {
241
+ const result = validateReceipt(receipt, operation);
242
+ // Historical schema-v1 snapshots remain readable. New post-merge completion
243
+ // must attest to the actual merge result, including squash/rebase commits.
244
+ if (['deploy', 'tracker-update', 'tracker-transition'].includes(operation.action))
245
+ ensure(result.commitSha === operation.mergeReceipt?.commitSha, 'invalid-receipt');
246
+ return result;
247
+ }
248
+ async function finish(state, operation, receipt) {
249
+ const success = validateOutcome(receipt, operation);
250
+ const operations = state.operations.map((item) =>
251
+ item.digest === operation.digest ? { ...item, state: 'succeeded', receipt: success } : item
252
+ );
253
+ const stage = {
254
+ 'branch-publish': 'branch-published',
255
+ 'review-request': 'review-requested',
256
+ 'review-update': 'review-requested',
257
+ merge: 'merged',
258
+ deploy: 'deployed',
259
+ 'tracker-update': 'tracker-updated',
260
+ 'tracker-transition': 'tracker-status-confirmed',
261
+ }[operation.action];
262
+ return save(state, { operations, proposal: null, stage: operations.some(op => op.action === 'tracker-transition' && op.state === 'succeeded') ? 'tracker-status-confirmed' : stage });
263
+ }
264
+ async function execute(input) {
265
+ ensure(input && typeof input === 'object' && !Array.isArray(input));
266
+ for (const key of Reflect.ownKeys(input)) {
267
+ ensure(typeof key === 'string' && ['expectedVersion', 'proposalDigest', 'approval'].includes(key));
268
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
269
+ ensure(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
270
+ }
271
+ const approval = input.approval;
272
+ const request = plain({ expectedVersion: input.expectedVersion, proposalDigest: input.proposalDigest });
273
+ ensure(Object.keys(input).length === 3 && Object.hasOwn(input, 'approval'));
274
+ digest(request.proposalDigest);
275
+ return store.exclusive(async () => {
276
+ let state = await load(request.expectedVersion);
277
+ writable(state);
278
+ const proposal = state.proposal;
279
+ ensure(
280
+ proposal && proposal.digest === request.proposalDigest && proposal.providerId === providerId,
281
+ 'proposal-mismatch'
282
+ );
283
+ ensure(Date.parse(proposal.expiresAt) > Date.parse(now()), 'expired-proposal');
284
+ allowed(state, proposal.action);
285
+ const facts = await observe(state.candidate);
286
+ ensure(factsDigest(facts) === proposal.factsDigest, 'changed-facts');
287
+ allowed({ ...state, observation: facts }, proposal.action);
288
+ ensure(Date.parse(proposal.expiresAt) > Date.parse(now()), 'expired-proposal');
289
+ ensure(
290
+ approval?.singleUse === true && !state.usedApprovalIds.includes(approval.id),
291
+ 'approval-replayed'
292
+ );
293
+ const decision = evaluateAuthority(
294
+ authority,
295
+ {
296
+ actorId: subjectId,
297
+ action: 'provider.write',
298
+ resource: proposal.resource,
299
+ providerId,
300
+ capability: proposal.action,
301
+ },
302
+ { approval, approvalRegistry, expectedApproverId, nowMs: Date.parse(now()) }
303
+ );
304
+ ensure(decision.decision === 'allow', 'approval-required');
305
+ const operation = plain({
306
+ digest: proposal.digest,
307
+ action: proposal.action,
308
+ payload: proposal.payload,
309
+ candidate: state.candidate,
310
+ factsDigest: proposal.factsDigest,
311
+ mergeReceipt: proposal.mergeReceipt,
312
+ approvalId: approval.id,
313
+ dispatchedAt: now(),
314
+ state: 'dispatching',
315
+ receipt: null,
316
+ });
317
+ state = await save(state, {
318
+ operations: [...state.operations, operation],
319
+ usedApprovalIds: [...state.usedApprovalIds, approval.id],
320
+ stage: proposal.action === 'merge' ? 'merge-approved' : state.stage,
321
+ });
322
+ if (Date.parse(now()) >= Math.min(Date.parse(proposal.expiresAt), Date.parse(approval.expiresAt))) {
323
+ return save(state, {
324
+ operations: state.operations.map((value) =>
325
+ value.digest === operation.digest ? { ...value, state: 'not-applied' } : value
326
+ ),
327
+ proposal: null,
328
+ stage: observedStage(
329
+ { ...state, stage: proposal.action === 'merge' ? 'checks-passed' : state.stage },
330
+ facts
331
+ ),
332
+ });
333
+ }
334
+ let receipt;
335
+ try {
336
+ receipt = await bounded(() => {
337
+ const pending = inFlight;
338
+ const context = Object.freeze({ deadline: new Date(Math.min(
339
+ Date.parse(now()) + timeoutMs, Date.parse(proposal.expiresAt), Date.parse(approval.expiresAt)
340
+ )).toISOString() });
341
+ const promise = Promise.resolve().then(() => executor.dispatch(operation, context));
342
+ pending.set(operation.digest, promise);
343
+ promise.then(
344
+ () => pending.delete(operation.digest),
345
+ () => pending.delete(operation.digest)
346
+ );
347
+ return promise;
348
+ });
349
+ validateOutcome(receipt, operation);
350
+ } catch {
351
+ return save(state, {
352
+ operations: state.operations.map((value) =>
353
+ value.digest === operation.digest ? { ...value, state: 'indeterminate' } : value
354
+ ),
355
+ });
356
+ }
357
+ return finish(state, operation, receipt);
358
+ });
359
+ }
360
+ async function reconcile(input) {
361
+ const request = plain(input);
362
+ exact(request, ['expectedVersion']);
363
+ return store.exclusive(async () => {
364
+ const state = await load(request.expectedVersion);
365
+ const operation = state.operations.find((value) =>
366
+ ['dispatching', 'indeterminate'].includes(value.state)
367
+ );
368
+ ensure(operation, 'nothing-to-reconcile');
369
+ // Pending operations retain the exact approved proposal. Bind recovery to
370
+ // its provider as well as the repository kind before any external read.
371
+ ensure(state.proposal?.digest === operation.digest && state.proposal.providerId === providerId,
372
+ 'provider-mismatch');
373
+ ensure(executor.capabilities.some((value) => value.action === operation.action && value.reconcile),
374
+ 'unsupported-action');
375
+ let result;
376
+ try {
377
+ result = inFlight.has(operation.digest)
378
+ ? { status: 'unknown' }
379
+ : plain(await bounded(() => executor.reconcile(operation)));
380
+ if (result.status === 'succeeded') {
381
+ exact(result, ['status', 'receipt']);
382
+ validateOutcome(result.receipt, operation);
383
+ } else {
384
+ exact(result, ['status']);
385
+ ensure(['not-applied', 'unknown'].includes(result.status));
386
+ }
387
+ } catch {
388
+ result = { status: 'unknown' };
389
+ }
390
+ if (result.status === 'succeeded') return finish(state, operation, result.receipt);
391
+ const operations = state.operations.map((value) =>
392
+ value.digest === operation.digest
393
+ ? { ...value, state: result.status === 'not-applied' ? 'not-applied' : 'indeterminate' }
394
+ : value
395
+ );
396
+ return save(state, { operations, ...(result.status === 'not-applied' ? { proposal: null } : {}) });
397
+ });
398
+ }
399
+ return Object.freeze({ initialize, refresh, propose, execute, reconcile, status: () => store.read() });
400
+ }
@@ -0,0 +1,76 @@
1
+ import { acquireLock, recoverAbandonedLock } from '../state/lock.js';
2
+ import { createSnapshotStore, readSnapshotWithoutLock } from '../state/snapshot-store.js';
3
+ import { verifyResolvedStatePaths } from '../state/paths.js';
4
+ import { ensure, hash, plain, validateRecord } from './contract.js';
5
+ const stores = new WeakSet();
6
+ export function isDeliveryStore(value) {
7
+ return stores.has(value);
8
+ }
9
+ export function createDeliveryStore(paths, options = {}) {
10
+ const snapshots = createSnapshotStore(paths, options.snapshotOptions ?? {});
11
+ async function read() {
12
+ const snapshot = await readSnapshotWithoutLock(paths);
13
+ return snapshot === null ? null : plain({ ...validateRecord(snapshot.data), version: snapshot.version });
14
+ }
15
+ const store = Object.freeze({
16
+ read,
17
+ async recover() {
18
+ await verifyResolvedStatePaths(paths);
19
+ const operationPath = paths.operationLockPath ?? `${paths.snapshotPath}.operation.lock`;
20
+ const recoveredLocks = [];
21
+ if (await recoverAbandonedLock(operationPath, { now: options.recoveryNow })) recoveredLocks.push('operation');
22
+ const lock = await acquireLock(operationPath);
23
+ try {
24
+ await verifyResolvedStatePaths(paths);
25
+ if (await recoverAbandonedLock(paths.lockPath, { now: options.recoveryNow })) recoveredLocks.push('snapshot');
26
+ return plain({ recoveredLocks });
27
+ } finally {
28
+ await lock.release();
29
+ }
30
+ },
31
+ async write(input, { expectedVersion }) {
32
+ const value = validateRecord(input);
33
+ const prior = await read();
34
+ ensure((prior?.version ?? 0) === expectedVersion, 'version-conflict');
35
+ if (prior) {
36
+ ensure(
37
+ hash(prior.candidate) === hash(value.candidate) && value.createdAt === prior.createdAt,
38
+ 'state-conflict'
39
+ );
40
+ ensure(
41
+ value.operations.length >= prior.operations.length &&
42
+ prior.usedApprovalIds.every((id) => value.usedApprovalIds.includes(id)),
43
+ 'state-conflict'
44
+ );
45
+ for (let i = 0; i < prior.operations.length; i++) {
46
+ const old = prior.operations[i],
47
+ next = value.operations[i];
48
+ const { state: oldState, receipt: oldReceipt, ...oldBinding } = old;
49
+ const { state: nextState, receipt: nextReceipt, ...nextBinding } = next;
50
+ ensure(hash(oldBinding) === hash(nextBinding), 'state-conflict');
51
+ ensure(
52
+ oldState === 'dispatching' || oldState === 'indeterminate'
53
+ ? ['indeterminate', 'succeeded', 'not-applied'].includes(nextState)
54
+ : oldState === nextState,
55
+ 'state-conflict'
56
+ );
57
+ if (old.state === 'succeeded' || old.state === 'not-applied')
58
+ ensure(hash(old) === hash(next), 'state-conflict');
59
+ }
60
+ }
61
+ const result = await snapshots.write(value, { expectedVersion, environment: {} });
62
+ return plain({ ...result.data, version: result.version });
63
+ },
64
+ async exclusive(operation) {
65
+ await verifyResolvedStatePaths(paths);
66
+ const lock = await acquireLock(paths.operationLockPath ?? `${paths.snapshotPath}.operation.lock`);
67
+ try {
68
+ return await operation();
69
+ } finally {
70
+ await lock.release();
71
+ }
72
+ },
73
+ });
74
+ stores.add(store);
75
+ return store;
76
+ }
@@ -0,0 +1,51 @@
1
+ import { validateWorkRequest } from '../work-request/contract.js';
2
+ import { immutableJson } from '../clients/contract.js';
3
+ import { DeliveryError } from './contract.js';
4
+
5
+ const ISSUE_KEY = /^[A-Z][A-Z0-9]{0,31}-[1-9][0-9]{0,15}$/;
6
+ function requireTarget(condition) {
7
+ if (!condition) throw new DeliveryError('tracker-target');
8
+ }
9
+
10
+ function issueUrl(value, kind, issueKey) {
11
+ requireTarget(typeof value === 'string' && value.length <= 2048);
12
+ const url = new URL(value);
13
+ requireTarget(url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash);
14
+ const segments = url.pathname.split('/').filter(Boolean);
15
+ requireTarget(kind === 'jira'
16
+ ? segments.length === 2 && segments[0] === 'browse' && segments[1] === issueKey
17
+ : [3, 4].includes(segments.length) && segments[1] === 'issue' && segments[2] === issueKey);
18
+ return value;
19
+ }
20
+
21
+ /** Derive a tracker identity from the approved run; callers cannot supply a different ticket. */
22
+ export function trackerTargetFromRun(run, candidate) {
23
+ try {
24
+ requireTarget(typeof run?.runId === 'string' && run.runId.length > 0 && run.runId === candidate?.runId);
25
+ // Snapshot source data before validation so the validated identity cannot change afterward.
26
+ const request = immutableJson(run.workRequest);
27
+ validateWorkRequest(request);
28
+ requireTarget(run.featurePlan?.workRequestDigest === request.digest);
29
+ let kind = request.source.kind;
30
+ let issueKey = request.source.ref;
31
+ let url = request.source.url;
32
+ if (kind === 'host-observation') {
33
+ const sources = request.context.sources;
34
+ const primary = sources[0];
35
+ const matches = sources.filter(source => `${source.providerId}:${source.resourceId}` === request.source.ref);
36
+ requireTarget(matches.length === 1 && matches[0] === primary);
37
+ requireTarget(request.source.revision === `sha256:${primary.contentDigest}` && url === primary.url);
38
+ requireTarget(primary.content.title === request.title && primary.content.description === request.description);
39
+ requireTarget(Array.isArray(primary.content.acceptanceCriteria));
40
+ const criteria = [...new Set([...primary.content.acceptanceCriteria, ...request.context.userAcceptanceCriteria])];
41
+ requireTarget(JSON.stringify(criteria) === JSON.stringify(request.acceptanceCriteria));
42
+ kind = primary.provider;
43
+ issueKey = primary.resourceId;
44
+ url = primary.url;
45
+ }
46
+ requireTarget(['jira', 'linear'].includes(kind) && typeof issueKey === 'string' && ISSUE_KEY.test(issueKey));
47
+ return Object.freeze({ kind, issueKey, issueUrl: issueUrl(url, kind, issueKey), requestDigest: request.digest });
48
+ } catch {
49
+ throw new DeliveryError('tracker-target');
50
+ }
51
+ }