@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,96 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { join } from 'node:path';
4
+
5
+ import { loadProjectConfig } from '../config/load.js';
6
+ import { createFeaturePlan } from './plan-contract.js';
7
+ import { createFeatureRuntimeState } from './runtime-bridge.js';
8
+ import { immutableJson } from '../clients/contract.js';
9
+ import { validateRuntimeSnapshot } from '../runtime/orchestrator.js';
10
+ import { acquireLock, recoverAbandonedLock } from '../state/lock.js';
11
+ import { resolveExistingFeatureRunPaths, resolveExistingStatePaths, verifyResolvedStatePaths } from '../state/paths.js';
12
+ import { readSnapshotWithoutLock } from '../state/snapshot-store.js';
13
+ import { createFeatureRunStore } from './run-store.js';
14
+
15
+ function invalid() {
16
+ const error = new Error('Lock recovery requires valid saved host state or a finished spawned task awaiting local acceptance.');
17
+ error.code = 'ERR_HOST_RUN_RECOVERY_STATE';
18
+ error.safeMessage = error.message;
19
+ throw error;
20
+ }
21
+
22
+ /** Recover lock ownership only. Never construct a runtime or change a saved run. */
23
+ export async function recoverHostRunLocks(project, runId) {
24
+ const paths = await resolveExistingFeatureRunPaths(project, runId);
25
+ if (paths === null) invalid();
26
+ const runtimePaths = await resolveExistingStatePaths(project, runId);
27
+ const config = await loadProjectConfig(project);
28
+ async function validate() {
29
+ await verifyResolvedStatePaths(paths);
30
+ const run = await createFeatureRunStore(paths).readOnly();
31
+ if (!run) invalid();
32
+ const spawned = run.featurePlan.client !== 'host';
33
+ if (spawned && !['awaiting-final-approval', 'completed'].includes(run.status)) invalid();
34
+ createFeaturePlan({ proposal: run.featurePlan, config, workRequest: run.workRequest, baselineCommit: run.featurePlan.baselineCommit, client: run.featurePlan.client });
35
+ let runtime = null;
36
+ let savedSnapshot = null;
37
+ if (runtimePaths !== null) {
38
+ await verifyResolvedStatePaths(runtimePaths);
39
+ savedSnapshot = await readSnapshotWithoutLock(runtimePaths);
40
+ if (savedSnapshot === null) {
41
+ if (run.status !== 'approved' || run.runtimeRefs.length !== 0) invalid();
42
+ } else {
43
+ runtime = validateRuntimeSnapshot(savedSnapshot.data, runId);
44
+ const expected = createFeatureRuntimeState({ config, run }).graph;
45
+ const fixedGraph = graph => ({ ...graph, status: undefined, nodes: graph.nodes.map(node => ({ ...node, status: undefined, evidenceRefs: undefined })) });
46
+ if (!isDeepStrictEqual(fixedGraph(runtime.graph), fixedGraph(expected))) invalid();
47
+ }
48
+ } else if (!['proposed', 'approved'].includes(run.status) || run.runtimeRefs.length !== 0) invalid();
49
+ // A finished spawned task may have crashed while holding this lock during
50
+ // local confirmation/application. Recover ownership only: this does not
51
+ // authorize a Git update, restart a Worker, or change acceptance evidence.
52
+ if (spawned && (!runtime
53
+ || runtime.graph.nodes.find(node => node.id === 'final-delivery')?.status !== 'ready'
54
+ || runtime.graph.nodes.some(node => node.owner.role === 'worker'
55
+ && !['completed', 'archived'].includes(node.status)))) invalid();
56
+ return { spawned, runVersion: run.version, runtimeVersion: runtime?.version ?? null,
57
+ fingerprint: createHash('sha256').update(JSON.stringify({ run, savedSnapshot })).digest('hex') };
58
+ }
59
+ const initial = await validate();
60
+ const recoveredLocks = [];
61
+ const held = [];
62
+ let blockedLock = 'host-operation';
63
+ let failure = false;
64
+ const lockPaths = [
65
+ ['host-operation', join(paths.runDir, 'host-operation.lock'), paths],
66
+ ['run', paths.lockPath, paths],
67
+ ...(runtimePaths ? [['runtime', runtimePaths.runtimeLockPath, runtimePaths], ['state', runtimePaths.lockPath, runtimePaths]] : []),
68
+ ];
69
+ try {
70
+ for (const [label, path, ownerPaths] of lockPaths) {
71
+ blockedLock = label;
72
+ await verifyResolvedStatePaths(ownerPaths);
73
+ const current = await validate();
74
+ if (current.fingerprint !== initial.fingerprint) invalid();
75
+ if (await recoverAbandonedLock(path)) recoveredLocks.push(label);
76
+ held.push({ label, lock: await acquireLock(path) });
77
+ }
78
+ if ((await validate()).fingerprint !== initial.fingerprint) invalid();
79
+ } catch {
80
+ failure = true;
81
+ } finally {
82
+ for (const { label, lock } of held.reverse()) {
83
+ try { await lock.release(); }
84
+ catch { if (!failure) blockedLock = label; failure = true; }
85
+ }
86
+ }
87
+ return immutableJson({
88
+ status: failure ? 'blocked' : 'recovered', runId, runVersion: initial.runVersion, runtimeVersion: initial.runtimeVersion, recoveredLocks,
89
+ ...(failure ? { blockedLock } : {}),
90
+ nextAction: failure
91
+ ? 'Recovery stopped safely. Inspect the remaining lock owner and private state before retrying; recovered locks are listed above.'
92
+ : initial.spawned
93
+ ? 'Read rivet task status, then rivet task approve to inspect local acceptance. Recovery did not resume workers, apply changes, or change task state.'
94
+ : 'Read task status and continue in the owning harness. Recovery did not resume workers or change task state.',
95
+ });
96
+ }
@@ -0,0 +1,32 @@
1
+ import { join } from 'node:path';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+
4
+ import { acquireLock, LockBusyError, StaleLockError } from '../state/lock.js';
5
+ import { assertResolvedStatePaths, verifyResolvedStatePaths } from '../state/paths.js';
6
+
7
+ export class HostRunLockError extends Error {
8
+ constructor(stale = false) {
9
+ super(stale
10
+ ? 'Host run lock needs inspection after an interrupted operation; no lock was removed automatically.'
11
+ : 'Host run is busy. Read work status and retry with the current versions.');
12
+ this.name = 'HostRunLockError';
13
+ this.code = stale ? 'ERR_HOST_RUN_STALE_LOCK' : 'ERR_HOST_RUN_BUSY';
14
+ this.safeMessage = this.message;
15
+ }
16
+ }
17
+
18
+ export async function acquireHostRunLock(paths) {
19
+ assertResolvedStatePaths(paths);
20
+ if (paths.runDir !== paths.instanceDir) throw new TypeError('Host run paths are invalid');
21
+ const deadline = Date.now() + 2_000;
22
+ while (true) {
23
+ await verifyResolvedStatePaths(paths);
24
+ try { return await acquireLock(join(paths.runDir, 'host-operation.lock')); }
25
+ catch (error) {
26
+ if (error instanceof StaleLockError) throw new HostRunLockError(true);
27
+ if (!(error instanceof LockBusyError)) throw error;
28
+ if (Date.now() >= deadline) throw new HostRunLockError();
29
+ await delay(10);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,183 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {constants} from 'node:fs';
3
+ import {lstat, open} from 'node:fs/promises';
4
+ import {join, resolve} from 'node:path';
5
+ import {immutableJson} from '../clients/contract.js';
6
+ import {loadProjectConfig} from '../config/load.js';
7
+ import {CONFIG_FILES, MAX_CONFIG_FILE_BYTES} from '../config/defaults.js';
8
+ import {assertGitClient} from '../git/client.js';
9
+ import {assertSelectedProtocolRefs} from '../protocols/project.js';
10
+ import {resolveExistingFeatureRunPaths, verifyResolvedStatePaths} from '../state/paths.js';
11
+ import {createHostExecution} from './host-execution.js';
12
+ import {acquireHostRunLock} from './host-run-lock.js';
13
+ import {createFeatureRunStore} from './run-store.js';
14
+ import {createFeatureRuntimeState} from './runtime-bridge.js';
15
+
16
+ export class LocalApprovalError extends Error {
17
+ constructor(reason = 'conflict') {
18
+ const messages = {
19
+ conflict: 'Local application requires unchanged verified evidence, configuration, and clean checkouts. Inspect task status before retrying.',
20
+ interrupted: 'A local application was interrupted. Work is preserved. Inspect the source branch and private approval record; no merge was retried.',
21
+ cancelled: 'Local application was cancelled before the Git update.',
22
+ invalid: 'Local approval input is invalid.',
23
+ configuration: 'Local application requires the four Rivet configuration files committed in the approved baseline and unchanged in both checkouts. Commit intended configuration changes, then create and verify a new reviewed task.',
24
+ };
25
+ super(messages[reason] ?? messages.conflict);
26
+ this.name = 'LocalApprovalError';
27
+ this.code = `ERR_LOCAL_APPROVAL_${reason.toUpperCase()}`;
28
+ this.safeMessage = this.message;
29
+ }
30
+ }
31
+ function fail(reason) { throw new LocalApprovalError(reason); }
32
+ function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
33
+ function same(a,b) { return a.dev===b.dev && a.ino===b.ino; }
34
+ const MAX_RECORD = 64 * 1024;
35
+ async function readBounded(path, maximum, privateFile = false) {
36
+ let handle;
37
+ try {
38
+ const before = await lstat(path);
39
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink!==1 || before.size>maximum
40
+ || (privateFile && (before.mode & 0o777)!==0o600)) fail();
41
+ handle=await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
42
+ const opened=await handle.stat();
43
+ if(!same(before,opened))fail();
44
+ const bytes=Buffer.alloc(maximum+1); let count=0;
45
+ while(count<bytes.length){const result=await handle.read(bytes,count,bytes.length-count,null);if(!result.bytesRead)break;count+=result.bytesRead;}
46
+ const after=await handle.stat();
47
+ if(count>maximum||!same(after,await lstat(path))||after.size!==opened.size||after.mtimeMs!==opened.mtimeMs)fail();
48
+ return bytes.subarray(0,count);
49
+ } finally {await handle?.close();}
50
+ }
51
+ async function record(paths,name) {
52
+ await verifyResolvedStatePaths(paths);
53
+ try {return JSON.parse((await readBounded(join(paths.runDir,name),MAX_RECORD,true)).toString('utf8'));}
54
+ catch(error){if(error.code==='ENOENT')return null;fail();}
55
+ }
56
+ // Create once, fsync both file and directory. An interrupted partial write is
57
+ // intentionally an inspection boundary; neither a stale lock nor an intent is
58
+ // permission to execute a second merge. Private state is a trusted local boundary.
59
+ async function writeRecord(paths,name,value) {
60
+ await verifyResolvedStatePaths(paths);
61
+ const data=JSON.stringify(value);
62
+ if(Buffer.byteLength(data)>MAX_RECORD)fail();
63
+ const handle=await open(join(paths.runDir,name),constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600);
64
+ try{await handle.writeFile(data);await handle.sync();}finally{await handle.close();}
65
+ await verifyResolvedStatePaths(paths);
66
+ const directory=await open(paths.runDir,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);
67
+ try{await directory.sync();}finally{await directory.close();}
68
+ }
69
+
70
+ export async function applyVerifiedTask(input) {
71
+ let lock;
72
+ let selectedRunId;
73
+ try {
74
+ if(!input || typeof input!=='object' || Array.isArray(input) || ![Object.prototype,null].includes(Object.getPrototypeOf(input))
75
+ || Reflect.ownKeys(input).some(key=>typeof key!=='string'))fail('invalid');
76
+ const descriptors=Object.getOwnPropertyDescriptors(input);
77
+ if(Object.keys(descriptors).some(key=>!['project','runId','gitClient','confirm','signal'].includes(key)
78
+ || !Object.hasOwn(descriptors[key],'value')))fail('invalid');
79
+ const values=Object.fromEntries(Object.entries(descriptors).map(([key,descriptor])=>[key,descriptor.value]));
80
+ const {project,runId,gitClient,confirm,signal}=values;
81
+ if(typeof project!=='string'||resolve(project)!==project||typeof runId!=='string'
82
+ ||! /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(runId)||runId.length>64||typeof confirm!=='function')fail('invalid');
83
+ selectedRunId=runId;
84
+ assertGitClient(gitClient);
85
+ const cancelled=()=>{if(signal?.aborted)fail('cancelled');};
86
+ cancelled();
87
+ const paths=await resolveExistingFeatureRunPaths(project,runId);if(!paths)fail();
88
+ lock=await acquireHostRunLock(paths);
89
+ const store=createFeatureRunStore(paths);
90
+ const host=createHostExecution({gitClient});
91
+ async function observe() {
92
+ await verifyResolvedStatePaths(paths);
93
+ const status=await host.status({project,runId});
94
+ const {run,verification,checkout,runtime}=status;
95
+ const config=await loadProjectConfig(project);
96
+ createFeatureRuntimeState({config,run});
97
+ assertSelectedProtocolRefs(project,run.workRequest.contextRefs);
98
+ if(!verification||!checkout||!runtime||checkout.status!=='clean'||verification.status!=='pass')fail();
99
+ const source=await gitClient.inspectRepository(project);
100
+ if(source.root!==project||source.dirty||source.detached||source.branch!==config.project.repository.defaultBranch)fail();
101
+ // Require exact baseline configuration bytes, including files hidden from
102
+ // ordinary Git status with assume-unchanged. Policy-changing work needs a
103
+ // separate reviewed delivery flow, not reuse of this local approval.
104
+ const configHashes=[];
105
+ try {
106
+ for(const name of Object.values(CONFIG_FILES)){
107
+ const relative=`.rivet/${name}`;
108
+ const tracked=await gitClient.inspectTrackedFile(project,run.featurePlan.baselineCommit,relative);
109
+ const bytes=await readBounded(join(project,relative),MAX_CONFIG_FILE_BYTES);
110
+ const blob=createHash(tracked.blob.length===40?'sha1':'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
111
+ if(blob!==tracked.blob)fail();
112
+ const integrationBytes=await readBounded(join(checkout.path,relative),MAX_CONFIG_FILE_BYTES);
113
+ if(!bytes.equals(integrationBytes))fail();
114
+ configHashes.push(blob);
115
+ }
116
+ } catch { fail('configuration'); }
117
+ const changed=await gitClient.changedPaths(project,run.featurePlan.baselineCommit,verification.commitSha);
118
+ const selectedPaths=new Set(run.workRequest.contextRefs.filter(ref=>ref.startsWith('protocol:'))
119
+ .map(ref=>`.rivet/protocols/${ref.split(':')[1]}.md`));
120
+ if(changed.some(path=>selectedPaths.has(path)))fail();
121
+ if(changed.length!==verification.changedPathCount||hash(changed)!==hash(verification.changedPaths))fail();
122
+ const evidenceDigest=hash({configHashes,config,proposalDigest:run.proposalDigest,activation:run.activation,
123
+ workRequest:run.workRequest,featurePlan:run.featurePlan,verification,runtime,checkout});
124
+ return {status,source,evidenceDigest};
125
+ }
126
+ let current=await observe();
127
+ const intent=await record(paths,'local-approval-intent.json');
128
+ let receipt=await record(paths,'local-approval-receipt.json');
129
+ const {run}=current.status;
130
+ const preview=immutableJson({runId,targetBranch:current.source.branch,baselineCommit:run.featurePlan.baselineCommit,
131
+ commitSha:current.status.verification.commitSha,integrationPath:current.status.checkout.path,
132
+ changedPaths:current.status.verification.changedPaths,
133
+ checks:current.status.verification.checks.map(({id,status,required,exitCode})=>({id,status,required,exitCode}))});
134
+ const binding={schemaVersion:1,kind:'local-acceptance',runId,runVersion:run.status==='completed'?run.version-1:run.version,
135
+ evidenceDigest:current.evidenceDigest,source:{root:current.source.root,rootIdentity:current.source.rootIdentity,
136
+ gitCommonDir:current.source.gitCommonDir,repositoryId:current.source.repositoryId},preview};
137
+ const identity=hash(binding);
138
+ const reference=`local-acceptance:${identity}`;
139
+ if(intent!==null){
140
+ if(hash(intent)!==hash({...binding,digest:identity}))fail();
141
+ if(current.source.headSha!==preview.commitSha)fail('interrupted');
142
+ if(receipt!==null&&hash(receipt)!==hash({schemaVersion:1,kind:'local-acceptance',intentDigest:identity,commitSha:preview.commitSha}))fail();
143
+ if(run.status==='completed'){
144
+ if(receipt===null||!run.evidenceRefs.includes(reference))fail();
145
+ return immutableJson({status:'completed',commitSha:preview.commitSha,nextAction:'Verified changes are in your local default branch. No push or deployment was performed.'});
146
+ }
147
+ if(!current.status.deliveryReady)fail();
148
+ // Exact durable intent plus the already-observed target permits receipt-only
149
+ // reconciliation. It never authorizes another Git mutation.
150
+ }else{
151
+ if(receipt!==null||run.status!=='awaiting-final-approval'||!current.status.deliveryReady
152
+ ||current.source.headSha!==preview.baselineCommit)fail();
153
+ if(await confirm(preview)!==true)return Object.freeze({status:'declined'});
154
+ cancelled();
155
+ const fresh=await observe();
156
+ if(fresh.status.run.version!==run.version||fresh.evidenceDigest!==current.evidenceDigest
157
+ ||hash(fresh.source)!==hash(current.source)||!fresh.status.deliveryReady)fail();
158
+ await writeRecord(paths,'local-approval-intent.json',{...binding,digest:identity});
159
+ cancelled();
160
+ await gitClient.fastForward(project,{branch:preview.targetBranch,expectedTip:preview.baselineCommit,newTip:preview.commitSha});
161
+ current=await observe();
162
+ if(current.evidenceDigest!==binding.evidenceDigest||current.status.run.version!==run.version
163
+ ||current.source.headSha!==preview.commitSha||!current.status.deliveryReady)fail('interrupted');
164
+ }
165
+ if(receipt===null){
166
+ receipt={schemaVersion:1,kind:'local-acceptance',intentDigest:identity,commitSha:preview.commitSha};
167
+ await writeRecord(paths,'local-approval-receipt.json',receipt);
168
+ }
169
+ await store.update({status:'completed',updatedAt:new Date(Math.max(Date.now(),Date.parse(run.updatedAt))).toISOString(),
170
+ runtimeRefs:run.runtimeRefs,evidenceRefs:[...run.evidenceRefs,reference]}, {expectedVersion:run.version});
171
+ return immutableJson({status:'completed',commitSha:preview.commitSha,nextAction:'Verified changes are in your local default branch. No push or deployment was performed.'});
172
+ }catch(error){
173
+ if(error instanceof LocalApprovalError)throw error;
174
+ if(error?.code==='ERR_HOST_RUN_STALE_LOCK' && selectedRunId){
175
+ const recovery=new LocalApprovalError('interrupted');
176
+ recovery.message=`A prior local operation was interrupted. From this project, run rivet task recover --run=${selectedRunId}, then inspect task status before approving. No lock was removed automatically.`;
177
+ recovery.safeMessage=recovery.message;
178
+ throw recovery;
179
+ }
180
+ throw new LocalApprovalError();
181
+ }
182
+ finally{await lock?.release();}
183
+ }
@@ -0,0 +1,215 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ import Ajv from 'ajv';
5
+
6
+ import { immutableJson } from '../clients/contract.js';
7
+ import { validateProjectConfiguration } from '../config/validate.js';
8
+ import { validateWorkRequest } from '../work-request/contract.js';
9
+ import { matchesClientProfile, matchesWorkerExecution } from './client-profile.js';
10
+
11
+ const schema = JSON.parse(readFileSync(new URL('../../schemas/feature-plan.schema.json', import.meta.url), 'utf8'));
12
+ const validateSchema = new Ajv({ allErrors: true, strict: true }).compile(schema);
13
+ const plans = new WeakSet();
14
+ const GIT_OBJECT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
15
+ const CLIENTS = new Set(['claude', 'codex', 'host']);
16
+ const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
17
+
18
+ const VIOLATIONS = Object.freeze({
19
+ 'owned-path-invalid': 'An owned path contains a reserved or non-normalized component.',
20
+ 'owned-path-protected': 'An owned path targets repository metadata or a protected project path.',
21
+ 'owned-path-duplicate': 'A work item contains owned paths that collide after case normalization.',
22
+ 'plan-schema-invalid': 'The compiled plan does not match the governed plan schema.',
23
+ 'plan-policy-invalid': 'The compiled plan violates the configured governance policy.',
24
+ });
25
+
26
+ export class FeaturePlanError extends Error {
27
+ constructor(reason = 'invalid-plan', violation) {
28
+ const messages = {
29
+ 'invalid-plan': 'Feature plan is invalid.',
30
+ 'decomposition-invalid': 'Planning provider returned an invalid feature decomposition.',
31
+ 'compiled-plan-invalid': 'Feature decomposition could not be compiled into a valid governed plan.',
32
+ };
33
+ const detail = Object.hasOwn(VIOLATIONS, violation) ? violation : undefined;
34
+ super(`${messages[reason] ?? messages['invalid-plan']}${detail ? ` ${VIOLATIONS[detail]}` : ''}`);
35
+ this.name = 'FeaturePlanError';
36
+ this.code = 'ERR_INVALID_FEATURE_PLAN';
37
+ this.safeMessage = this.message;
38
+ this.details = Object.freeze({ reason, ...(detail ? { violation: detail } : {}) });
39
+ }
40
+ }
41
+
42
+ function fail(violation = 'plan-policy-invalid') { throw new FeaturePlanError('invalid-plan', violation); }
43
+
44
+ function canonical(value) {
45
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
46
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
47
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
48
+ }
49
+
50
+ function safeSnapshot(value) {
51
+ try { return immutableJson(value); } catch { fail(); }
52
+ }
53
+
54
+ function validateBindings(bindings) {
55
+ try {
56
+ if (!bindings || typeof bindings !== 'object' || Array.isArray(bindings)) fail();
57
+ const keys = Reflect.ownKeys(bindings);
58
+ if (keys.length !== 5 || keys.some(key => typeof key !== 'string'
59
+ || !['proposal', 'config', 'workRequest', 'baselineCommit', 'client'].includes(key))) fail();
60
+ validateProjectConfiguration(bindings.config);
61
+ validateWorkRequest(bindings.workRequest);
62
+ if (typeof bindings.baselineCommit !== 'string' || !GIT_OBJECT.test(bindings.baselineCommit)
63
+ || !CLIENTS.has(bindings.client)) fail();
64
+ return bindings;
65
+ } catch (error) {
66
+ if (error instanceof FeaturePlanError) throw error;
67
+ fail();
68
+ }
69
+ }
70
+
71
+ function safeOwnedPath(path, sensitivePaths) {
72
+ const parts = path.replaceAll('\\', '/').split('/');
73
+ if (parts.some(part => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
74
+ || WINDOWS_RESERVED.test(part))) fail('owned-path-invalid');
75
+ const folded = parts.map(part => part.toLowerCase()).join('/');
76
+ if (folded === '.git' || folded.startsWith('.git/') || folded === '.rivet' || folded.startsWith('.rivet/')
77
+ || folded === '.rivet.cjs' || folded.startsWith('.rivet.cjs/')) fail('owned-path-protected');
78
+ for (const pattern of sensitivePaths) {
79
+ const prefix = pattern.replaceAll('\\', '/').split('*', 1)[0].replace(/\/$/, '').toLowerCase();
80
+ if (folded === prefix || folded.startsWith(`${prefix}/`)) fail('owned-path-protected');
81
+ }
82
+ return folded;
83
+ }
84
+
85
+ function pathOverlap(left, right) {
86
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
87
+ }
88
+
89
+ function visit(id, nodeMap, visiting, visited) {
90
+ if (visited.has(id)) return;
91
+ if (visiting.has(id)) fail();
92
+ visiting.add(id);
93
+ const node = nodeMap.get(id);
94
+ for (const dependency of node.dependencies) visit(dependency, nodeMap, visiting, visited);
95
+ visiting.delete(id);
96
+ visited.add(id);
97
+ }
98
+
99
+ function dependsOn(node, targetId, nodeMap) {
100
+ const pending = [...node.dependencies];
101
+ const seen = new Set();
102
+ while (pending.length > 0) {
103
+ const id = pending.pop();
104
+ if (id === targetId) return true;
105
+ if (seen.has(id)) continue;
106
+ seen.add(id);
107
+ pending.push(...nodeMap.get(id).dependencies);
108
+ }
109
+ return false;
110
+ }
111
+
112
+ function validatePlanSemantics(plan, { config, workRequest, baselineCommit, client }) {
113
+ if (plan.baselineCommit !== baselineCommit || plan.workRequestDigest !== workRequest.digest || plan.client !== client
114
+ || !matchesClientProfile(client, plan.clientProfile)) fail();
115
+ if (client === 'host' && config.orchestration.roles.some(role => role.harness !== undefined)) fail();
116
+ const configuredProviders = new Set(config.providers.providers.map(provider => provider.id));
117
+ if (plan.providerRefs.some(provider => !configuredProviders.has(provider))) fail();
118
+ const roles = new Map(config.orchestration.roles.map(role => [role.id, role]));
119
+ const profiles = new Map(config.orchestration.completionProfiles.map(profile => [profile.id, profile]));
120
+ const commands = new Set(Object.keys(config.project.commands));
121
+ const qualityEvidence = new Set(config.quality.evidence.requiredTypes);
122
+ const nodeMap = new Map();
123
+ for (const node of plan.nodes) {
124
+ if (nodeMap.has(node.id)) fail();
125
+ nodeMap.set(node.id, node);
126
+ }
127
+ const roots = plan.nodes.filter(node => node.parentId === undefined);
128
+ const activation = plan.nodes.filter(node => node.approvalGate === 'activation');
129
+ const final = plan.nodes.filter(node => node.approvalGate === 'final-delivery');
130
+ if (roots.length !== 1 || activation.length !== 1 || final.length !== 1 || roots[0] !== activation[0]
131
+ || activation[0].role !== 'boss' || final[0].role !== 'boss' || final[0].parentId !== activation[0].id) fail();
132
+ if (!['boss', 'manager', 'worker'].every(kind => plan.nodes.some(node => node.role === kind))) fail();
133
+
134
+ const criteria = new Set(workRequest.acceptanceCriteria);
135
+ const covered = new Set();
136
+ const pathSets = new Map();
137
+ for (const node of plan.nodes) {
138
+ const role = roles.get(node.roleId);
139
+ if (!role || role.kind !== node.role || node.dependencies.includes(node.id)) fail();
140
+ if (!matchesWorkerExecution(role, node.execution)) fail();
141
+ if (node !== roots[0] && (node.dependencies.length === 0 || !node.parentId)) fail();
142
+ if (node.parentId && !nodeMap.has(node.parentId)) fail();
143
+ if (node.dependencies.some(id => !nodeMap.has(id))) fail();
144
+ if (node.authorityScopes.some(scope => !role.authorityScopes.includes(scope))) fail();
145
+ if (node.commandIds.some(command => !commands.has(command))) fail();
146
+ for (const name of ['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']) {
147
+ if (node.budget[name] > role.budget[name]) fail();
148
+ }
149
+ const profile = profiles.get(role.completionProfile);
150
+ if (!profile || profile.requiredEvidence.some(type => !node.requiredEvidenceTypes.includes(type))
151
+ || node.requiredEvidenceTypes.some(type => !qualityEvidence.has(type))) fail();
152
+ if (node.approvalGate && !config.orchestration.approvalGates.includes(node.approvalGate)) fail();
153
+ if ((node.approvalGate === 'activation' || node.approvalGate === 'final-delivery')
154
+ && !node.requiredEvidenceTypes.includes('human-approval')) fail();
155
+ if (node.authorityScopes.includes('external-write') && !role.permissions.externalWrites) fail();
156
+ if (node.authorityScopes.includes('merge') && !role.permissions.merge) fail();
157
+ if (node.authorityScopes.includes('deploy') && !role.permissions.deploy) fail();
158
+ if (node.role === 'worker' && node.ownedPaths.length === 0) fail();
159
+ if (node.role !== 'worker' && node.ownedPaths.length > 0) fail();
160
+ const foldedPaths = node.ownedPaths.map(path => safeOwnedPath(path, config.project.repository.sensitivePaths ?? []));
161
+ if (new Set(foldedPaths).size !== foldedPaths.length) fail('owned-path-duplicate');
162
+ pathSets.set(node.id, foldedPaths);
163
+ for (const criterion of node.acceptanceCriteria) {
164
+ if (!criteria.has(criterion)) fail();
165
+ if (node.role === 'worker') covered.add(criterion);
166
+ }
167
+ if (node.role === 'worker' && node.acceptanceCriteria.length === 0) fail();
168
+ }
169
+ if (covered.size !== criteria.size) fail();
170
+
171
+ for (const node of plan.nodes) {
172
+ if (!node.parentId) continue;
173
+ const parent = nodeMap.get(node.parentId);
174
+ const parentRole = roles.get(parent.roleId);
175
+ const finalBoss = node === final[0] && parent === activation[0];
176
+ if (!finalBoss && (!parentRole.canDelegate || !parentRole.delegatesTo.includes(node.role))) fail();
177
+ if (node.authorityScopes.some(scope => !parent.authorityScopes.includes(scope))) fail();
178
+ for (const name of ['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']) {
179
+ if (node.budget[name] > parent.budget[name]) fail();
180
+ }
181
+ }
182
+ const visited = new Set();
183
+ for (const node of plan.nodes) visit(node.id, nodeMap, new Set(), visited);
184
+
185
+ const workers = plan.nodes.filter(node => node.role === 'worker');
186
+ for (let leftIndex = 0; leftIndex < workers.length; leftIndex += 1) {
187
+ for (let rightIndex = leftIndex + 1; rightIndex < workers.length; rightIndex += 1) {
188
+ const left = workers[leftIndex];
189
+ const right = workers[rightIndex];
190
+ if (dependsOn(left, right.id, nodeMap) || dependsOn(right, left.id, nodeMap)) continue;
191
+ if (pathSets.get(left.id).some(a => pathSets.get(right.id).some(b => pathOverlap(a, b)))) fail();
192
+ }
193
+ }
194
+ const requiredCommands = config.quality.commandGates.filter(gate => gate.required).map(gate => gate.command);
195
+ if (requiredCommands.some(command => !final[0].commandIds.includes(command))) fail();
196
+ }
197
+
198
+ export function createFeaturePlan(bindingsInput) {
199
+ const bindings = validateBindings(bindingsInput);
200
+ const proposal = safeSnapshot(bindings.proposal);
201
+ if (!validateSchema(proposal)) fail('plan-schema-invalid');
202
+ validatePlanSemantics(proposal, bindings);
203
+ plans.add(proposal);
204
+ return proposal;
205
+ }
206
+
207
+ export function validateFeaturePlan(value) {
208
+ if (!plans.has(value)) fail();
209
+ return value;
210
+ }
211
+
212
+ export function featurePlanDigest(value) {
213
+ validateFeaturePlan(value);
214
+ return createHash('sha256').update(canonical(value)).digest('hex');
215
+ }