@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,168 @@
1
+ import { immutableJson } from '../clients/contract.js';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { assertResolvedStatePaths } from '../state/paths.js';
4
+ import { createSnapshotStore, readSnapshotWithoutLock } from '../state/snapshot-store.js';
5
+
6
+ const SHA = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
7
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
+
9
+ export class VerificationReportVersionConflictError extends Error {
10
+ constructor() {
11
+ super('Verification report changed during this attempt. Read work status before retrying.');
12
+ this.name = 'VerificationReportVersionConflictError';
13
+ this.code = 'ERR_VERIFICATION_REPORT_VERSION_CONFLICT';
14
+ this.safeMessage = this.message;
15
+ }
16
+ }
17
+
18
+ function bounded(value, maximum) {
19
+ return typeof value === 'string' ? value.slice(0, maximum) : '';
20
+ }
21
+
22
+ function diagnostic(value) {
23
+ const source = typeof value === 'string' ? value : '';
24
+ const omitted = source.length > 512;
25
+ const marker = '[earlier output omitted]\n';
26
+ return { text: omitted ? `${marker}${source.slice(-(512 - marker.length))}` : source, omitted };
27
+ }
28
+
29
+ function nextAction(status) {
30
+ return status === 'pass'
31
+ ? 'Review the integration diff and executed checks, then make the separate final delivery decision.'
32
+ : 'If locked dependencies are missing, run rivet task deps in the project and approve the shown install. Then retry verification in the unchanged integration checkout at the same commit. Source fixes require a new reviewed proposal.';
33
+ }
34
+
35
+ function checkResult(value) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value)
37
+ || typeof value.id !== 'string' || value.id.length > 64
38
+ || !['passed', 'failed'].includes(value.status)
39
+ || typeof value.required !== 'boolean'
40
+ || typeof value.cwd !== 'string' || value.cwd.length > 500
41
+ || (value.exitCode !== null && !Number.isSafeInteger(value.exitCode))
42
+ || typeof value.executionStatus !== 'string' || value.executionStatus.length > 64
43
+ || !value.output || typeof value.output !== 'object' || Array.isArray(value.output)) {
44
+ throw new TypeError('Verification check is invalid');
45
+ }
46
+ return {
47
+ id: value.id,
48
+ status: value.status,
49
+ required: value.required,
50
+ cwd: value.cwd,
51
+ exitCode: value.exitCode,
52
+ executionStatus: value.executionStatus,
53
+ output: {
54
+ stdout: diagnostic(value.output.stdout).text,
55
+ stderr: diagnostic(value.output.stderr).text,
56
+ truncated: {
57
+ stdout: value.output.truncated?.stdout === true || diagnostic(value.output.stdout).omitted,
58
+ stderr: value.output.truncated?.stderr === true || diagnostic(value.output.stderr).omitted,
59
+ combined: value.output.truncated?.combined === true
60
+ || diagnostic(value.output.stdout).omitted || diagnostic(value.output.stderr).omitted,
61
+ },
62
+ redacted: value.output.redacted === true,
63
+ suppressed: value.output.suppressed === true,
64
+ },
65
+ };
66
+ }
67
+
68
+ function valid(report) {
69
+ if (!report || typeof report !== 'object' || Array.isArray(report)
70
+ || report.schemaVersion !== 1 || !['pass', 'fail'].includes(report.status)
71
+ || !SHA.test(report.baselineCommit) || !SHA.test(report.commitSha)
72
+ || typeof report.runId !== 'string' || report.runId.length > 64 || !ID.test(report.runId)
73
+ || !Number.isSafeInteger(report.runVersion) || report.runVersion < 1
74
+ || !Number.isSafeInteger(report.runtimeVersion) || report.runtimeVersion < 1
75
+ || typeof report.integration?.path !== 'string' || report.integration.path.length > 4096
76
+ || !isAbsolute(report.integration.path) || resolve(report.integration.path) !== report.integration.path
77
+ || typeof report.integration?.branch !== 'string' || report.integration.branch.length > 256
78
+ || /[\u0000-\u001f\u007f]/.test(report.integration.branch)
79
+ || !Array.isArray(report.changedPaths) || report.changedPaths.length > 128
80
+ || report.changedPaths.some(path => typeof path !== 'string' || path.length > 500)
81
+ || !Number.isSafeInteger(report.changedPathCount) || report.changedPathCount < report.changedPaths.length
82
+ || !Array.isArray(report.workerClaims) || report.workerClaims.length > 64
83
+ || report.workerClaims.some(ref => typeof ref !== 'string' || ref.length > 256)
84
+ || !Array.isArray(report.checks) || report.checks.length > 64
85
+ || (report.failure !== null && (typeof report.failure !== 'string' || report.failure.length > 300))
86
+ || typeof report.checkedAt !== 'string' || !Number.isFinite(Date.parse(report.checkedAt))
87
+ || new Date(report.checkedAt).toISOString() !== report.checkedAt) {
88
+ throw new TypeError('Verification report is invalid');
89
+ }
90
+ return immutableJson({
91
+ schemaVersion: 1,
92
+ runId: report.runId,
93
+ runVersion: report.runVersion,
94
+ runtimeVersion: report.runtimeVersion,
95
+ status: report.status,
96
+ baselineCommit: report.baselineCommit,
97
+ commitSha: report.commitSha,
98
+ integration: { path: report.integration.path, branch: report.integration.branch },
99
+ changedPaths: report.changedPaths,
100
+ changedPathCount: report.changedPathCount,
101
+ workerClaims: report.workerClaims,
102
+ checks: report.checks.map(checkResult),
103
+ failure: report.failure,
104
+ nextAction: nextAction(report.status),
105
+ checkedAt: report.checkedAt,
106
+ });
107
+ }
108
+
109
+ export function verificationReport(input) {
110
+ const changedPaths = input.changedPaths.slice(0, 128);
111
+ const workerNodes = new Set(input.state.graph.nodes.filter(node => node.owner?.role === 'worker').map(node => node.id));
112
+ const workerClaims = input.state.evidence.filter(item => workerNodes.has(item.nodeId))
113
+ .map(item => item.id).slice(0, 64);
114
+ const checks = (input.quality?.gates ?? []).map(gate => ({
115
+ id: gate.id,
116
+ status: gate.status,
117
+ required: gate.required,
118
+ cwd: gate.cwd,
119
+ exitCode: gate.exitCode,
120
+ executionStatus: gate.executionStatus,
121
+ output: {
122
+ stdout: gate.output?.stdout ?? '',
123
+ stderr: gate.output?.stderr ?? '',
124
+ truncated: gate.output?.truncated ?? { stdout: false, stderr: false, combined: false },
125
+ redacted: gate.output?.redacted === true,
126
+ suppressed: gate.output?.suppressed === true,
127
+ },
128
+ }));
129
+ return valid({
130
+ schemaVersion: 1,
131
+ runId: input.run.runId,
132
+ runVersion: Number.isSafeInteger(input.run.version) ? input.run.version : 1,
133
+ runtimeVersion: input.state.version,
134
+ status: input.quality?.status === 'pass' ? 'pass' : 'fail',
135
+ baselineCommit: input.run.featurePlan.baselineCommit,
136
+ commitSha: input.commitSha,
137
+ integration: { path: input.integration.path, branch: input.integration.branch },
138
+ changedPaths,
139
+ changedPathCount: input.changedPaths.length,
140
+ workerClaims,
141
+ checks,
142
+ failure: input.failure === undefined ? null : bounded(input.failure, 300),
143
+ nextAction: nextAction(input.quality?.status === 'pass' ? 'pass' : 'fail'),
144
+ checkedAt: input.checkedAt,
145
+ });
146
+ }
147
+
148
+ export function createVerificationReportStore(paths) {
149
+ assertResolvedStatePaths(paths);
150
+ const snapshots = createSnapshotStore(paths, { environment: {} });
151
+ return Object.freeze({
152
+ async readOnly() {
153
+ const snapshot = await readSnapshotWithoutLock(paths);
154
+ return snapshot === null ? null : immutableJson({ ...valid(snapshot.data), version: snapshot.version });
155
+ },
156
+ async write(report) {
157
+ const data = valid(report);
158
+ const version = await snapshots.version();
159
+ let stored;
160
+ try { stored = await snapshots.write(data, { expectedVersion: version, environment: {} }); }
161
+ catch (error) {
162
+ if (/state version conflict/i.test(error?.message ?? '')) throw new VerificationReportVersionConflictError();
163
+ throw error;
164
+ }
165
+ return immutableJson({ ...valid(stored.data), version: stored.version });
166
+ },
167
+ });
168
+ }
@@ -0,0 +1,375 @@
1
+ import {assertSelectedProtocolRefs, selectedProtocolStatus} from '../protocols/project.js';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+
4
+ import { assertGitClient } from '../git/client.js';
5
+ import { immutableJson } from '../clients/contract.js';
6
+ import { activeProtocolContextRefs } from '../commands/protocols.js';
7
+ import { loadProjectConfig } from '../config/load.js';
8
+ import { resolveFeatureRunPaths } from '../state/paths.js';
9
+ import { createWorkRequest, userAcceptanceCriteria } from '../work-request/contract.js';
10
+ import { resolveInlineWorkRequest, resolveMarkdownWorkRequest } from '../work-request/local.js';
11
+ import { resolveHostWorkRequest } from '../work-request/host.js';
12
+ import { resolveTrackerWorkRequest } from '../work-request/tracker.js';
13
+ import { createFeaturePlan, featurePlanDigest } from './plan-contract.js';
14
+ import { createFeaturePlanner, createHostFeaturePlan } from './planner.js';
15
+ import { createFeatureRunStore } from './run-store.js';
16
+ import { acquireHostRunLock } from './host-run-lock.js';
17
+
18
+ const INPUT_KEYS = new Set([
19
+ 'gitClient', 'planningClientFor', 'trackerAdapterFor', 'executeFeature', 'now', 'loadConfig', 'protocolsFor',
20
+ ]);
21
+ const CLIENTS = new Set(['claude', 'codex', 'host']);
22
+ const TRACKERS = new Set(['jira', 'linear']);
23
+ const RUN_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
24
+ const DIGEST = /^[a-f0-9]{64}$/;
25
+ const REF = /^[a-z][a-z0-9-]{0,63}:[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
26
+ const PROTOCOL_REF = /^protocol:[a-z][a-z0-9]*(?:-[a-z0-9]+)*:[1-9][0-9]*:sha256:[a-f0-9]{64}$/;
27
+
28
+ export class FeatureWorkflowError extends Error {
29
+ constructor(reason = 'invalid-input') {
30
+ const messages = {
31
+ 'invalid-input': 'Feature workflow input is invalid.',
32
+ 'configuration': 'Feature workflow configuration is missing or invalid.',
33
+ 'repository': 'Feature workflow requires a clean checked-out default branch.',
34
+ 'run-missing': 'Feature run was not found.',
35
+ 'state-conflict': 'Feature run state changed or does not permit this operation.',
36
+ 'proposal-mismatch': 'Feature activation does not match the exact stored proposal.',
37
+ 'execution-result': 'Feature execution returned an invalid bounded result.',
38
+ 'host-use-work': 'Host runs use work status and work next; blocked host work requires a new reviewed corrective proposal.',
39
+ };
40
+ super(messages[reason] ?? messages['invalid-input']);
41
+ this.name = 'FeatureWorkflowError';
42
+ this.code = `ERR_FEATURE_WORKFLOW_${reason.replaceAll('-', '_').toUpperCase()}`;
43
+ this.safeMessage = this.message;
44
+ this.details = Object.freeze({ reason });
45
+ }
46
+ }
47
+
48
+ function fail(reason) { throw new FeatureWorkflowError(reason); }
49
+
50
+ function capture(input, allowed, required = allowed) {
51
+ try {
52
+ if (!input || typeof input !== 'object' || Array.isArray(input)
53
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail('invalid-input');
54
+ const keys = Reflect.ownKeys(input);
55
+ if (keys.length > allowed.size || keys.some(key => typeof key !== 'string' || !allowed.has(key))
56
+ || [...required].some(key => !keys.includes(key))) fail('invalid-input');
57
+ const output = Object.create(null);
58
+ for (const key of keys) {
59
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
60
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-input');
61
+ output[key] = descriptor.value;
62
+ }
63
+ return output;
64
+ } catch (error) {
65
+ if (error instanceof FeatureWorkflowError) throw error;
66
+ fail('invalid-input');
67
+ }
68
+ }
69
+
70
+ function absolute(value) {
71
+ if (typeof value !== 'string' || value.length < 2 || value.length > 4096 || !isAbsolute(value)
72
+ || resolve(value) !== value || /[\u0000\r\n]/.test(value)) fail('invalid-input');
73
+ return value;
74
+ }
75
+
76
+ function exactRunId(value) {
77
+ if (typeof value !== 'string' || value.length > 64 || !RUN_ID.test(value)) fail('invalid-input');
78
+ return value;
79
+ }
80
+
81
+ function expectedVersion(value) {
82
+ if (!Number.isSafeInteger(value) || value < 1) fail('invalid-input');
83
+ return value;
84
+ }
85
+
86
+ function executionSignal(options) {
87
+ if (!options || typeof options !== 'object' || Array.isArray(options)
88
+ || Reflect.ownKeys(options).some(key => key !== 'signal')
89
+ || (options.signal !== undefined && !(options.signal instanceof AbortSignal))) fail('invalid-input');
90
+ return options.signal;
91
+ }
92
+
93
+ function workerExecutionOptions(options) {
94
+ if (!options || typeof options !== 'object' || Array.isArray(options)
95
+ || Reflect.ownKeys(options).some(key => !['signal', 'confirmDependencyInstall'].includes(key))
96
+ || (options.signal !== undefined && !(options.signal instanceof AbortSignal))
97
+ || (options.confirmDependencyInstall !== undefined && typeof options.confirmDependencyInstall !== 'function')) fail('invalid-input');
98
+ return Object.freeze({
99
+ signal: options.signal,
100
+ ...(options.confirmDependencyInstall ? { confirmDependencyInstall: options.confirmDependencyInstall } : {}),
101
+ });
102
+ }
103
+
104
+ function sourcePath(project, value) {
105
+ const request = absolute(value);
106
+ const path = relative(project, request);
107
+ if (!path || path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) fail('invalid-input');
108
+ return path.replaceAll(sep, '/');
109
+ }
110
+
111
+ function selectedTracker(config, requested) {
112
+ if (requested !== undefined && !TRACKERS.has(requested)) fail('invalid-input');
113
+ const candidates = config.providers.providers.filter(provider => TRACKERS.has(provider.kind)
114
+ && (provider.transport === undefined || provider.transport === 'direct-api')
115
+ && (!provider.projectIds?.length || provider.projectIds.includes(config.project.id))
116
+ && provider.mode !== 'disabled' && provider.capabilities.includes('issues-read'));
117
+ if (requested !== undefined) {
118
+ if (!candidates.some(provider => provider.kind === requested)) fail('configuration');
119
+ return requested;
120
+ }
121
+ const kinds = [...new Set(candidates.map(provider => provider.kind))];
122
+ if (kinds.length !== 1) fail('configuration');
123
+ return kinds[0];
124
+ }
125
+
126
+ function runIdentifier(plan, request) {
127
+ const suffix = request.digest.slice(0, 12);
128
+ const prefix = plan.id.slice(0, 64 - suffix.length - 1).replace(/-+$/, '');
129
+ return exactRunId(`${prefix}-${suffix}`);
130
+ }
131
+
132
+ function proposalView(record) {
133
+ const profile = record.featurePlan.clientProfile;
134
+ const summary = profile === undefined
135
+ ? `${record.featurePlan.nodes.length} governed nodes; activation and final delivery require human approval.`
136
+ : `${record.featurePlan.nodes.length} governed nodes; Claude ${profile.model}, ${profile.planning.timeoutMs / 1000}s/$${profile.planning.maxCostUsd} planning, $${profile.execution.maxCostUsd} execution cap, no fallback; activation and final delivery require human approval.`;
137
+ return immutableJson({
138
+ runId: record.runId,
139
+ version: record.version,
140
+ status: record.status,
141
+ proposalDigest: record.proposalDigest,
142
+ summary,
143
+ workRequest: record.workRequest,
144
+ featurePlan: record.featurePlan,
145
+ });
146
+ }
147
+
148
+ function lifecycleView(record, summary) {
149
+ return immutableJson({ ...record, ...(summary === undefined ? {} : { summary }) });
150
+ }
151
+
152
+ function executionResult(value) {
153
+ const input = capture(value, new Set(['status', 'summary', 'runtimeRefs', 'evidenceRefs']));
154
+ if (!['blocked', 'awaiting-final-approval'].includes(input.status)
155
+ || typeof input.summary !== 'string' || input.summary.length < 1 || input.summary.length > 4000
156
+ || !Array.isArray(input.runtimeRefs) || !Array.isArray(input.evidenceRefs)
157
+ || input.runtimeRefs.length > 256 || input.evidenceRefs.length > 256
158
+ || [...input.runtimeRefs, ...input.evidenceRefs].some(ref => typeof ref !== 'string' || !REF.test(ref))) {
159
+ fail('execution-result');
160
+ }
161
+ return immutableJson(input);
162
+ }
163
+
164
+ export function createFeatureWorkflow(input) {
165
+ let configured;
166
+ try {
167
+ configured = capture(input, INPUT_KEYS, new Set(['gitClient', 'planningClientFor', 'executeFeature']));
168
+ assertGitClient(configured.gitClient);
169
+ } catch (error) {
170
+ if (error instanceof FeatureWorkflowError) throw error;
171
+ fail('configuration');
172
+ }
173
+ const gitClient = configured.gitClient;
174
+ const planningClientFor = configured.planningClientFor;
175
+ const trackerAdapterFor = configured.trackerAdapterFor;
176
+ const executeFeature = configured.executeFeature;
177
+ const now = configured.now ?? (() => new Date().toISOString());
178
+ const loadConfig = configured.loadConfig ?? loadProjectConfig;
179
+ const protocolsFor = configured.protocolsFor ?? (project => activeProtocolContextRefs(project));
180
+ if (typeof planningClientFor !== 'function' || typeof executeFeature !== 'function'
181
+ || (trackerAdapterFor !== undefined && typeof trackerAdapterFor !== 'function')
182
+ || typeof now !== 'function' || typeof loadConfig !== 'function' || typeof protocolsFor !== 'function') fail('configuration');
183
+
184
+ async function repository(project) {
185
+ const root = absolute(project);
186
+ let observed;
187
+ let config;
188
+ try { observed = await gitClient.inspectRepository(root); } catch { fail('repository'); }
189
+ try { config = await loadConfig(root); } catch { fail('configuration'); }
190
+ if (observed.root !== root || observed.detached || observed.dirty
191
+ || observed.branch !== config.project.repository.defaultBranch) fail('repository');
192
+ return { observed, config };
193
+ }
194
+
195
+ async function storeFor(project, runId) {
196
+ const paths = await resolveFeatureRunPaths(absolute(project), exactRunId(runId));
197
+ return createFeatureRunStore(paths);
198
+ }
199
+
200
+ async function readRun(project, runId) {
201
+ const store = await storeFor(project, runId);
202
+ const record = await store.read();
203
+ if (record === null) fail('run-missing');
204
+ return { store, record };
205
+ }
206
+
207
+ async function propose(raw, options = {}) {
208
+ const signal = executionSignal(options);
209
+ const request = capture(
210
+ raw,
211
+ new Set(['project', 'source', 'client', 'tracker', 'decomposition', 'userAcceptanceCriteria']),
212
+ new Set(['project', 'source']),
213
+ );
214
+ const { observed, config } = await repository(request.project);
215
+ const selectedClient = request.client ?? 'claude';
216
+ if (!CLIENTS.has(selectedClient)) fail('invalid-input');
217
+ const source = capture(request.source, new Set(['kind', 'value']));
218
+ const additions = request.userAcceptanceCriteria === undefined ? undefined : userAcceptanceCriteria(request.userAcceptanceCriteria);
219
+ if (additions && source.kind !== 'ticket') fail('invalid-input');
220
+ let workRequest;
221
+ if (source.kind === 'inline') {
222
+ workRequest = resolveInlineWorkRequest({ text: source.value, capturedAt: now() });
223
+ } else if (source.kind === 'file') {
224
+ workRequest = await resolveMarkdownWorkRequest({
225
+ root: observed.root, path: sourcePath(observed.root, source.value), capturedAt: now(),
226
+ });
227
+ } else if (source.kind === 'host-observation') {
228
+ if (selectedClient !== 'host') fail('invalid-input');
229
+ workRequest = resolveHostWorkRequest({ config, bundle: source.value, capturedAt: now() });
230
+ } else if (source.kind === 'ticket') {
231
+ if (typeof trackerAdapterFor !== 'function') fail('configuration');
232
+ const provider = selectedTracker(config, request.tracker);
233
+ const adapter = await trackerAdapterFor(immutableJson({ provider, config, project: observed.root }));
234
+ workRequest = await resolveTrackerWorkRequest({ provider, ticketId: source.value, adapter, ...(additions ? { userAcceptanceCriteria: additions } : {}) });
235
+ } else fail('invalid-input');
236
+ let protocolRefs;
237
+ try { protocolRefs = await protocolsFor(observed.root); } catch { fail('configuration'); }
238
+ if (!Array.isArray(protocolRefs) || protocolRefs.length > 256
239
+ || protocolRefs.some(ref => typeof ref !== 'string' || !PROTOCOL_REF.test(ref))
240
+ || new Set(protocolRefs).size !== protocolRefs.length) fail('configuration');
241
+ if (protocolRefs.length > 0) {
242
+ workRequest = createWorkRequest({
243
+ source: workRequest.source,
244
+ ...(workRequest.context === undefined ? {} : { context: workRequest.context }),
245
+ ...(workRequest.criteriaProvenance === undefined ? {} : { criteriaProvenance: workRequest.criteriaProvenance }),
246
+ title: workRequest.title,
247
+ description: workRequest.description,
248
+ acceptanceCriteria: workRequest.acceptanceCriteria,
249
+ contextRefs: [...workRequest.contextRefs, ...protocolRefs].sort(),
250
+ capturedAt: workRequest.capturedAt,
251
+ });
252
+ }
253
+ if ((selectedClient === 'host') !== (request.decomposition !== undefined)) fail('invalid-input');
254
+ let featurePlan;
255
+ if (selectedClient === 'host') {
256
+ featurePlan = createHostFeaturePlan({
257
+ config,
258
+ workRequest,
259
+ baselineCommit: observed.headSha,
260
+ decomposition: request.decomposition,
261
+ });
262
+ } else {
263
+ const planningClient = await planningClientFor(immutableJson({ client: selectedClient, project: observed.root }));
264
+ const planner = createFeaturePlanner({ planningClient: {
265
+ propose: contract => planningClient.propose(contract, { signal }),
266
+ } });
267
+ featurePlan = await planner.propose({
268
+ config, workRequest, baselineCommit: observed.headSha, client: selectedClient,
269
+ });
270
+ }
271
+ const runId = runIdentifier(featurePlan, workRequest);
272
+ const store = await storeFor(observed.root, runId);
273
+ const existing = await store.read();
274
+ if (existing !== null) {
275
+ if (existing.proposalDigest !== featurePlanDigest(featurePlan)) fail('state-conflict');
276
+ return proposalView(existing);
277
+ }
278
+ const record = await store.create({ workRequest, featurePlan, createdAt: now() });
279
+ return proposalView(record);
280
+ }
281
+
282
+ async function validateSavedPlan(project, record) {
283
+ assertSelectedProtocolRefs(project, record.workRequest.contextRefs);
284
+ const config = await loadConfig(absolute(project));
285
+ createFeaturePlan({ proposal: record.featurePlan, config, workRequest: record.workRequest,
286
+ baselineCommit: record.featurePlan.baselineCommit, client: record.featurePlan.client });
287
+ }
288
+
289
+ async function start(raw) {
290
+ const request = capture(raw, new Set(['project', 'runId', 'expectedVersion', 'proposalDigest']));
291
+ const { store, record } = await readRun(request.project, request.runId);
292
+ if (record.status !== 'proposed' || record.version !== expectedVersion(request.expectedVersion)) fail('state-conflict');
293
+ if (typeof request.proposalDigest !== 'string' || !DIGEST.test(request.proposalDigest)
294
+ || request.proposalDigest !== record.proposalDigest) fail('proposal-mismatch');
295
+ await validateSavedPlan(request.project, record);
296
+ const at = now();
297
+ const updated = await store.update({
298
+ status: 'approved', updatedAt: at,
299
+ activation: {
300
+ approverId: 'human-cli-operator', approvedAt: at,
301
+ requestDigest: record.workRequest.digest, proposalDigest: record.proposalDigest,
302
+ },
303
+ runtimeRefs: record.runtimeRefs, evidenceRefs: [...record.evidenceRefs, 'approval:activation'],
304
+ }, { expectedVersion: record.version });
305
+ return lifecycleView(updated);
306
+ }
307
+
308
+ async function status(raw) {
309
+ const request = capture(raw, new Set(['project', 'runId']));
310
+ const record = (await readRun(request.project, request.runId)).record;
311
+ return immutableJson({...lifecycleView(record), protocols: selectedProtocolStatus(request.project, record.workRequest.contextRefs)});
312
+ }
313
+
314
+ async function resume(raw, options = {}) {
315
+ const execution = workerExecutionOptions(options);
316
+ const signal = execution.signal;
317
+ const request = capture(raw, new Set(['project', 'runId', 'expectedVersion']));
318
+ const { store, record } = await readRun(request.project, request.runId);
319
+ if (record.featurePlan.client === 'host') fail('host-use-work');
320
+ if (!['approved', 'blocked'].includes(record.status) || record.version !== expectedVersion(request.expectedVersion)) {
321
+ fail('state-conflict');
322
+ }
323
+ await validateSavedPlan(request.project, record);
324
+ const running = await store.update({
325
+ status: 'running', updatedAt: now(), runtimeRefs: record.runtimeRefs, evidenceRefs: record.evidenceRefs,
326
+ }, { expectedVersion: record.version });
327
+ let result;
328
+ try {
329
+ result = executionResult(await executeFeature(
330
+ immutableJson({ project: absolute(request.project), run: running }), execution,
331
+ ));
332
+ } catch (error) {
333
+ try {
334
+ await store.update({
335
+ status: 'blocked', updatedAt: now(), runtimeRefs: running.runtimeRefs, evidenceRefs: running.evidenceRefs,
336
+ }, { expectedVersion: running.version });
337
+ } catch {}
338
+ throw error;
339
+ }
340
+ const updated = await store.update({
341
+ status: result.status,
342
+ updatedAt: now(),
343
+ runtimeRefs: [...new Set([...running.runtimeRefs, ...result.runtimeRefs])],
344
+ evidenceRefs: [...new Set([...running.evidenceRefs, ...result.evidenceRefs])],
345
+ }, { expectedVersion: running.version });
346
+ return lifecycleView(updated, result.summary);
347
+ }
348
+
349
+ async function cancel(raw) {
350
+ const request = capture(raw, new Set(['project', 'runId', 'expectedVersion']));
351
+ const { store, record } = await readRun(request.project, request.runId);
352
+ const cancelCurrent = async current => {
353
+ if (current.version !== expectedVersion(request.expectedVersion)
354
+ || !['proposed', 'approved', 'running', 'blocked', 'awaiting-final-approval'].includes(current.status)) fail('state-conflict');
355
+ return lifecycleView(await store.update({
356
+ status: 'cancelled', updatedAt: now(), runtimeRefs: current.runtimeRefs, evidenceRefs: current.evidenceRefs,
357
+ }, { expectedVersion: current.version }));
358
+ };
359
+ if (record.featurePlan.client !== 'host') {
360
+ // The CLI cannot prove that a spawned worker has stopped. Cancelling its
361
+ // record while it runs would leave an untracked writer in the checkout.
362
+ if (record.status === 'running') fail('state-conflict');
363
+ return cancelCurrent(record);
364
+ }
365
+ const paths = await resolveFeatureRunPaths(request.project, request.runId);
366
+ const lock = await acquireHostRunLock(paths);
367
+ try {
368
+ const current = await store.read();
369
+ if (current === null || current.featurePlan.client !== 'host') fail('state-conflict');
370
+ return await cancelCurrent(current);
371
+ } finally { await lock.release(); }
372
+ }
373
+
374
+ return Object.freeze({ propose, start, status, resume, watch: resume, cancel });
375
+ }