@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,78 @@
1
+ import { immutableJson } from './contract.js';
2
+
3
+ const AGENT_RESULT_CONTRACT = immutableJson({
4
+ version: 1,
5
+ kind: 'agilno.agent-result',
6
+ framing: 'Emit exactly one JSON object matching this result schema on stdout. Emit no Markdown or commentary. For a successful result, output.evidence must contain exactly the evidence identifiers declared by the launch contract, with no invented or omitted identifiers.',
7
+ schema: {
8
+ $schema: 'http://json-schema.org/draft-07/schema#',
9
+ title: 'agilno.agent-result',
10
+ type: 'object',
11
+ additionalProperties: false,
12
+ required: ['version', 'status', 'output', 'usage'],
13
+ properties: {
14
+ version: { const: 1 },
15
+ status: { enum: ['success', 'retry', 'failed', 'blocked', 'budget-exhausted'] },
16
+ output: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ required: ['summary', 'evidence'],
20
+ properties: {
21
+ summary: { type: 'string', minLength: 1, maxLength: 16_384 },
22
+ evidence: {
23
+ type: 'array',
24
+ maxItems: 64,
25
+ uniqueItems: true,
26
+ items: {
27
+ type: 'string',
28
+ minLength: 1,
29
+ maxLength: 64,
30
+ pattern: '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$',
31
+ },
32
+ },
33
+ },
34
+ },
35
+ usage: {
36
+ type: 'object',
37
+ additionalProperties: false,
38
+ required: ['tokens', 'costUsd'],
39
+ properties: {
40
+ tokens: { type: 'integer', minimum: 0, maximum: 10_000_000 },
41
+ costUsd: { type: 'number', minimum: 0, maximum: 100_000 },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ });
47
+
48
+ export function agentResultContract(evidence = undefined) {
49
+ if (evidence === undefined) return AGENT_RESULT_CONTRACT;
50
+ if (!Array.isArray(evidence) || evidence.length > 64
51
+ || evidence.some(item => typeof item !== 'string' || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(item))) {
52
+ throw new TypeError('Agent result evidence is invalid.');
53
+ }
54
+ return immutableJson({
55
+ ...AGENT_RESULT_CONTRACT,
56
+ schema: {
57
+ ...AGENT_RESULT_CONTRACT.schema,
58
+ properties: {
59
+ ...AGENT_RESULT_CONTRACT.schema.properties,
60
+ output: {
61
+ ...AGENT_RESULT_CONTRACT.schema.properties.output,
62
+ properties: {
63
+ ...AGENT_RESULT_CONTRACT.schema.properties.output.properties,
64
+ evidence: {
65
+ ...AGENT_RESULT_CONTRACT.schema.properties.output.properties.evidence,
66
+ minItems: evidence.length,
67
+ maxItems: evidence.length,
68
+ items: {
69
+ ...AGENT_RESULT_CONTRACT.schema.properties.output.properties.evidence.items,
70
+ enum: [...evidence],
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ });
78
+ }
@@ -0,0 +1,80 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpath } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
5
+ import { createAuthorityEnvelope } from '../policy/authority.js';
6
+ import { createDeliveryService } from '../delivery/service.js';
7
+ import { createBranchPublicationExecutor, publicationPayload } from '../delivery/branch-publication.js';
8
+ import { createGitPublicationTransport, validatePublicationExecutable } from '../delivery/publication-transport.js';
9
+ import { runPublicationProcess } from '../delivery/publication-process.js';
10
+ import { parseRepositoryRemote } from '../repositories/identity.js';
11
+ import { ensure, hash } from '../delivery/contract.js';
12
+ const ENDPOINTS={github:'https://api.github.com',gitlab:'https://gitlab.com/api/v4',bitbucket:'https://api.bitbucket.org/2.0'};
13
+ export function publicationProvider(config,repository,flags,writing) {
14
+ const candidates=config.providers.providers.filter(p=>p.kind==='git-ci'&&p.mode!=='disabled'&&(!writing||p.mode==='read-write-with-approval')&&(p.transport??'direct-api')==='direct-api'&&p.endpoint?.replace(/\/$/,'')===ENDPOINTS[repository.provider]&&['repository-read','branch-publish'].every(c=>p.capabilities.includes(c))&&(!p.projectIds?.length||p.projectIds.includes(config.project.id))&&(!p.resourceIds?.length||p.resourceIds.includes(repository.fullName))&&(flags.provider===undefined||flags.provider===p.id));
15
+ ensure(candidates.length===1,'publication-provider-required');return candidates[0];
16
+ }
17
+ export function publicationToken(provider,kind,environment) {
18
+ const keys=Object.keys(provider.credentials??{});ensure(keys.length===1&&['tokenEnv','accessTokenEnv','apiTokenEnv'].includes(keys[0]),'invalid-credentials');
19
+ if(kind==='bitbucket')ensure(keys[0]==='accessTokenEnv','bitbucket-access-token-required');
20
+ const name=provider.credentials[keys[0]];ensure(typeof name==='string'&&/^[A-Z][A-Z0-9_]{1,127}$/.test(name),'invalid-credentials');
21
+ return environment[name];
22
+ }
23
+ export async function inspectPublicationSource({project,gitExecutable,repository,runner=runPublicationProcess}) {
24
+ await validatePublicationExecutable(gitExecutable,project);
25
+ const run=async args=>{
26
+ const result=await runner(gitExecutable,args,{cwd:project,env:{PATH:'/usr/bin:/bin',HOME:'/nonexistent',GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',GIT_CONFIG_NOSYSTEM:'1',GIT_TERMINAL_PROMPT:'0'},timeoutMs:3000,maxOutputBytes:32768});
27
+ ensure(result.code===0&&!result.reason,'publication-source-unavailable');return result.stdout.trim();
28
+ };
29
+ const remoteOutput=await run(['remote','-v']);const matching=new Set();const pushes=[];
30
+ for(const line of remoteOutput.split('\n').filter(Boolean)) {
31
+ const match=/^(\S+)\s+(\S+) \((fetch|push)\)$/.exec(line);ensure(match,'invalid-remote');
32
+ if(match[3]==='fetch') {try {if(hash(parseRepositoryRemote(match[2]))===hash(repository))matching.add(match[1]);}catch{}}
33
+ else pushes.push({name:match[1],url:match[2]});
34
+ }
35
+ ensure(matching.size>0,'publication-remote-missing');
36
+ for(const name of matching) {
37
+ const selected=pushes.filter(p=>p.name===name);ensure(selected.length===1,'conflicting-push-url');
38
+ ensure(hash(parseRepositoryRemote(selected[0].url))===hash(repository),'conflicting-push-url');
39
+ }
40
+ const objects=await run(['rev-parse','--git-path','objects']);ensure(objects&&!/[\u0000\r\n]/.test(objects),'unsafe-object-store');
41
+ const sourceObjects=resolve(project,objects);ensure(await realpath(sourceObjects)===sourceObjects,'unsafe-object-store');
42
+ return {sourceObjects,remoteDigest:hash(pushes.filter(p=>matching.has(p.name)).sort((a,b)=>a.name.localeCompare(b.name)))};
43
+ }
44
+ async function publicationDelivery({action,store,config,flags,dependencies,validateLocal,reloadConfig,publicationContext,confirm}) {
45
+ let state=await store.read();const writing=action==='publish';
46
+ const provider=structuredClone(publicationProvider(config,state.candidate.repository,flags,writing));
47
+ const projectDigest=hash(config.project);
48
+ const source=await inspectPublicationSource({...publicationContext,repository:state.candidate.repository,runner:dependencies.delivery?.publicationSourceRunner});
49
+ const factory=dependencies.delivery?.publicationTransportFactory??createGitPublicationTransport;
50
+ const token=publicationToken(provider,state.candidate.repository.provider,dependencies.env);
51
+ const transport=await factory({...publicationContext,repository:state.candidate.repository,sourceObjects:source.sourceObjects,token, signal: dependencies.publicationSignal});
52
+ const executor=createBranchPublicationExecutor({repository:state.candidate.repository,transport});
53
+ const service=createDeliveryService({store,executor,timeoutMs:120000,providerId:provider.id,subjectId:'delivery-cli',expectedApproverId:'terminal-human',approvalRegistry:createApprovalRegistry({approvers:[{id:'terminal-human',principal:'human'}]}),authority:createAuthorityEnvelope({actorId:'delivery-cli',principal:'agent',actions:writing?['provider.write']:[],ownedPaths:[],commands:[],providers:writing?[{id:provider.id,mode:provider.mode,capabilities:['branch-publish']}]:[]})});
54
+ if(action==='reconcile')return service.reconcile({expectedVersion:state.version});
55
+ await validateLocal(state.candidate);
56
+ state=await service.refresh({expectedVersion:state.version});
57
+ const publication=state.observation.publication;
58
+ if(publication.remoteSha===state.candidate.headSha) {dependencies.output.log('The exact verified commit is already available at the destination branch. No publication was dispatched.');return state;}
59
+ ensure(publication.remoteSha===null,'branch-exists');
60
+ const expiresAt=new Date(Date.now()+300000).toISOString();
61
+ state=await service.propose({expectedVersion:state.version,action:'branch-publish',payload:publicationPayload(state.candidate),expiresAt});
62
+ dependencies.output.log(`Publish ${state.candidate.headSha} to ${publication.destinationUrl}`);
63
+ dependencies.output.log(`Create only: ${publication.ref}. An existing branch will never be overwritten. This uploads the commit and its reachable history; it does not create or merge a review.`);
64
+ ensure(await confirm(state),'approval-required');
65
+ const currentConfig=await reloadConfig();const currentProvider=publicationProvider(currentConfig,state.candidate.repository,flags,true);
66
+ ensure(hash(currentProvider)===hash(provider)&&hash(currentConfig.project)===projectDigest,'changed-configuration');
67
+ ensure(publicationToken(currentProvider,state.candidate.repository.provider,dependencies.env)===token,'changed-credentials');
68
+ await validateLocal(state.candidate);
69
+ const currentSource=await inspectPublicationSource({...publicationContext,repository:state.candidate.repository,runner:dependencies.delivery?.publicationSourceRunner});
70
+ ensure(hash(currentSource)===hash(source),'changed-publication-source');
71
+ const approval=createApprovalReceipt({id:`publish-${randomUUID()}`,approverId:'terminal-human',approverPrincipal:'human',subjectId:'delivery-cli',action:'provider.write',resource:state.proposal.approvalResource,policyId:'authority.external-write',decision:'approved',expiresAt,singleUse:true});
72
+ return service.execute({expectedVersion:state.version,proposalDigest:state.proposal.digest,approval});
73
+ }
74
+
75
+ export async function runPublicationDelivery(input) {
76
+ const controller=new AbortController();const abort=()=>controller.abort();
77
+ process.once('SIGINT',abort);process.once('SIGTERM',abort);
78
+ try {return await publicationDelivery({...input,dependencies:{...input.dependencies,publicationSignal:controller.signal}});}
79
+ finally {process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);}
80
+ }
@@ -0,0 +1,230 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliError } from '../cli/output.js';
3
+ import { createNodeProviderTransport } from '../adapters/node-transport.js';
4
+ import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
5
+ import { createAuthorityEnvelope } from '../policy/authority.js';
6
+ import { hash, plain } from '../delivery/contract.js';
7
+ import { createDeliveryService } from '../delivery/service.js';
8
+
9
+ function fail(message, code = 'REPOSITORY_CONFLICT') {
10
+ throw new CliError(message, code);
11
+ }
12
+ const ENDPOINTS = Object.freeze({ github: 'https://api.github.com', gitlab: 'https://gitlab.com/api/v4', bitbucket: 'https://api.bitbucket.org/2.0' });
13
+ export function providerFor(config, repository, flags, writing, operation = 'merge') {
14
+ const providers = config.providers.providers.filter(
15
+ (provider) =>
16
+ provider.kind === 'git-ci' &&
17
+ provider.mode !== 'disabled' &&
18
+ (!writing || provider.mode === 'read-write-with-approval') &&
19
+ (provider.transport ?? 'direct-api') === 'direct-api' &&
20
+ ['repository-read', ...(operation === 'deploy' ? ['deployments-read', 'actions-read'] : ['review-request', 'review-update'].includes(operation) ? [] : ['checks-read']), ...(writing ? [operation] : [])].every((cap) =>
21
+ provider.capabilities.includes(cap)
22
+ ) &&
23
+ (!provider.projectIds?.length || provider.projectIds.includes(config.project.id)) &&
24
+ (!provider.resourceIds?.length || provider.resourceIds.includes(repository.fullName)) &&
25
+ provider.endpoint?.replace(/\/$/, '') === ENDPOINTS[repository.provider] &&
26
+ (flags.provider === undefined || provider.id === flags.provider)
27
+ );
28
+ if (providers.length !== 1)
29
+ fail(
30
+ 'Configure one scoped repository API provider. Review operations need repository-read and review-request or review-update; merge needs repository-read/checks-read; deployment needs repository-read/deployments-read/actions-read. Writes require the action capability and read-write-with-approval mode.',
31
+ 'MISSING_CONFIGURATION'
32
+ );
33
+ return providers[0];
34
+ }
35
+ export function headers(provider, environment, kind) {
36
+ const credentials = provider.credentials ?? {};
37
+ const keys = Object.keys(credentials);
38
+ if (
39
+ keys.length !== 1 ||
40
+ !['tokenEnv', 'accessTokenEnv', 'apiTokenEnv'].includes(keys[0]) ||
41
+ !/^[A-Z][A-Z0-9_]{1,127}$/.test(credentials[keys[0]])
42
+ )
43
+ fail('Configure one token environment reference for repository delivery.', 'MISSING_CONFIGURATION');
44
+ if (kind === 'bitbucket' && keys[0] !== 'accessTokenEnv')
45
+ fail('Bitbucket review operations require accessTokenEnv for a Bearer access token. API tokens use a different authentication scheme and are not supported here.', 'MISSING_CONFIGURATION');
46
+ const token = environment[credentials[keys[0]]];
47
+ if (typeof token !== 'string' || !token || token.length > 8192 || /[\s\u0000-\u001f\u007f]/.test(token))
48
+ fail('The configured repository token is missing or invalid.', 'PROVIDER_UNAVAILABLE');
49
+ return {
50
+ authorization: `Bearer ${token}`,
51
+ accept: kind === 'github' ? 'application/vnd.github+json' : 'application/json',
52
+ };
53
+ }
54
+ async function confirm(dependencies, preview) {
55
+ let timer;
56
+ try {
57
+ return (
58
+ (await Promise.race([
59
+ Promise.resolve().then(() => dependencies.confirmDelivery(preview)),
60
+ new Promise((resolve) => {
61
+ timer = setTimeout(() => resolve(false), 30000);
62
+ }),
63
+ ])) === true
64
+ );
65
+ } catch {
66
+ return false;
67
+ } finally {
68
+ clearTimeout(timer);
69
+ }
70
+ }
71
+
72
+ export async function runRemoteDelivery({ action, store, config, flags, dependencies, validateLocal, loadTrackerTarget, reloadConfig = async () => config, publicationContext }) {
73
+ if (action === 'review') action = 'review-request';
74
+ const writing = ['publish', 'review-request', 'review-update', 'merge', 'deploy', 'tracker-update', 'tracker-transition'].includes(action);
75
+ if (!['publish', 'review-request', 'review-update', 'merge', 'deploy', 'tracker-update', 'tracker-status', 'tracker-transition', 'refresh', 'reconcile'].includes(action))
76
+ fail('Unsupported delivery action.', 'INVALID_INPUT');
77
+ if (
78
+ writing &&
79
+ (flags.json ||
80
+ dependencies.terminalIsInteractive?.() !== true ||
81
+ typeof dependencies.confirmDelivery !== 'function')
82
+ )
83
+ fail(
84
+ 'Run delivery writes in an interactive terminal to review and approve the exact operation. JSON and unattended writes are unavailable.',
85
+ 'INVALID_INPUT'
86
+ );
87
+ let state = await store.read();
88
+ if (!state) fail('Run rivet delivery prepare after verification first.');
89
+ const pendingAction = state.operations.find(op => ['dispatching','indeterminate'].includes(op.state))?.action;
90
+ if (action === 'review-update' || (action === 'reconcile' && pendingAction === 'review-update')) {
91
+ const {runReviewUpdate} = await import('./delivery-review-update.js');
92
+ return runReviewUpdate({action,state,store,config,flags,dependencies,reloadConfig,validateLocal,confirm:preview=>confirm(dependencies,preview)});
93
+ }
94
+ if (action === 'publish' || (action === 'reconcile' && pendingAction === 'branch-publish')) {
95
+ const {runPublicationDelivery} = await import('./delivery-publish.js');
96
+ return runPublicationDelivery({action,store,config,flags,dependencies,validateLocal,reloadConfig,publicationContext,confirm: preview => confirm(dependencies,preview)});
97
+ }
98
+ if (['tracker-status', 'tracker-transition'].includes(action) || (action === 'reconcile' && pendingAction === 'tracker-transition')) {
99
+ const {runTrackerTransition} = await import('./delivery-transition.js');
100
+ return runTrackerTransition({action,state,store,config,flags,dependencies,reloadConfig,loadTrackerTarget,
101
+ confirm: preview => confirm(dependencies,preview)});
102
+ }
103
+ if (action === 'tracker-update' || (action === 'reconcile' && pendingAction === 'tracker-update')) {
104
+ const {runTrackerDelivery} = await import('./delivery-tracker.js');
105
+ return runTrackerDelivery({action,state,store,config,flags,dependencies,reloadConfig,loadTrackerTarget,
106
+ confirm: preview => confirm(dependencies,preview)});
107
+ }
108
+ const kind = state.candidate.repository.provider;
109
+ if (!Object.hasOwn(ENDPOINTS, kind))
110
+ fail('Native repository delivery supports GitHub.com, GitLab.com and Bitbucket Cloud review creation.', 'PROVIDER_UNAVAILABLE');
111
+ if (action === 'merge' && kind === 'gitlab' && flags.method !== undefined && flags.method !== 'merge')
112
+ fail(
113
+ 'GitLab currently supports --method=merge only; squash and rebase are unavailable.',
114
+ 'INVALID_INPUT'
115
+ );
116
+ if (writing && state.operations.some((op) => op.action === action && op.state === 'succeeded'))
117
+ return state;
118
+ const operation = action === 'reconcile'
119
+ ? state.operations.find(op => ['dispatching', 'indeterminate'].includes(op.state))?.action
120
+ : action;
121
+ if (kind === 'bitbucket' && operation !== 'review-request')
122
+ fail('Native Bitbucket delivery currently supports review creation and its reconciliation only. Native merge and deployment are unavailable.', 'PROVIDER_UNAVAILABLE');
123
+ const deploying = operation === 'deploy';
124
+ const reviewing = operation === 'review-request';
125
+ let deployment;
126
+ let mergeReceipt;
127
+ if (deploying) {
128
+ if (kind !== 'github' || !config.project.deployment)
129
+ fail('Configure project.deployment for GitHub Actions deployment first.', 'MISSING_CONFIGURATION');
130
+ deployment = plain(config.project.deployment);
131
+ if (flags.provider !== undefined && flags.provider !== deployment.providerId)
132
+ fail('Deployment uses the provider declared in project.deployment.', 'INVALID_INPUT');
133
+ mergeReceipt = state.operations.find(op => op.action === 'merge' && op.state === 'succeeded')?.receipt;
134
+ if (!mergeReceipt) fail('Deployment requires a confirmed merge receipt.');
135
+ }
136
+ const selectedFlags = deploying ? { ...flags, provider: deployment.providerId } : flags;
137
+ const provider = providerFor(config, state.candidate.repository, selectedFlags, writing, deploying ? 'deploy' : reviewing ? 'review-request' : 'merge');
138
+ const auth = headers(provider, dependencies.env, kind);
139
+ const executorFactory =
140
+ (deploying ? dependencies.delivery?.deploymentExecutorFactory : reviewing ? dependencies.delivery?.reviewExecutorFactory : dependencies.delivery?.executorFactory) ??
141
+ (deploying ? (await import('../delivery/github-deployment.js')).createGithubDeploymentExecutor : reviewing
142
+ ? (kind === 'github' ? (await import('../delivery/github-review.js')).createGithubReviewExecutor : kind === 'bitbucket' ? (await import('../delivery/bitbucket-review.js')).createBitbucketReviewExecutor : (await import('../delivery/gitlab-review.js')).createGitlabReviewExecutor) :
143
+ (kind === 'github'
144
+ ? (await import('../delivery/github.js')).createGithubDeliveryExecutor
145
+ : (await import('../delivery/gitlab.js')).createGitlabDeliveryExecutor));
146
+ const executor = executorFactory({
147
+ repository: state.candidate.repository,
148
+ ...(deploying ? {deployment, mergeReceipt} : {}),
149
+ headers: auth,
150
+ transport: dependencies.delivery?.transport ?? createNodeProviderTransport(),
151
+ });
152
+ const service = createDeliveryService({
153
+ store,
154
+ executor,
155
+ timeoutMs: 120000,
156
+ providerId: provider.id,
157
+ subjectId: 'delivery-cli',
158
+ expectedApproverId: 'terminal-human',
159
+ authority: createAuthorityEnvelope({
160
+ actorId: 'delivery-cli',
161
+ principal: 'agent',
162
+ actions: writing ? ['provider.write'] : [],
163
+ ownedPaths: [],
164
+ commands: [],
165
+ providers: writing ? [{ id: provider.id, mode: provider.mode, capabilities: [action] }] : [],
166
+ }),
167
+ approvalRegistry: createApprovalRegistry({ approvers: [{ id: 'terminal-human', principal: 'human' }] }),
168
+ });
169
+ if (action === 'reconcile') return service.reconcile({ expectedVersion: state.version });
170
+ if (action === 'merge' || reviewing) await validateLocal(state.candidate);
171
+ state = await service.refresh({ expectedVersion: state.version });
172
+ if (!writing) return state;
173
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
174
+ state = await service.propose({
175
+ expectedVersion: state.version,
176
+ action,
177
+ expiresAt,
178
+ payload: reviewing ? (await import('../delivery/review-request.js')).reviewRequestPayload(state.candidate) : deploying ? {workflow: deployment.workflow, environment: deployment.environment, productionEnvironment: deployment.productionEnvironment} : {
179
+ reviewNumber: state.observation.review?.number,
180
+ mergeMethod: flags.method ?? (kind === 'github' ? 'squash' : 'merge'),
181
+ },
182
+ });
183
+ if (reviewing) {
184
+ const content = (await import('../delivery/review-request.js')).reviewRequestContent({ ...state.proposal, digest: state.proposal.digest });
185
+ dependencies.output.log(`Create review: ${state.candidate.repository.url}`);
186
+ dependencies.output.log(`${state.candidate.sourceBranch} (${state.candidate.headSha}) -> ${state.candidate.targetBranch} (${state.observation.baseSha})`);
187
+ dependencies.output.log('Branch selection is not atomic. Rivet verifies the created review afterward; a changed or uncertain result requires reconciliation.');
188
+ dependencies.output.log(`Title: ${content.title}`);
189
+ dependencies.output.log(`Body:\n${content.body}`);
190
+ } else if (deploying) {
191
+ dependencies.output.log(`Deploy ${mergeReceipt.commitSha} to ${deployment.environment}`);
192
+ dependencies.output.log(`Workflow: ${deployment.workflow}. Production environment: ${deployment.productionEnvironment}.`);
193
+ } else {
194
+ dependencies.output.log(`Merge ${state.observation.review.url}`);
195
+ dependencies.output.log(
196
+ `${state.candidate.sourceBranch} (${state.candidate.headSha}) -> ${state.candidate.targetBranch} (${state.observation.baseSha})`
197
+ );
198
+ dependencies.output.log(
199
+ `Method: ${state.proposal.payload.mergeMethod}. Required checks and supported review policy passed.`
200
+ );
201
+ }
202
+ if (!(await confirm(dependencies, state)))
203
+ fail('Delivery operation was not approved. Nothing was dispatched.', 'INVALID_INPUT');
204
+ if (reviewing) {
205
+ const currentConfig = await reloadConfig();
206
+ const currentProvider = providerFor(currentConfig, state.candidate.repository, selectedFlags, true, 'review-request');
207
+ if (hash(currentProvider) !== hash(provider) || hash(currentConfig.project) !== hash(config.project))
208
+ fail('Review configuration changed. Review a new proposal.');
209
+ }
210
+ if (deploying) {
211
+ const currentConfig = await reloadConfig();
212
+ const currentProvider = providerFor(currentConfig, state.candidate.repository, selectedFlags, true, 'deploy');
213
+ if (hash(currentConfig.project.deployment) !== hash(deployment) || hash(currentProvider) !== hash(provider))
214
+ fail('Deployment configuration changed. Review a new proposal.');
215
+ }
216
+ if (action === 'merge' || reviewing) await validateLocal(state.candidate);
217
+ const approval = createApprovalReceipt({
218
+ id: `${action}-${randomUUID()}`,
219
+ approverId: 'terminal-human',
220
+ approverPrincipal: 'human',
221
+ subjectId: 'delivery-cli',
222
+ action: 'provider.write',
223
+ resource: state.proposal.approvalResource,
224
+ policyId: 'authority.external-write',
225
+ decision: 'approved',
226
+ expiresAt,
227
+ singleUse: true,
228
+ });
229
+ return service.execute({ expectedVersion: state.version, proposalDigest: state.proposal.digest, approval });
230
+ }
@@ -0,0 +1,39 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliError } from '../cli/output.js';
3
+ import { createNodeProviderTransport } from '../adapters/node-transport.js';
4
+ import { createDeliveryService } from '../delivery/service.js';
5
+ import { createReviewUpdateExecutor, reviewUpdateContent } from '../delivery/review-update.js';
6
+ import { hash } from '../delivery/contract.js';
7
+ import { createAuthorityEnvelope } from '../policy/authority.js';
8
+ import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
9
+ import { providerFor, headers } from './delivery-remote.js';
10
+ function fail(message,code='REPOSITORY_CONFLICT'){throw new CliError(message,code);}
11
+ export async function runReviewUpdate({action,state,store,config,flags,dependencies,reloadConfig,validateLocal,confirm}){
12
+ const writing=action==='review-update',pending=state.operations.find(op=>['dispatching','indeterminate'].includes(op.state));
13
+ if(writing&&pending)fail('Reconcile the pending delivery operation before updating review metadata.');
14
+ if(state.operations.some(op=>op.action==='merge'&&op.state==='succeeded'))fail('Review metadata updates require an open unmerged candidate.');
15
+ let desired;if(writing){try{desired=reviewUpdateContent({title:flags.title,body:flags.body},state.candidate.repository.provider);}catch{fail('Review update requires an exact --title and --body. Only bounded title/body text can be changed.','INVALID_INPUT');}}
16
+ let number=state.candidate.reviewNumber??state.observation?.review?.number;
17
+ if(!number){const receipt=state.operations.find(op=>op.action==='review-request'&&op.state==='succeeded')?.receipt;const prefix=`${state.candidate.repository.url}/${{github:'pull',gitlab:'-/merge_requests',bitbucket:'pull-requests'}[state.candidate.repository.provider]}/`;if(receipt?.resourceUrl?.startsWith(prefix))number=Number(receipt.resourceUrl.slice(prefix.length));}
18
+ let selectedId=flags.provider;
19
+ if(!writing){if(pending?.action!=='review-update'||state.proposal?.digest!==pending.digest)fail('No pending review metadata update was found.');number=pending.payload.before.review.number;if(selectedId!==undefined&&selectedId!==state.proposal.providerId)fail('Reconciliation requires the originally approved provider.');selectedId=state.proposal.providerId;}
20
+ if(!Number.isSafeInteger(number)||number<=0)fail('Prepare or create the exact review request before updating it.');
21
+ const provider=providerFor(config,state.candidate.repository,{...flags,provider:selectedId},writing,'review-update');
22
+ const prepared=(dependencies.delivery?.reviewUpdateFactory??createReviewUpdateExecutor)({repository:state.candidate.repository,reviewNumber:number,headers:headers(provider,dependencies.env,state.candidate.repository.provider),transport:dependencies.delivery?.transport??createNodeProviderTransport()});
23
+ const service=createDeliveryService({store,executor:prepared.executor,providerId:provider.id,timeoutMs:120000,subjectId:'delivery-cli',expectedApproverId:'terminal-human',authority:createAuthorityEnvelope({actorId:'delivery-cli',principal:'agent',actions:writing?['provider.write']:[],ownedPaths:[],commands:[],providers:writing?[{id:provider.id,mode:provider.mode,capabilities:['review-update']}]:[]}),approvalRegistry:createApprovalRegistry({approvers:[{id:'terminal-human',principal:'human'}]})});
24
+ if(!writing)return service.reconcile({expectedVersion:state.version});
25
+ await validateLocal(state.candidate);
26
+ const payload=await prepared.prepare(state.candidate,desired);
27
+ if(hash(payload.desired)===hash({title:payload.before.review.title,body:payload.before.review.body}))return {...state,reviewUpdateNoop:true};
28
+ state=await service.refresh({expectedVersion:state.version});
29
+ const expiresAt=new Date(Date.now()+5*60*1000).toISOString();state=await service.propose({expectedVersion:state.version,action:'review-update',payload,expiresAt});
30
+ dependencies.output.log(`Update review: ${payload.before.review.url}\n${state.candidate.sourceBranch} (${state.candidate.headSha}) -> ${state.candidate.targetBranch} (${payload.before.baseSha})`);
31
+ dependencies.output.log(`Previous title: ${payload.before.review.title}\nPrevious body:\n${payload.before.review.body}\nNew title: ${payload.desired.title}\nNew body:\n${payload.desired.body}`);
32
+ dependencies.output.log('Assurance: precheck and readback, without atomic metadata or commit conditions. Concurrent edits may be overwritten. Confirmation observes the desired state and does not prove exclusive causation.');
33
+ if(!(await confirm(state)))fail('Review metadata update was not approved. No write was sent.','INVALID_INPUT');
34
+ const currentConfig=await reloadConfig(),currentProvider=providerFor(currentConfig,state.candidate.repository,{provider:provider.id},true,'review-update');
35
+ if(hash(currentProvider)!==hash(provider)||hash(currentConfig.project)!==hash(config.project))fail('Review configuration changed. Review a new proposal.');
36
+ await validateLocal(state.candidate);
37
+ const approval=createApprovalReceipt({id:`review-update-${randomUUID()}`,approverId:'terminal-human',approverPrincipal:'human',subjectId:'delivery-cli',action:'provider.write',resource:state.proposal.approvalResource,policyId:'authority.external-write',decision:'approved',expiresAt,singleUse:true});
38
+ return service.execute({expectedVersion:state.version,proposalDigest:state.proposal.digest,approval});
39
+ }
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliError } from '../cli/output.js';
3
+ import { createNodeProviderTransport } from '../adapters/node-transport.js';
4
+ import { createDeliveryService } from '../delivery/service.js';
5
+ import { hash, plain } from '../delivery/contract.js';
6
+ import { createAuthorityEnvelope } from '../policy/authority.js';
7
+ import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
8
+
9
+ function fail(message, code = 'MISSING_CONFIGURATION') { throw new CliError(message, code); }
10
+ export function selectProvider(config, target, selectedId, writing, operation = 'tracker-update') {
11
+ const reading = operation === 'tracker-transition' ? 'transitions-read' : 'comments-read';
12
+ const matches = config.providers.providers.filter(provider =>
13
+ provider.kind === target.kind && provider.mode !== 'disabled' &&
14
+ (!writing || provider.mode === 'read-write-with-approval') &&
15
+ (provider.transport ?? 'direct-api') === 'direct-api' &&
16
+ ['issues-read', reading, ...(writing ? [operation] : [])].every(cap => provider.capabilities.includes(cap)) &&
17
+ (!provider.projectIds?.length || provider.projectIds.includes(config.project.id)) &&
18
+ (!provider.resourceIds?.length || provider.resourceIds.includes(target.issueKey)) &&
19
+ (selectedId === undefined || selectedId === provider.id));
20
+ if (matches.length !== 1) fail(`Configure one scoped direct Jira/Linear provider with issues-read and ${reading}. Writing also requires ${operation} and read-write-with-approval. Use --provider when ambiguous.`);
21
+ const provider = matches[0];
22
+ const validEndpoint = target.kind === 'jira'
23
+ ? /^https:\/\/[a-z0-9][a-z0-9-]*\.atlassian\.net$/.test(provider.endpoint ?? '') && target.issueUrl === `${provider.endpoint}/browse/${target.issueKey}`
24
+ : provider.endpoint === 'https://api.linear.app';
25
+ if (!validEndpoint) fail('Tracker endpoint must match the recorded Jira Cloud site or the Linear API.');
26
+ return provider;
27
+ }
28
+ export function authHeaders(provider, env) {
29
+ const credentials = provider.credentials ?? {}, keys = Object.keys(credentials);
30
+ const value = name => {
31
+ if (typeof name !== 'string' || !/^[A-Z][A-Z0-9_]{1,127}$/.test(name)) fail('Configure tracker credential environment references.');
32
+ const text = env[name];
33
+ if (typeof text !== 'string' || !text || text.length > 8192 || /[\s\u0000-\u001f\u007f]/.test(text))
34
+ fail('Configured tracker credentials are missing or invalid.', 'PROVIDER_UNAVAILABLE');
35
+ return text;
36
+ };
37
+ if (provider.kind === 'jira') {
38
+ if (keys.length !== 2 || !keys.includes('usernameEnv') || !keys.includes('apiTokenEnv')) fail('Jira delivery requires usernameEnv and apiTokenEnv.');
39
+ const username = value(credentials.usernameEnv);
40
+ if (username.includes(':')) fail('Jira username is invalid.');
41
+ return {authorization: `Basic ${Buffer.from(`${username}:${value(credentials.apiTokenEnv)}`).toString('base64')}`, accept:'application/json'};
42
+ }
43
+ if (keys.length !== 1 || !['apiTokenEnv','accessTokenEnv','tokenEnv'].includes(keys[0])) fail('Configure one Linear API key or OAuth token reference.');
44
+ const token = value(credentials[keys[0]]);
45
+ return {authorization: keys[0] === 'accessTokenEnv' ? `Bearer ${token}` : token, accept:'application/json'};
46
+ }
47
+
48
+ export async function runTrackerDelivery({action, state, store, config, flags, dependencies, reloadConfig, loadTrackerTarget, confirm}) {
49
+ const writing = action === 'tracker-update';
50
+ if (writing && state.operations.some(op => op.action === 'tracker-update' && op.state === 'succeeded')) return state;
51
+ const pending = state.operations.find(op => ['dispatching','indeterminate'].includes(op.state));
52
+ if (writing && pending) fail('Reconcile the pending delivery operation before posting a tracker update.', 'REPOSITORY_CONFLICT');
53
+ const mergeReceipt = state.operations.find(op => op.action === 'merge' && op.state === 'succeeded')?.receipt;
54
+ if (!mergeReceipt) fail('A tracker delivery summary requires a confirmed merge.', 'REPOSITORY_CONFLICT');
55
+ const deploymentReceipt = state.operations.find(op => op.action === 'deploy' && op.state === 'succeeded')?.receipt ?? null;
56
+ let target, selectedId = flags.provider;
57
+ if (writing) {
58
+ if (typeof loadTrackerTarget !== 'function') fail('The run must contain a recorded Jira or Linear source ticket.');
59
+ try { target = plain(await loadTrackerTarget(state.candidate)); }
60
+ catch { fail('This run has no validated Jira/Linear source ticket. Tracker updates require the recorded source; a replacement ticket cannot be supplied.'); }
61
+ } else {
62
+ if (pending?.action !== 'tracker-update' || state.proposal?.digest !== pending.digest) fail('No pending tracker update was found.', 'REPOSITORY_CONFLICT');
63
+ const p = pending.payload;
64
+ target = plain({kind:p.kind,issueKey:p.issueKey,issueUrl:p.issueUrl,requestDigest:p.requestDigest});
65
+ if (selectedId !== undefined && selectedId !== state.proposal.providerId) fail('Reconciliation requires the originally approved tracker provider.');
66
+ selectedId = state.proposal.providerId;
67
+ }
68
+ const provider = selectProvider(config, target, selectedId, writing);
69
+ if (!writing && pending.payload.endpoint !== provider.endpoint) fail('Restore the originally approved tracker endpoint before reconciliation.');
70
+ const factory = dependencies.delivery?.trackerFactory ?? (await import('../delivery/tracker.js')).createTrackerDelivery;
71
+ const prepared = await factory({repository:state.candidate.repository,target,mergeReceipt,deploymentReceipt,
72
+ providerId:provider.id,baseUrl:provider.endpoint,headers:authHeaders(provider,dependencies.env),
73
+ transport:dependencies.delivery?.transport ?? createNodeProviderTransport(),
74
+ ...(!writing ? {persistedPayload:pending.payload} : {})});
75
+ const service = createDeliveryService({store,executor:prepared.executor,providerId:provider.id,timeoutMs:120000,
76
+ subjectId:'delivery-cli',expectedApproverId:'terminal-human',
77
+ authority:createAuthorityEnvelope({actorId:'delivery-cli',principal:'agent',actions:writing?['provider.write']:[],ownedPaths:[],commands:[],
78
+ providers:writing?[{id:provider.id,mode:provider.mode,capabilities:['tracker-update']}]:[]}),
79
+ approvalRegistry:createApprovalRegistry({approvers:[{id:'terminal-human',principal:'human'}]})});
80
+ if (!writing) return service.reconcile({expectedVersion:state.version});
81
+ state = await service.refresh({expectedVersion:state.version});
82
+ const expiresAt = new Date(Date.now()+5*60*1000).toISOString();
83
+ state = await service.propose({expectedVersion:state.version,action:'tracker-update',payload:prepared.payload,expiresAt});
84
+ dependencies.output.log(`Post delivery summary to ${target.issueUrl}`);
85
+ dependencies.output.log(`${prepared.preview}\n\nRivet delivery operation: ${state.proposal.digest}`);
86
+ if (!(await confirm(state))) fail('Tracker update was not approved. No comment was posted.', 'INVALID_INPUT');
87
+ const freshConfig = await reloadConfig();
88
+ const freshProvider = selectProvider(freshConfig,target,provider.id,true);
89
+ if (hash(freshProvider) !== hash(provider) || hash(await loadTrackerTarget(state.candidate)) !== hash(target))
90
+ fail('Tracker source or provider configuration changed. Review a new proposal.', 'REPOSITORY_CONFLICT');
91
+ const approval = createApprovalReceipt({id:`tracker-${randomUUID()}`,approverId:'terminal-human',approverPrincipal:'human',subjectId:'delivery-cli',
92
+ action:'provider.write',resource:state.proposal.approvalResource,policyId:'authority.external-write',decision:'approved',expiresAt,singleUse:true});
93
+ return service.execute({expectedVersion:state.version,proposalDigest:state.proposal.digest,approval});
94
+ }
@@ -0,0 +1,79 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliError } from '../cli/output.js';
3
+ import { createNodeProviderTransport } from '../adapters/node-transport.js';
4
+ import { createDeliveryService } from '../delivery/service.js';
5
+ import { hash, plain } from '../delivery/contract.js';
6
+ import { createAuthorityEnvelope } from '../policy/authority.js';
7
+ import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
8
+ import { selectProvider, authHeaders } from './delivery-tracker.js';
9
+
10
+ function fail(message, code = 'REPOSITORY_CONFLICT') { throw new CliError(message, code); }
11
+ async function select(dependencies, choices) {
12
+ let timer;
13
+ try {
14
+ const index = await Promise.race([
15
+ Promise.resolve().then(() => dependencies.selectTrackerDestination(choices)),
16
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), 30000); }),
17
+ ]);
18
+ return Number.isInteger(index) && index >= 0 && index < choices.length ? choices[index] : null;
19
+ } catch { return null; } finally { clearTimeout(timer); }
20
+ }
21
+
22
+ export async function runTrackerTransition({action,state,store,config,flags,dependencies,reloadConfig,loadTrackerTarget,confirm}) {
23
+ const writing = action === 'tracker-transition', reconciling = action === 'reconcile';
24
+ if (writing && state.operations.some(op => op.action === action && op.state === 'succeeded')) return state;
25
+ const pending = state.operations.find(op => ['dispatching','indeterminate'].includes(op.state));
26
+ if (writing && pending) fail('Reconcile the pending delivery operation before changing tracker status.');
27
+ const mergeReceipt = state.operations.find(op => op.action === 'merge' && op.state === 'succeeded')?.receipt;
28
+ if (!mergeReceipt) fail('Tracker status delivery requires a confirmed merge.');
29
+ let target, selectedId = flags.provider;
30
+ if (reconciling) {
31
+ if (pending?.action !== 'tracker-transition' || state.proposal?.digest !== pending.digest) fail('No pending tracker transition was found.');
32
+ const p = pending.payload;
33
+ target = plain({kind:p.kind,issueKey:p.issueKey,issueUrl:p.issueUrl,requestDigest:p.requestDigest});
34
+ if (selectedId !== undefined && selectedId !== state.proposal.providerId) fail('Reconciliation requires the originally approved tracker provider.');
35
+ selectedId = state.proposal.providerId;
36
+ } else {
37
+ try { target = plain(await loadTrackerTarget(state.candidate)); }
38
+ catch { fail('This run has no validated Jira/Linear source ticket. A replacement ticket cannot be supplied.','MISSING_CONFIGURATION'); }
39
+ }
40
+ const provider = selectProvider(config,target,selectedId,writing,'tracker-transition');
41
+ if (reconciling && provider.endpoint !== pending.payload.endpoint) fail('Restore the originally approved tracker endpoint before reconciliation.');
42
+ const intake = await (dependencies.delivery?.transitionFactory ?? (await import('../delivery/tracker-transition.js')).createTrackerTransition)({
43
+ repository:state.candidate.repository,target,mergeReceipt,providerId:provider.id,baseUrl:provider.endpoint,
44
+ headers:authHeaders(provider,dependencies.env),transport:dependencies.delivery?.transport ?? createNodeProviderTransport(),
45
+ ...(reconciling ? {persistedPayload:pending.payload} : {}),
46
+ });
47
+ if (action === 'tracker-status') return {...state,trackerStatus:intake.status};
48
+ let prepared = intake;
49
+ if (writing) {
50
+ dependencies.output.log(`Tracker: ${target.issueUrl}\nCurrent status: ${intake.status.current.name}`);
51
+ const choices = intake.status.destinations.filter(choice => choice.eligible);
52
+ const labels = choices.map(choice => `${choice.state.name} (${choice.name}; ${choice.state.type})`);
53
+ if (new Set(labels).size !== labels.length) fail('Some tracker destinations have indistinguishable names. Review these transitions in the tracker before continuing.');
54
+ for (const [index,choice] of choices.entries()) dependencies.output.log(`${index+1}. ${choice.state.name} (${choice.name}; ${choice.state.type})`);
55
+ for (const choice of intake.status.destinations.filter(choice => !choice.eligible)) dependencies.output.log(`Unavailable: ${choice.state.name} (${choice.name}): requires fields or a transition screen.`);
56
+ if (!choices.length) fail('No supported tracker destinations are available. Complete required fields or transition screens in the tracker.');
57
+ const selected = await select(dependencies,choices);
58
+ if (!selected) fail('No destination selected. Tracker status was not changed.','INVALID_INPUT');
59
+ prepared = await intake.select(selected.id);
60
+ if (prepared.alreadyDesired) return {...state,trackerStatus:intake.status,trackerTransitionNoop:true};
61
+ }
62
+ const service = createDeliveryService({store,executor:prepared.executor,providerId:provider.id,timeoutMs:120000,
63
+ subjectId:'delivery-cli',expectedApproverId:'terminal-human',
64
+ authority:createAuthorityEnvelope({actorId:'delivery-cli',principal:'agent',actions:writing?['provider.write']:[],ownedPaths:[],commands:[],
65
+ providers:writing?[{id:provider.id,mode:provider.mode,capabilities:['tracker-transition']}]:[]}),
66
+ approvalRegistry:createApprovalRegistry({approvers:[{id:'terminal-human',principal:'human'}]})});
67
+ if (reconciling) return service.reconcile({expectedVersion:state.version});
68
+ state = await service.refresh({expectedVersion:state.version});
69
+ const expiresAt = new Date(Date.now()+5*60*1000).toISOString();
70
+ state = await service.propose({expectedVersion:state.version,action:'tracker-transition',payload:prepared.payload,expiresAt});
71
+ dependencies.output.log(`${prepared.preview}\nConfirmed merge: ${mergeReceipt.commitSha}\nRivet delivery operation: ${state.proposal.digest}`);
72
+ dependencies.output.log('Assurance: precheck and readback; the tracker provides no atomic revision condition. Confirmation observes the desired state and cannot prove that this operation alone caused it.');
73
+ if (!(await confirm(state))) fail('Tracker transition was not approved. No status change was sent.','INVALID_INPUT');
74
+ const freshProvider = selectProvider(await reloadConfig(),target,provider.id,true,'tracker-transition');
75
+ if (hash(freshProvider) !== hash(provider) || hash(await loadTrackerTarget(state.candidate)) !== hash(target)) fail('Tracker source or provider configuration changed. Review a new proposal.');
76
+ const approval = createApprovalReceipt({id:`tracker-transition-${randomUUID()}`,approverId:'terminal-human',approverPrincipal:'human',subjectId:'delivery-cli',
77
+ action:'provider.write',resource:state.proposal.approvalResource,policyId:'authority.external-write',decision:'approved',expiresAt,singleUse:true});
78
+ return service.execute({expectedVersion:state.version,proposalDigest:state.proposal.digest,approval});
79
+ }