@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,497 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, statSync } from 'node:fs';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import { isAbsolute, resolve } from 'node:path';
4
+
5
+ import { sha256 } from '../evidence/checksum.js';
6
+ import { assertGitClient } from '../git/client.js';
7
+ import { runCommand } from '../policy/commands.js';
8
+ import { redactSecrets } from '../state/redact.js';
9
+
10
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
11
+ const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
12
+ const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
13
+ const AC_ID = /^[A-Z][A-Z0-9]+-[1-9][0-9]*-AC[1-9][0-9]*$/;
14
+ const PACKAGE_RUNNERS = new Set(['npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'yarn', 'yarn.cmd', 'bun']);
15
+ const PACKAGE_SCRIPT = /^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
16
+ const MAX_PACKAGE_MANIFEST_BYTES = 256 * 1024;
17
+ const qualityRuns = new WeakSet();
18
+
19
+ export class QualityError extends Error {
20
+ constructor(reason = 'invalid-quality-input') {
21
+ const messages = {
22
+ 'invalid-quality-input': 'Quality gate input is invalid.',
23
+ 'unsafe-artifact': 'Quality gate artifact path is unsafe.',
24
+ 'clock-regressed': 'Quality gate clock regressed.',
25
+ 'gate-failed': 'Quality gate execution failed safely.',
26
+ };
27
+ super(messages[reason] ?? messages['invalid-quality-input']);
28
+ this.name = 'QualityError';
29
+ this.code = 'ERR_QUALITY_GATE';
30
+ this.safeMessage = this.message;
31
+ this.details = Object.freeze({ reason });
32
+ }
33
+ }
34
+
35
+ function fail(reason) { throw new QualityError(reason); }
36
+
37
+ function capture(value, allowed, required = []) {
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-quality-input');
39
+ let keys;
40
+ try { keys = Reflect.ownKeys(value); } catch { fail('invalid-quality-input'); }
41
+ if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-quality-input');
42
+ const result = Object.create(null);
43
+ try {
44
+ for (const key of keys) {
45
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
46
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-quality-input');
47
+ result[key] = descriptor.value;
48
+ }
49
+ } catch (error) {
50
+ if (error instanceof QualityError) throw error;
51
+ fail('invalid-quality-input');
52
+ }
53
+ if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-quality-input');
54
+ return result;
55
+ }
56
+
57
+ function array(value, maximum, convert) {
58
+ try {
59
+ if (!Array.isArray(value)) fail('invalid-quality-input');
60
+ const descriptor = Object.getOwnPropertyDescriptor(value, 'length');
61
+ const length = descriptor?.value;
62
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximum
63
+ || Reflect.ownKeys(value).length !== length + 1) fail('invalid-quality-input');
64
+ const result = [];
65
+ for (let index = 0; index < length; index += 1) {
66
+ const child = Object.getOwnPropertyDescriptor(value, String(index));
67
+ if (!child?.enumerable || !Object.hasOwn(child, 'value')) fail('invalid-quality-input');
68
+ result.push(convert(child.value));
69
+ }
70
+ return result;
71
+ } catch (error) {
72
+ if (error instanceof QualityError) throw error;
73
+ fail('invalid-quality-input');
74
+ }
75
+ }
76
+
77
+ function id(value) {
78
+ if (typeof value !== 'string' || value.length > 64 || !ID.test(value)) fail('invalid-quality-input');
79
+ return value;
80
+ }
81
+
82
+ function relativePath(value, allowDot = false) {
83
+ if (allowDot && value === '.') return value;
84
+ if (typeof value !== 'string' || value.length < 1 || value.length > 500
85
+ || isAbsolute(value) || value.normalize('NFKC') !== value
86
+ || /[\\:\u0000-\u001f\u007f]/.test(value) || value.startsWith('-')
87
+ || value.startsWith('/') || value.endsWith('/') || value.includes('//')) fail('invalid-quality-input');
88
+ const parts = value.split('/');
89
+ if (parts.some(part => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
90
+ || WINDOWS_RESERVED.test(part) || part.toUpperCase().toLowerCase() === '.git')) fail('invalid-quality-input');
91
+ return value;
92
+ }
93
+
94
+ function strings(value, maximum, itemMaximum) {
95
+ return array(value, maximum, child => {
96
+ if (typeof child !== 'string' || child.length > itemMaximum || child.includes('\0')) fail('invalid-quality-input');
97
+ return child;
98
+ });
99
+ }
100
+
101
+ function environment(value) {
102
+ if (value === undefined) return Object.freeze(Object.create(null));
103
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-quality-input');
104
+ let keys;
105
+ try { keys = Reflect.ownKeys(value); } catch { fail('invalid-quality-input'); }
106
+ if (keys.length > 256 || keys.some(key => typeof key !== 'string' || !/^[A-Z_][A-Z0-9_]*$/.test(key))) {
107
+ fail('invalid-quality-input');
108
+ }
109
+ const result = Object.create(null);
110
+ try {
111
+ for (const key of keys) {
112
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
113
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')
114
+ || typeof descriptor.value !== 'string' || descriptor.value.length > 32_768
115
+ || descriptor.value.includes('\0')) fail('invalid-quality-input');
116
+ result[key] = descriptor.value;
117
+ }
118
+ } catch (error) {
119
+ if (error instanceof QualityError) throw error;
120
+ fail('invalid-quality-input');
121
+ }
122
+ return Object.freeze(result);
123
+ }
124
+
125
+ function acceptanceCriterion(value) {
126
+ if (typeof value !== 'string' || value.length > 100 || !AC_ID.test(value)) fail('invalid-quality-input');
127
+ return value;
128
+ }
129
+
130
+ function configuredTest(value) {
131
+ const input = capture(value, new Set(['id', 'acceptanceCriteria']), ['id', 'acceptanceCriteria']);
132
+ const acceptanceCriteria = array(input.acceptanceCriteria, 128, acceptanceCriterion);
133
+ if (acceptanceCriteria.length === 0
134
+ || new Set(acceptanceCriteria.map(item => item.toLowerCase())).size !== acceptanceCriteria.length) {
135
+ fail('invalid-quality-input');
136
+ }
137
+ return Object.freeze({ id: id(input.id), acceptanceCriteria: Object.freeze(acceptanceCriteria) });
138
+ }
139
+
140
+ function packageScript(value, args) {
141
+ if (value === undefined) return null;
142
+ const input = capture(value, new Set(['runner', 'script']), ['runner', 'script']);
143
+ if (typeof input.runner !== 'string' || !PACKAGE_RUNNERS.has(input.runner.toLowerCase())
144
+ || typeof input.script !== 'string' || !PACKAGE_SCRIPT.test(input.script)
145
+ || args.length !== 2 || args[0] !== 'run' || args[1] !== input.script) fail('invalid-quality-input');
146
+ return Object.freeze({ runner: input.runner.toLowerCase(), script: input.script });
147
+ }
148
+
149
+ function gate(value) {
150
+ const input = capture(value, new Set([
151
+ 'id', 'executable', 'args', 'cwd', 'packageScript', 'execution', 'required', 'artifactPaths', 'tests', 'resultPath',
152
+ ]), ['id', 'executable', 'args', 'cwd', 'required', 'artifactPaths', 'tests']);
153
+ const gateId = id(input.id);
154
+ if (input.execution !== undefined && input.execution !== 'argv') fail('invalid-quality-input');
155
+ if (input.execution === 'argv' && input.packageScript !== undefined) fail('invalid-quality-input');
156
+ if (typeof input.executable !== 'string' || !isAbsolute(input.executable)
157
+ || input.executable.length > 1_024 || /[\u0000\r\n]/.test(input.executable)
158
+ || typeof input.required !== 'boolean') fail('invalid-quality-input');
159
+ const artifactPaths = strings(input.artifactPaths, 256, 500).map(path => relativePath(path));
160
+ const tests = array(input.tests, 2_000, configuredTest);
161
+ const args = Object.freeze(strings(input.args, 256, 4_096));
162
+ if (new Set(artifactPaths.map(path => path.toLowerCase())).size !== artifactPaths.length) fail('invalid-quality-input');
163
+ if (new Set(tests.map(item => item.id.toLowerCase())).size !== tests.length) fail('invalid-quality-input');
164
+ const resultPath = input.resultPath === undefined ? null : relativePath(input.resultPath);
165
+ if ((tests.length > 0 && resultPath === null) || (resultPath !== null && !artifactPaths.includes(resultPath))) {
166
+ fail('invalid-quality-input');
167
+ }
168
+ return Object.freeze({
169
+ id: gateId,
170
+ executable: input.executable,
171
+ ...(input.execution === 'argv' ? { execution: 'argv' } : {}),
172
+ args,
173
+ cwd: relativePath(input.cwd, true),
174
+ packageScript: packageScript(input.packageScript, args),
175
+ required: input.required,
176
+ artifactPaths: Object.freeze(artifactPaths),
177
+ tests: Object.freeze(tests),
178
+ resultPath,
179
+ });
180
+ }
181
+
182
+ function withPinnedProject(project, operation) {
183
+ let original;
184
+ try { original = process.cwd(); } catch { fail('unsafe-artifact'); }
185
+ let changed = false;
186
+ let result;
187
+ let primary = null;
188
+ try {
189
+ process.chdir(project.path);
190
+ changed = true;
191
+ const pinned = statSync('.', { bigint: true });
192
+ if (!pinned.isDirectory() || pinned.dev !== project.dev || pinned.ino !== project.ino) fail('unsafe-artifact');
193
+ result = operation();
194
+ } catch (error) { primary = error; }
195
+ if (changed) {
196
+ try { process.chdir(original); }
197
+ catch (error) { if (primary === null) primary = error; }
198
+ }
199
+ if (primary !== null) {
200
+ if (primary instanceof QualityError) throw primary;
201
+ fail('unsafe-artifact');
202
+ }
203
+ return result;
204
+ }
205
+
206
+ function readPinnedFile(project, relative, maxBytes) {
207
+ return withPinnedProject(project, () => {
208
+ const components = relative.split('/');
209
+ for (const component of components.slice(0, -1)) {
210
+ const before = lstatSync(component, { bigint: true });
211
+ if (!before.isDirectory() || before.isSymbolicLink()) fail('unsafe-artifact');
212
+ process.chdir(component);
213
+ const pinned = statSync('.', { bigint: true });
214
+ if (!pinned.isDirectory() || pinned.dev !== before.dev || pinned.ino !== before.ino) fail('unsafe-artifact');
215
+ }
216
+ const name = components.at(-1);
217
+ const before = lstatSync(name, { bigint: true });
218
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
219
+ || before.size < 0n || before.size > BigInt(maxBytes)) fail('unsafe-artifact');
220
+ let descriptor;
221
+ try {
222
+ descriptor = openSync(name, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
223
+ const opened = fstatSync(descriptor, { bigint: true });
224
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino
225
+ || opened.size !== before.size || opened.mtimeNs !== before.mtimeNs || opened.ctimeNs !== before.ctimeNs) {
226
+ fail('unsafe-artifact');
227
+ }
228
+ const bytes = readFileSync(descriptor);
229
+ const after = fstatSync(descriptor, { bigint: true });
230
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
231
+ || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs
232
+ || after.size !== BigInt(bytes.length)) fail('unsafe-artifact');
233
+ const current = lstatSync(name, { bigint: true });
234
+ if (current.isSymbolicLink() || current.dev !== after.dev || current.ino !== after.ino
235
+ || current.size !== after.size || current.mtimeNs !== after.mtimeNs || current.ctimeNs !== after.ctimeNs) {
236
+ fail('unsafe-artifact');
237
+ }
238
+ return Object.freeze({
239
+ bytes, size: bytes.length, sha256: sha256(bytes), dev: after.dev.toString(), ino: after.ino.toString(),
240
+ mtimeNs: after.mtimeNs.toString(), ctimeNs: after.ctimeNs.toString(),
241
+ });
242
+ } finally { if (descriptor !== undefined) closeSync(descriptor); }
243
+ });
244
+ }
245
+
246
+ function verifyPackageScript(project, configured) {
247
+ if (configured.packageScript === null) return true;
248
+ const relative = configured.cwd === '.'
249
+ ? 'package.json' : `${configured.cwd}/package.json`;
250
+ let manifest;
251
+ try {
252
+ const contents = readPinnedFile(project, relative, MAX_PACKAGE_MANIFEST_BYTES);
253
+ manifest = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(contents.bytes));
254
+ } catch {
255
+ fail('gate-failed');
256
+ }
257
+ const scripts = manifest?.scripts;
258
+ if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)
259
+ || typeof scripts[configured.packageScript.script] !== 'string') return false;
260
+ return true;
261
+ }
262
+
263
+ async function priorResult(project, path) {
264
+ if (path === null) return null;
265
+ try { return readPinnedFile(project, path, 1024 * 1024); }
266
+ catch (error) {
267
+ let missing = false;
268
+ try {
269
+ withPinnedProject(project, () => {
270
+ try { lstatSync(path); } catch (nested) { missing = nested?.code === 'ENOENT'; }
271
+ });
272
+ } catch {}
273
+ if (missing) return null;
274
+ fail('unsafe-artifact');
275
+ }
276
+ }
277
+
278
+ function attestedTest(value) {
279
+ const input = capture(value, new Set(['id', 'status', 'acceptanceCriteria']), ['id', 'status', 'acceptanceCriteria']);
280
+ if (!['passed', 'failed', 'skipped'].includes(input.status)) fail('gate-failed');
281
+ const acceptanceCriteria = array(input.acceptanceCriteria, 128, acceptanceCriterion);
282
+ if (acceptanceCriteria.length === 0
283
+ || new Set(acceptanceCriteria.map(item => item.toLowerCase())).size !== acceptanceCriteria.length) fail('gate-failed');
284
+ return Object.freeze({ id: id(input.id), status: input.status, acceptanceCriteria: Object.freeze(acceptanceCriteria) });
285
+ }
286
+
287
+ async function readAttestation(project, configured, before) {
288
+ if (configured.resultPath === null) return Object.freeze([]);
289
+ let contents;
290
+ try { contents = readPinnedFile(project, configured.resultPath, 1024 * 1024); }
291
+ catch { fail('gate-failed'); }
292
+ if (before && before.dev === contents.dev && before.ino === contents.ino) fail('gate-failed');
293
+ let parsed;
294
+ try { parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(contents.bytes)); }
295
+ catch { fail('gate-failed'); }
296
+ const record = capture(parsed, new Set(['schemaVersion', 'gateId', 'tests']), ['schemaVersion', 'gateId', 'tests']);
297
+ if (record.schemaVersion !== 1 || record.gateId !== configured.id) fail('gate-failed');
298
+ const tests = array(record.tests, 2_000, attestedTest);
299
+ if (tests.length !== configured.tests.length
300
+ || new Set(tests.map(test => test.id.toLowerCase())).size !== tests.length) fail('gate-failed');
301
+ for (let index = 0; index < tests.length; index += 1) {
302
+ const expected = configured.tests[index];
303
+ const actual = tests[index];
304
+ if (actual.id !== expected.id
305
+ || actual.acceptanceCriteria.length !== expected.acceptanceCriteria.length
306
+ || actual.acceptanceCriteria.some((criterion, criterionIndex) => criterion !== expected.acceptanceCriteria[criterionIndex])) {
307
+ fail('gate-failed');
308
+ }
309
+ }
310
+ return Object.freeze(tests.map(test => Object.freeze({
311
+ ...test, gateId: configured.id, resultPath: configured.resultPath, resultSha256: contents.sha256,
312
+ })));
313
+ }
314
+
315
+ async function canonicalDirectory(path) {
316
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path.length > 1_024) fail('invalid-quality-input');
317
+ const before = await lstat(path, { bigint: true });
318
+ const canonical = await realpath(path);
319
+ const after = await lstat(path, { bigint: true });
320
+ if (!before.isDirectory() || before.isSymbolicLink() || !after.isDirectory() || after.isSymbolicLink()
321
+ || before.dev !== after.dev || before.ino !== after.ino || canonical !== path) fail('invalid-quality-input');
322
+ return Object.freeze({ path, dev: after.dev, ino: after.ino });
323
+ }
324
+
325
+ function timestamp(value) {
326
+ if (!Number.isSafeInteger(value) || value < 0) fail('invalid-quality-input');
327
+ return new Date(value).toISOString();
328
+ }
329
+
330
+ function immutable(value) {
331
+ if (Array.isArray(value)) return Object.freeze(value.map(immutable));
332
+ if (value && typeof value === 'object') {
333
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, immutable(child)])));
334
+ }
335
+ return value;
336
+ }
337
+
338
+ function snapshotInput(input, options) {
339
+ const value = capture(input, new Set([
340
+ 'projectRoot', 'commitSha', 'authority', 'gates', 'environment', 'timeoutMs',
341
+ 'maxOutputBytes', 'maxStreamOutputBytes',
342
+ ]), ['projectRoot', 'commitSha', 'authority', 'gates']);
343
+ const optionValues = capture(options, new Set(['now', 'gitClient', 'signal']), ['gitClient']);
344
+ if (!SHA.test(value.commitSha)) fail('invalid-quality-input');
345
+ const gates = array(value.gates, 64, gate);
346
+ if (gates.length === 0 || new Set(gates.map(item => item.id.toLowerCase())).size !== gates.length) fail('invalid-quality-input');
347
+ const allTestIds = gates.flatMap(item => item.tests.map(test => test.id.toLowerCase()));
348
+ if (new Set(allTestIds).size !== allTestIds.length) fail('invalid-quality-input');
349
+ if (optionValues.now !== undefined && typeof optionValues.now !== 'function') fail('invalid-quality-input');
350
+ if (optionValues.signal !== undefined && !(optionValues.signal instanceof AbortSignal)) fail('invalid-quality-input');
351
+ try { assertGitClient(optionValues.gitClient); } catch { fail('invalid-quality-input'); }
352
+ return Object.freeze({
353
+ ...value,
354
+ gates: Object.freeze(gates),
355
+ environment: environment(value.environment),
356
+ now: optionValues.now ?? Date.now,
357
+ gitClient: optionValues.gitClient,
358
+ signal: optionValues.signal,
359
+ });
360
+ }
361
+
362
+ export function assertQualityRun(value) {
363
+ if (!qualityRuns.has(value)) fail('invalid-quality-input');
364
+ }
365
+
366
+ export async function runQualityGates(input, options = {}) {
367
+ let value;
368
+ try { value = snapshotInput(input, options); } catch (error) {
369
+ if (error instanceof QualityError) throw error;
370
+ fail('invalid-quality-input');
371
+ }
372
+ const project = await canonicalDirectory(value.projectRoot).catch(error => {
373
+ if (error instanceof QualityError) throw error;
374
+ fail('invalid-quality-input');
375
+ });
376
+ let repository;
377
+ const projectRoot = project.path;
378
+ try { repository = await value.gitClient.inspectRepository(projectRoot); }
379
+ catch { fail('invalid-quality-input'); }
380
+ if (repository.root !== projectRoot || repository.headSha !== value.commitSha
381
+ || repository.dirty || repository.dirtyPaths.length !== 0) fail('invalid-quality-input');
382
+ const results = [];
383
+ const attestedTests = [];
384
+ for (const configured of value.gates) {
385
+ if (value.signal?.aborted) fail('gate-failed');
386
+ let startedMs;
387
+ let endedMs;
388
+ let commandResult;
389
+ const beforeResult = await priorResult(project, configured.resultPath);
390
+ try {
391
+ const scriptAvailable = verifyPackageScript(project, configured);
392
+ if (!scriptAvailable && configured.required) fail('gate-failed');
393
+ startedMs = value.now();
394
+ timestamp(startedMs);
395
+ commandResult = scriptAvailable ? await runCommand({
396
+ worktree: projectRoot,
397
+ authority: value.authority,
398
+ commands: {
399
+ [configured.id]: {
400
+ executable: configured.executable,
401
+ ...(configured.execution === 'argv' ? { execution: 'argv' } : {}),
402
+ args: configured.args,
403
+ action: 'command.' + configured.id,
404
+ },
405
+ },
406
+ environment: value.environment,
407
+ ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
408
+ ...(value.maxOutputBytes === undefined ? {} : { maxOutputBytes: value.maxOutputBytes }),
409
+ ...(value.maxStreamOutputBytes === undefined ? {} : { maxStreamOutputBytes: value.maxStreamOutputBytes }),
410
+ }, {
411
+ actorId: value.authority.actorId,
412
+ commandId: configured.id,
413
+ cwd: configured.cwd,
414
+ args: [],
415
+ }, { signal: value.signal }) : Object.freeze({
416
+ status: 'unavailable', code: 127, stdout: '', stderr: '',
417
+ redacted: false, suppressed: false, truncated: false,
418
+ });
419
+ endedMs = value.now();
420
+ if (value.signal?.aborted) fail('gate-failed');
421
+ timestamp(endedMs);
422
+ if (endedMs < startedMs) fail('clock-regressed');
423
+ } catch (error) {
424
+ if (error instanceof QualityError) throw error;
425
+ fail('gate-failed');
426
+ }
427
+
428
+ const artifacts = [];
429
+ let artifactFailure = false;
430
+ for (const path of configured.artifactPaths) {
431
+ try {
432
+ const contents = readPinnedFile(project, path, 32 * 1024 * 1024);
433
+ artifacts.push(Object.freeze({ path, sha256: contents.sha256, bytes: contents.size }));
434
+ } catch {
435
+ artifactFailure = true;
436
+ artifacts.push(Object.freeze({ path, status: 'missing-or-unsafe' }));
437
+ }
438
+ }
439
+ let gateTests = Object.freeze([]);
440
+ if (commandResult.status === 'success' && !artifactFailure) {
441
+ gateTests = await readAttestation(project, configured, beforeResult);
442
+ const resultArtifact = artifacts.find(artifact => artifact.path === configured.resultPath);
443
+ if (configured.resultPath !== null
444
+ && (!resultArtifact || gateTests.some(test => test.resultSha256 !== resultArtifact.sha256))) fail('gate-failed');
445
+ } else {
446
+ gateTests = Object.freeze(configured.tests.map(test => Object.freeze({
447
+ ...test, status: 'failed', gateId: configured.id,
448
+ ...(configured.resultPath === null ? {} : { resultPath: configured.resultPath }),
449
+ })));
450
+ }
451
+ attestedTests.push(...gateTests);
452
+ const passed = commandResult.status === 'success' && !artifactFailure
453
+ && gateTests.every(test => test.status === 'passed');
454
+ let commandProvenance;
455
+ try {
456
+ commandProvenance = redactSecrets({ executable: configured.executable, args: configured.args }, {
457
+ environment: value.environment,
458
+ });
459
+ } catch { fail('invalid-quality-input'); }
460
+ results.push(immutable({
461
+ id: configured.id,
462
+ required: configured.required,
463
+ status: passed ? 'passed' : 'failed',
464
+ commitSha: value.commitSha,
465
+ startedAt: timestamp(startedMs),
466
+ endedAt: timestamp(endedMs),
467
+ cwd: configured.cwd,
468
+ command: commandProvenance,
469
+ exitCode: commandResult.code,
470
+ executionStatus: commandResult.status,
471
+ output: {
472
+ stdout: commandResult.stdout,
473
+ stderr: commandResult.stderr,
474
+ redacted: commandResult.redacted,
475
+ suppressed: commandResult.suppressed,
476
+ truncated: commandResult.truncated,
477
+ },
478
+ artifacts,
479
+ }));
480
+ }
481
+ let after;
482
+ try { after = await value.gitClient.inspectRepository(projectRoot); }
483
+ catch { fail('gate-failed'); }
484
+ if (after.root !== repository.root || after.repositoryId !== repository.repositoryId
485
+ || after.rootIdentity.dev !== repository.rootIdentity.dev || after.rootIdentity.ino !== repository.rootIdentity.ino
486
+ || after.headSha !== repository.headSha || after.dirty || after.dirtyPaths.length !== 0) fail('gate-failed');
487
+ const failedRequired = results.some(result => result.required && result.status !== 'passed');
488
+ const output = immutable({
489
+ status: failedRequired ? 'fail' : 'pass',
490
+ projectRoot,
491
+ commitSha: repository.headSha,
492
+ gates: results,
493
+ tests: attestedTests,
494
+ });
495
+ qualityRuns.add(output);
496
+ return output;
497
+ }