@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,814 @@
1
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
2
+ import { Buffer } from 'node:buffer';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { CliError, EXIT_CODES } from '../cli/output.js';
6
+
7
+ const SKILL_CATEGORIES = {
8
+ Backend: ['backend-nestjs', 'backend-nestjs-agent', 'backend-django', 'backend-django-agent', 'backend-agent'],
9
+ 'Frontend & Mobile': ['frontend-nextjs', 'frontend-nextjs-agent', 'frontend-web-agent', 'mobile-react-native', 'mobile-agent'],
10
+ 'Data & Contracts': ['api-contract', 'postgres-analytics', 'data-performance-agent', 'database-migration'],
11
+ 'Quality & Testing': ['qa-bug-analysis', 'testing-quality', 'qa-agent', 'jest-agent', 'vitest-agent', 'playwright-agent', 'debugging'],
12
+ 'Delivery & Planning': ['product-jira-ticketing', 'bug-ticket-creation', 'sprint-planning', 'delivery-ticketing-agent', 'refactoring', 'documentation'],
13
+ Operations: ['devops-infra', 'devops-agent', 'cloudwatch-troubleshooting', 'kubernetes-troubleshooting', 'incident-postmortem', 'incident-response-agent', 'security-review'],
14
+ };
15
+
16
+ export const installedSkillName = name => `rivet-${name}`;
17
+
18
+ const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 750;
19
+ const DEFAULT_MAX_UPDATE_RESPONSE_BYTES = 32 * 1024;
20
+
21
+ function distributionPaths(packageRoot) {
22
+ return {
23
+ skills: join(packageRoot, 'dist', 'skills'),
24
+ agents: join(packageRoot, 'dist', 'agents'),
25
+ mandatory: join(packageRoot, 'dist', 'mandatory'),
26
+ };
27
+ }
28
+
29
+ function isWithin(root, candidate) {
30
+ const pathFromRoot = relative(root, candidate);
31
+ return pathFromRoot === '' || (
32
+ !isAbsolute(pathFromRoot)
33
+ && pathFromRoot !== '..'
34
+ && !pathFromRoot.startsWith(`..${sep}`)
35
+ );
36
+ }
37
+
38
+ function throwSymlinkConflict() {
39
+ throw new CliError('Target path contains a symbolic link.', 'REPOSITORY_CONFLICT');
40
+ }
41
+
42
+ function lstatIfExists(path, fs) {
43
+ try {
44
+ return fs.lstatSync(path);
45
+ } catch (error) {
46
+ if (error?.code === 'ENOENT') return null;
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ function assertNoSymlinkTree(path, fs) {
52
+ const status = lstatIfExists(path, fs);
53
+ if (!status) return;
54
+ if (status.isSymbolicLink()) throwSymlinkConflict();
55
+ if (!status.isDirectory()) return;
56
+ for (const entry of fs.readdirSync(path)) {
57
+ assertNoSymlinkTree(join(path, entry), fs);
58
+ }
59
+ }
60
+
61
+ function canonicalizePlan(lexicalAnchor, lexicalTarget, fs) {
62
+ const absoluteAnchor = resolve(lexicalAnchor);
63
+ const absoluteTarget = resolve(lexicalTarget);
64
+ const targetFromAnchor = relative(absoluteAnchor, absoluteTarget);
65
+ if (!isWithin(absoluteAnchor, absoluteTarget)) {
66
+ throw new CliError('Target path escapes its authorized root.', 'REPOSITORY_CONFLICT');
67
+ }
68
+
69
+ let existingAnchor = absoluteAnchor;
70
+ const missingSegments = [];
71
+ while (!fs.existsSync(existingAnchor)) {
72
+ const parent = dirname(existingAnchor);
73
+ if (parent === existingAnchor) {
74
+ throw new CliError('Could not establish an authorized target root.', 'REPOSITORY_CONFLICT');
75
+ }
76
+ missingSegments.unshift(basename(existingAnchor));
77
+ existingAnchor = parent;
78
+ }
79
+
80
+ try {
81
+ const canonicalBoundary = fs.realpathSync(existingAnchor);
82
+ const boundaryStatus = fs.lstatSync(canonicalBoundary);
83
+ if (boundaryStatus.isSymbolicLink() || !boundaryStatus.isDirectory()) {
84
+ throw new CliError('Authorized target root is not a directory.', 'REPOSITORY_CONFLICT');
85
+ }
86
+ const canonicalAnchor = join(canonicalBoundary, ...missingSegments);
87
+ return {
88
+ anchor: canonicalAnchor,
89
+ boundary: canonicalBoundary,
90
+ boundaryIdentity: Object.freeze({ dev: boundaryStatus.dev, ino: boundaryStatus.ino }),
91
+ target: resolve(canonicalAnchor, targetFromAnchor),
92
+ };
93
+ } catch (error) {
94
+ throw new CliError('Could not validate the authorized target root.', 'REPOSITORY_CONFLICT', {
95
+ cause: error,
96
+ });
97
+ }
98
+ }
99
+
100
+ export function createMutationGuard(lexicalAnchor, lexicalTarget, fs) {
101
+ const plan = canonicalizePlan(lexicalAnchor, lexicalTarget, fs);
102
+
103
+ const assertPath = (candidate) => {
104
+ const absoluteCandidate = resolve(candidate);
105
+ if (!isWithin(plan.target, absoluteCandidate)) {
106
+ throw new CliError('Mutation path escapes its authorized target.', 'REPOSITORY_CONFLICT');
107
+ }
108
+ const segments = relative(plan.boundary, absoluteCandidate).split(sep).filter(Boolean);
109
+ let current = plan.boundary;
110
+ let candidateIdentity = null;
111
+ try {
112
+ for (const segment of segments) {
113
+ current = join(current, segment);
114
+ const status = lstatIfExists(current, fs);
115
+ if (!status) continue;
116
+ if (status.isSymbolicLink()) throwSymlinkConflict();
117
+ const canonicalCurrent = fs.realpathSync(current);
118
+ if (!isWithin(plan.boundary, canonicalCurrent)) {
119
+ throw new CliError('Target path escapes its authorized root.', 'REPOSITORY_CONFLICT');
120
+ }
121
+ if (current === absoluteCandidate) {
122
+ candidateIdentity = Object.freeze({ dev: status.dev, ino: status.ino });
123
+ }
124
+ }
125
+ return candidateIdentity;
126
+ } catch (error) {
127
+ if (error instanceof CliError) throw error;
128
+ throw new CliError('Could not validate target path containment.', 'REPOSITORY_CONFLICT', {
129
+ cause: error,
130
+ });
131
+ }
132
+ };
133
+
134
+ return Object.freeze({
135
+ authorizationBoundary: plan.boundary,
136
+ authorizationBoundaryIdentity: plan.boundaryIdentity,
137
+ authorizationRoot: plan.anchor,
138
+ targetDir: plan.target,
139
+ assertPath,
140
+ });
141
+ }
142
+
143
+ function assertSourceTree(source, fs) {
144
+ try {
145
+ assertNoSymlinkTree(source, fs);
146
+ } catch (error) {
147
+ if (error instanceof CliError) {
148
+ throw new CliError('Distribution data contains an unsupported symbolic link.', 'MISSING_CONFIGURATION', {
149
+ cause: error,
150
+ });
151
+ }
152
+ throw new CliError('Could not validate distribution data.', 'MISSING_CONFIGURATION', {
153
+ cause: error,
154
+ });
155
+ }
156
+ }
157
+
158
+ function sameFileIdentity(left, right) {
159
+ return left.dev === right.dev && left.ino === right.ino;
160
+ }
161
+
162
+ // The resolved authorization boundary is the trusted root. Descendant
163
+ // mutations must remain synchronous and cwd-pinned so replacing their pathnames
164
+ // cannot redirect an in-flight operation; callers must control that root itself.
165
+ export function withPinnedTargetDirectory(directory, expectedIdentity, fs, operation) {
166
+ const originalCwd = process.cwd();
167
+ let changedDirectory = false;
168
+ let operationStarted = false;
169
+ try {
170
+ if (!expectedIdentity) {
171
+ throw new CliError('Target directory has no trusted identity.', 'REPOSITORY_CONFLICT');
172
+ }
173
+ process.chdir(directory);
174
+ changedDirectory = true;
175
+ const pinned = fs.statSync('.');
176
+ if (!sameFileIdentity(expectedIdentity, pinned)) {
177
+ throw new CliError('Target directory changed during validation.', 'REPOSITORY_CONFLICT');
178
+ }
179
+ operationStarted = true;
180
+ return operation();
181
+ } catch (error) {
182
+ if (operationStarted || error instanceof CliError) throw error;
183
+ throw new CliError('Could not pin the validated target directory.', 'REPOSITORY_CONFLICT', {
184
+ cause: error,
185
+ });
186
+ } finally {
187
+ if (changedDirectory) process.chdir(originalCwd);
188
+ }
189
+ }
190
+
191
+ export function ensureContainedDirectory(guard, fs) {
192
+ const segments = relative(guard.authorizationBoundary, guard.targetDir)
193
+ .split(sep)
194
+ .filter(Boolean);
195
+ if (!isWithin(guard.authorizationBoundary, guard.targetDir)) {
196
+ throw new CliError('Target path escapes its authorized root.', 'REPOSITORY_CONFLICT');
197
+ }
198
+
199
+ let targetIdentity = guard.authorizationBoundaryIdentity;
200
+ withPinnedTargetDirectory(
201
+ guard.authorizationBoundary,
202
+ guard.authorizationBoundaryIdentity,
203
+ fs,
204
+ () => {
205
+ for (const segment of segments) {
206
+ let status = lstatIfExists(segment, fs);
207
+ if (!status) {
208
+ fs.mkdirSync(segment);
209
+ status = fs.lstatSync(segment);
210
+ }
211
+ if (status.isSymbolicLink()) throwSymlinkConflict();
212
+ if (!status.isDirectory()) {
213
+ throw new CliError('Target path is not a directory.', 'REPOSITORY_CONFLICT');
214
+ }
215
+ const expected = Object.freeze({ dev: status.dev, ino: status.ino });
216
+ process.chdir(segment);
217
+ const pinned = fs.statSync('.');
218
+ if (!sameFileIdentity(expected, pinned)) {
219
+ throw new CliError('Target directory changed during validation.', 'REPOSITORY_CONFLICT');
220
+ }
221
+ targetIdentity = expected;
222
+ }
223
+ },
224
+ );
225
+ return targetIdentity;
226
+ }
227
+
228
+ function fileIdentity(status) {
229
+ return Object.freeze({ dev: status.dev, ino: status.ino });
230
+ }
231
+
232
+ export function copyFileIntoPinnedDirectory(source, destinationName, fs) {
233
+ const sourceStatus = fs.lstatSync(source);
234
+ if (sourceStatus.isSymbolicLink() || !sourceStatus.isFile()) {
235
+ throw new CliError('Distribution data contains an unsupported entry.', 'MISSING_CONFIGURATION');
236
+ }
237
+
238
+ const sourceData = fs.readFileSync(source);
239
+ const destinationStatus = lstatIfExists(destinationName, fs);
240
+ if (destinationStatus?.isSymbolicLink() || (destinationStatus && !destinationStatus.isFile())) {
241
+ throw new CliError('Managed destination path is not a regular file.', 'REPOSITORY_CONFLICT');
242
+ }
243
+
244
+ const expectedIdentity = destinationStatus ? fileIdentity(destinationStatus) : null;
245
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
246
+ const flags = destinationStatus
247
+ ? fs.constants.O_RDWR | noFollow
248
+ : fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow;
249
+
250
+ let descriptor;
251
+ let openedIdentity;
252
+ let originalData;
253
+ let originalMode;
254
+ let mutationStarted = false;
255
+
256
+ const writeAll = (data) => {
257
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
258
+ let offset = 0;
259
+ while (offset < buffer.byteLength) {
260
+ const written = fs.writeSync(
261
+ descriptor,
262
+ buffer,
263
+ offset,
264
+ buffer.byteLength - offset,
265
+ offset,
266
+ );
267
+ if (!Number.isInteger(written) || written <= 0) {
268
+ throw new Error('Managed destination write did not make progress');
269
+ }
270
+ offset += written;
271
+ }
272
+ };
273
+
274
+ try {
275
+ descriptor = fs.openSync(destinationName, flags, sourceStatus.mode & 0o777);
276
+ const openedStatus = fs.fstatSync(descriptor);
277
+ if (!openedStatus.isFile()) {
278
+ throw new CliError('Managed destination path is not a regular file.', 'REPOSITORY_CONFLICT');
279
+ }
280
+ openedIdentity = fileIdentity(openedStatus);
281
+ if (expectedIdentity && !sameFileIdentity(expectedIdentity, openedIdentity)) {
282
+ throw new CliError('Managed destination changed during validation.', 'REPOSITORY_CONFLICT');
283
+ }
284
+ const openedPathStatus = lstatIfExists(destinationName, fs);
285
+ if (
286
+ !openedPathStatus
287
+ || openedPathStatus.isSymbolicLink()
288
+ || !openedPathStatus.isFile()
289
+ || !sameFileIdentity(openedIdentity, openedPathStatus)
290
+ ) {
291
+ throw new CliError('Managed destination changed during validation.', 'REPOSITORY_CONFLICT');
292
+ }
293
+
294
+ if (destinationStatus) {
295
+ originalData = fs.readFileSync(descriptor);
296
+ originalMode = openedStatus.mode & 0o777;
297
+ }
298
+
299
+ mutationStarted = true;
300
+ fs.ftruncateSync(descriptor, 0);
301
+ writeAll(sourceData);
302
+ fs.fchmodSync(descriptor, sourceStatus.mode & 0o777);
303
+ fs.fsyncSync(descriptor);
304
+
305
+ const publishedStatus = lstatIfExists(destinationName, fs);
306
+ if (!publishedStatus || !sameFileIdentity(openedIdentity, publishedStatus)) {
307
+ throw new CliError('Managed destination changed during update.', 'REPOSITORY_CONFLICT');
308
+ }
309
+ } catch (error) {
310
+ if (descriptor !== undefined && destinationStatus && mutationStarted && originalData !== undefined) {
311
+ try {
312
+ fs.ftruncateSync(descriptor, 0);
313
+ writeAll(originalData);
314
+ fs.fchmodSync(descriptor, originalMode);
315
+ fs.fsyncSync(descriptor);
316
+ } catch {
317
+ // Best-effort restoration stays bound to the already verified descriptor.
318
+ }
319
+ }
320
+ throw error;
321
+ } finally {
322
+ if (descriptor !== undefined) fs.closeSync(descriptor);
323
+ }
324
+ }
325
+
326
+ function overlaySourceDirectory(sourceDirectory, fs) {
327
+ for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
328
+ const source = join(sourceDirectory, entry.name);
329
+ const sourceStatus = fs.lstatSync(source);
330
+ if (sourceStatus.isSymbolicLink()) {
331
+ throw new CliError('Distribution data contains an unsupported symbolic link.', 'MISSING_CONFIGURATION');
332
+ }
333
+ if (sourceStatus.isFile()) {
334
+ copyFileIntoPinnedDirectory(source, entry.name, fs);
335
+ continue;
336
+ }
337
+ if (!sourceStatus.isDirectory()) {
338
+ throw new CliError('Distribution data contains an unsupported entry.', 'MISSING_CONFIGURATION');
339
+ }
340
+
341
+ let destinationStatus = lstatIfExists(entry.name, fs);
342
+ if (!destinationStatus) {
343
+ fs.mkdirSync(entry.name, { mode: sourceStatus.mode & 0o777 });
344
+ destinationStatus = fs.lstatSync(entry.name);
345
+ }
346
+ if (destinationStatus.isSymbolicLink()) throwSymlinkConflict();
347
+ if (!destinationStatus.isDirectory()) {
348
+ throw new CliError('Managed destination path is not a directory.', 'REPOSITORY_CONFLICT');
349
+ }
350
+ withPinnedTargetDirectory(
351
+ entry.name,
352
+ fileIdentity(destinationStatus),
353
+ fs,
354
+ () => overlaySourceDirectory(source, fs),
355
+ );
356
+ }
357
+ }
358
+
359
+ function overlayManagedDirectory(source, destinationName, fs) {
360
+ const sourceStatus = fs.lstatSync(source);
361
+ if (sourceStatus.isSymbolicLink() || !sourceStatus.isDirectory()) {
362
+ throw new CliError('Distribution data contains an unsupported entry.', 'MISSING_CONFIGURATION');
363
+ }
364
+
365
+ let destinationStatus = lstatIfExists(destinationName, fs);
366
+ if (!destinationStatus) {
367
+ fs.mkdirSync(destinationName, { mode: sourceStatus.mode & 0o777 });
368
+ destinationStatus = fs.lstatSync(destinationName);
369
+ }
370
+ if (destinationStatus.isSymbolicLink()) throwSymlinkConflict();
371
+ if (!destinationStatus.isDirectory()) {
372
+ throw new CliError('Managed destination path is not a directory.', 'REPOSITORY_CONFLICT');
373
+ }
374
+ withPinnedTargetDirectory(
375
+ destinationName,
376
+ fileIdentity(destinationStatus),
377
+ fs,
378
+ () => overlaySourceDirectory(source, fs),
379
+ );
380
+ }
381
+
382
+ function buildGroupedChoices(skillNames, agentNames, separator) {
383
+ const agentSet = new Set(agentNames);
384
+ const remaining = new Set([...skillNames, ...agentNames]);
385
+ const choices = [];
386
+
387
+ for (const [label, members] of Object.entries(SKILL_CATEGORIES)) {
388
+ const inGroup = members.filter(member => remaining.has(member));
389
+ if (inGroup.length === 0) continue;
390
+ choices.push(separator(`── ${label} ──`));
391
+ for (const name of inGroup) {
392
+ choices.push({
393
+ name: agentSet.has(name) ? `${name} [agent]` : name,
394
+ value: name,
395
+ checked: true,
396
+ });
397
+ remaining.delete(name);
398
+ }
399
+ }
400
+
401
+ if (remaining.size > 0) {
402
+ choices.push(separator('── Other ──'));
403
+ for (const name of remaining) {
404
+ choices.push({
405
+ name: agentSet.has(name) ? `${name} [agent]` : name,
406
+ value: name,
407
+ checked: true,
408
+ });
409
+ }
410
+ }
411
+ return choices;
412
+ }
413
+
414
+ export function findProjectRoot(startDir, fs) {
415
+ let directory = resolve(startDir);
416
+ while (true) {
417
+ if (fs.existsSync(join(directory, '.git')) || fs.existsSync(join(directory, 'package.json'))) {
418
+ return directory;
419
+ }
420
+ const parent = join(directory, '..');
421
+ if (parent === directory) return null;
422
+ directory = parent;
423
+ }
424
+ }
425
+
426
+ function normalizeTarget(target) {
427
+ if (!target) return null;
428
+ if (target === 'both') return ['claude', 'codex'];
429
+ if (target === 'claude' || target === 'codex') return [target];
430
+ return null;
431
+ }
432
+
433
+ export function resolveTargetDir(platform, parsed, dependencies) {
434
+ const { cwd, env, fs, home } = dependencies;
435
+ if (parsed.flags.global) {
436
+ if (platform === 'claude') return join(home(), '.claude', 'skills');
437
+ const codexHome = env.CODEX_HOME ? resolve(cwd(), env.CODEX_HOME) : join(home(), '.codex');
438
+ return join(codexHome, 'skills');
439
+ }
440
+
441
+ const root = findProjectRoot(cwd(), fs);
442
+ if (!root) {
443
+ throw new CliError(
444
+ 'Could not find project root (no .git or package.json found in any parent directory).\nRun from inside a project, or use --global to install globally.',
445
+ 'INVALID_INPUT',
446
+ );
447
+ }
448
+
449
+ if (platform === 'claude') return join(root, '.claude', 'skills');
450
+ return join(root, '.codex', 'skills');
451
+ }
452
+
453
+ function resolveAuthorizationAnchor(platform, parsed, dependencies) {
454
+ const { cwd, env, fs, home } = dependencies;
455
+ if (!parsed.flags.global) {
456
+ const root = findProjectRoot(cwd(), fs);
457
+ if (!root) {
458
+ throw new CliError(
459
+ 'Could not find project root (no .git or package.json found in any parent directory).\nRun from inside a project, or use --global to install globally.',
460
+ 'INVALID_INPUT',
461
+ );
462
+ }
463
+ return root;
464
+ }
465
+ if (platform === 'codex' && env.CODEX_HOME) {
466
+ return dirname(resolve(cwd(), env.CODEX_HOME));
467
+ }
468
+ return home();
469
+ }
470
+
471
+ export function resolveTargetLocation(platform, parsed, dependencies) {
472
+ const lexicalTarget = resolveTargetDir(platform, parsed, dependencies);
473
+ const authorizationAnchor = resolveAuthorizationAnchor(platform, parsed, dependencies);
474
+ const guard = createMutationGuard(authorizationAnchor, lexicalTarget, dependencies.fs);
475
+ return {
476
+ target: platform,
477
+ dir: guard.targetDir,
478
+ displayDir: lexicalTarget,
479
+ authorizationAnchor: guard.authorizationRoot,
480
+ guard,
481
+ };
482
+ }
483
+
484
+ async function resolveTargets(action, parsed, dependencies) {
485
+ const { output, prompt } = dependencies;
486
+ if (parsed.flags.claude && parsed.flags.codex) return ['claude', 'codex'];
487
+ if (parsed.flags.claude) return ['claude'];
488
+ if (parsed.flags.codex) return ['codex'];
489
+
490
+ const target = parsed.flags.target;
491
+ const fromTargetFlag = typeof target === 'string'
492
+ ? normalizeTarget(target.toLowerCase())
493
+ : null;
494
+ if (target && !fromTargetFlag) {
495
+ throw new CliError("Invalid --target value. Use 'claude', 'codex', or 'both'.", 'INVALID_INPUT');
496
+ }
497
+ if (fromTargetFlag) return fromTargetFlag;
498
+
499
+ let selected;
500
+ try {
501
+ selected = await prompt({
502
+ message: `Select destinations to ${action}:`,
503
+ choices: [
504
+ { name: 'Claude Code', value: 'claude', checked: true },
505
+ { name: 'Codex', value: 'codex', checked: true },
506
+ ],
507
+ });
508
+ } catch {
509
+ output.log('\nCancelled.');
510
+ return null;
511
+ }
512
+
513
+ if (selected.length === 0) {
514
+ output.log('No destinations selected. Nothing to do.');
515
+ return null;
516
+ }
517
+ return selected;
518
+ }
519
+
520
+ function directoryNames(directory, fs, { required = false } = {}) {
521
+ if (!fs.existsSync(directory)) {
522
+ if (required) {
523
+ throw new CliError(
524
+ "dist/skills/ not found. Run 'npm run build' or reinstall the package.",
525
+ 'MISSING_CONFIGURATION',
526
+ );
527
+ }
528
+ return [];
529
+ }
530
+ try {
531
+ return fs.readdirSync(directory, { withFileTypes: true })
532
+ .filter(entry => entry.isDirectory())
533
+ .map(entry => entry.name);
534
+ } catch (error) {
535
+ throw new CliError('Could not read the installed distribution data.', 'MISSING_CONFIGURATION', {
536
+ cause: error,
537
+ });
538
+ }
539
+ }
540
+
541
+ export function getSkillNames(dependencies) {
542
+ return directoryNames(distributionPaths(dependencies.packageRoot).skills, dependencies.fs, { required: true });
543
+ }
544
+
545
+ export function getAgentNames(dependencies) {
546
+ return directoryNames(distributionPaths(dependencies.packageRoot).agents, dependencies.fs);
547
+ }
548
+
549
+ export function getMandatoryNames(dependencies) {
550
+ return directoryNames(distributionPaths(dependencies.packageRoot).mandatory, dependencies.fs);
551
+ }
552
+
553
+ async function readBoundedUpdateResponse(response, maxBytes) {
554
+ const declaredLength = Number(response.headers?.get?.('content-length'));
555
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
556
+ throw new Error('Update metadata exceeds the response limit');
557
+ }
558
+
559
+ if (response.body?.getReader) {
560
+ const reader = response.body.getReader();
561
+ const chunks = [];
562
+ let totalBytes = 0;
563
+ while (true) {
564
+ const { done, value } = await reader.read();
565
+ if (done) break;
566
+ const chunk = Buffer.from(value);
567
+ totalBytes += chunk.byteLength;
568
+ if (totalBytes > maxBytes) {
569
+ try { await reader.cancel(); } catch {}
570
+ throw new Error('Update metadata exceeds the response limit');
571
+ }
572
+ chunks.push(chunk);
573
+ }
574
+ return Buffer.concat(chunks, totalBytes).toString('utf8');
575
+ }
576
+
577
+ if (typeof response.text === 'function') {
578
+ const text = await response.text();
579
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) {
580
+ throw new Error('Update metadata exceeds the response limit');
581
+ }
582
+ return text;
583
+ }
584
+
585
+ if (typeof response.json === 'function') {
586
+ const text = JSON.stringify(await response.json());
587
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) {
588
+ throw new Error('Update metadata exceeds the response limit');
589
+ }
590
+ return text;
591
+ }
592
+
593
+ throw new Error('Update metadata has no readable body');
594
+ }
595
+
596
+ export async function checkForUpdate(dependencies) {
597
+ const { fetch, fs, output, packageRoot } = dependencies;
598
+ let metadata;
599
+ try { metadata = JSON.parse(fs.readFileSync(join(packageRoot, 'package.json'), 'utf8')); }
600
+ catch { return; }
601
+ // Private builds never query a public update channel.
602
+ if (metadata.private === true) return;
603
+ const timeoutMs = Number.isFinite(dependencies.updateCheckTimeoutMs)
604
+ ? Math.max(1, dependencies.updateCheckTimeoutMs)
605
+ : DEFAULT_UPDATE_CHECK_TIMEOUT_MS;
606
+ const maxResponseBytes = Number.isFinite(dependencies.maxUpdateResponseBytes)
607
+ ? Math.max(1, dependencies.maxUpdateResponseBytes)
608
+ : DEFAULT_MAX_UPDATE_RESPONSE_BYTES;
609
+ const controller = new AbortController();
610
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
611
+ try {
612
+ const response = await fetch(
613
+ `https://registry.npmjs.org/${encodeURIComponent(metadata.name ?? '@agilno-tech/rivet')}`,
614
+ { signal: controller.signal },
615
+ );
616
+ if (!response.ok) return;
617
+ const data = JSON.parse(await readBoundedUpdateResponse(response, maxResponseBytes));
618
+ const channel = metadata.publishConfig?.tag === 'alpha' ? 'alpha' : 'latest';
619
+ const latest = data['dist-tags']?.[channel];
620
+ const current = JSON.parse(fs.readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
621
+ if (latest && latest !== current) {
622
+ output.log(`\nUpdate available: ${current} → ${latest}`);
623
+ output.log(`Run: npm install -g @agilno-tech/rivet@${channel}`);
624
+ }
625
+ } catch {
626
+ // The install/uninstall operation must not depend on registry availability.
627
+ } finally {
628
+ clearTimeout(timeout);
629
+ }
630
+ }
631
+
632
+ export async function install(parsed, dependencies) {
633
+ if (parsed.flags['project-runtime']) {
634
+ const { projectRuntimeInstall } = await import('../install/project-runtime.js');
635
+ return projectRuntimeInstall(parsed, dependencies);
636
+ }
637
+ if (parsed.flags.minimal) {
638
+ const { managedInstall } = await import('../install/managed.js');
639
+ return managedInstall(parsed, dependencies);
640
+ }
641
+ const { fs, output, packageRoot, separator } = dependencies;
642
+ const paths = distributionPaths(packageRoot);
643
+ const mandatoryNames = getMandatoryNames(dependencies);
644
+ const skillNames = getSkillNames(dependencies);
645
+ const agentNames = getAgentNames(dependencies);
646
+ const agentSet = new Set(agentNames);
647
+
648
+ if (mandatoryNames.length > 0) {
649
+ output.log('\nMandatory skills (always installed — cannot be deselected):');
650
+ mandatoryNames.forEach(name => output.log(` ✔ ${name}`));
651
+ output.log('');
652
+ }
653
+
654
+ const targets = await resolveTargets('install', parsed, dependencies);
655
+ if (!targets) return EXIT_CODES.SUCCESS;
656
+ const targetDirs = targets.map(target => resolveTargetLocation(target, parsed, dependencies));
657
+
658
+ let selectedOptional;
659
+ if (parsed.flags.all) {
660
+ selectedOptional = [...skillNames, ...agentNames];
661
+ } else {
662
+ try {
663
+ selectedOptional = await dependencies.prompt({
664
+ message: 'Select optional skills to install:',
665
+ choices: buildGroupedChoices(skillNames, agentNames, separator),
666
+ });
667
+ } catch {
668
+ output.log('\nCancelled.');
669
+ return EXIT_CODES.SUCCESS;
670
+ }
671
+ }
672
+
673
+ for (const targetLocation of targetDirs) {
674
+ const { target, dir, guard } = targetLocation;
675
+ try {
676
+ guard.assertPath(dir);
677
+ targetLocation.identity = ensureContainedDirectory(guard, fs);
678
+ guard.assertPath(dir);
679
+ } catch (error) {
680
+ if (error instanceof CliError) throw error;
681
+ throw new CliError(`Could not create the ${target} target directory.`, 'REPOSITORY_CONFLICT', {
682
+ cause: error,
683
+ });
684
+ }
685
+ }
686
+
687
+ for (const { target, dir, guard, identity } of targetDirs) {
688
+ for (const name of mandatoryNames) {
689
+ const source = join(paths.mandatory, name);
690
+ const destination = join(dir, installedSkillName(name));
691
+ try {
692
+ assertSourceTree(source, fs);
693
+ guard.assertPath(destination);
694
+ withPinnedTargetDirectory(dir, identity, fs, () => {
695
+ overlayManagedDirectory(source, installedSkillName(name), fs);
696
+ });
697
+ guard.assertPath(destination);
698
+ } catch (error) {
699
+ if (error instanceof CliError) throw error;
700
+ throw new CliError(
701
+ `Failed to install mandatory skill '${name}' to ${target}.`,
702
+ 'REPOSITORY_CONFLICT',
703
+ { cause: error },
704
+ );
705
+ }
706
+ }
707
+ }
708
+
709
+ for (const { target, dir, guard, identity } of targetDirs) {
710
+ for (const name of selectedOptional) {
711
+ const sourceRoot = agentSet.has(name) ? paths.agents : paths.skills;
712
+ const source = join(sourceRoot, name);
713
+ const destination = join(dir, installedSkillName(name));
714
+ try {
715
+ assertSourceTree(source, fs);
716
+ guard.assertPath(destination);
717
+ withPinnedTargetDirectory(dir, identity, fs, () => {
718
+ overlayManagedDirectory(source, installedSkillName(name), fs);
719
+ });
720
+ guard.assertPath(destination);
721
+ } catch (error) {
722
+ if (error instanceof CliError) throw error;
723
+ throw new CliError(`Failed to install '${name}' to ${target}.`, 'REPOSITORY_CONFLICT', {
724
+ cause: error,
725
+ });
726
+ }
727
+ }
728
+ }
729
+
730
+ const selectedSkills = selectedOptional.filter(name => !agentSet.has(name));
731
+ const selectedAgents = selectedOptional.filter(name => agentSet.has(name));
732
+ const scope = parsed.flags.global ? 'globally' : 'in project';
733
+ const totalCount = mandatoryNames.length + selectedOptional.length;
734
+ output.log(`\nInstalled ${totalCount} skills ${scope} for: ${targets.join(', ')}`);
735
+ if (mandatoryNames.length > 0) {
736
+ output.log(` Mandatory (${mandatoryNames.length}):`);
737
+ mandatoryNames.forEach(name => output.log(` / ${installedSkillName(name)}`));
738
+ }
739
+ if (selectedSkills.length > 0) {
740
+ output.log(` Skills (${selectedSkills.length}):`);
741
+ selectedSkills.forEach(name => output.log(` / ${installedSkillName(name)}`));
742
+ }
743
+ if (selectedAgents.length > 0) {
744
+ output.log(` Agents (${selectedAgents.length}):`);
745
+ selectedAgents.forEach(name => output.log(` / ${installedSkillName(name)}`));
746
+ }
747
+ targetDirs.forEach(({ target, displayDir }) => output.log(` -> ${target}: ${displayDir}`));
748
+
749
+ const governanceSource = join(packageRoot, 'dist', 'governance');
750
+ if (fs.existsSync(governanceSource) && targets.includes('claude')) {
751
+ const claudeTarget = targetDirs.find(({ target }) => target === 'claude');
752
+ const claudeDir = join(claudeTarget.dir, '..');
753
+ const displayClaudeDir = join(claudeTarget.displayDir, '..');
754
+ const governanceGuard = createMutationGuard(
755
+ claudeTarget.authorizationAnchor,
756
+ claudeDir,
757
+ fs,
758
+ );
759
+ const safeClaudeDir = governanceGuard.targetDir;
760
+ let governanceIdentity;
761
+ try {
762
+ governanceGuard.assertPath(safeClaudeDir);
763
+ governanceIdentity = ensureContainedDirectory(governanceGuard, fs);
764
+ governanceGuard.assertPath(safeClaudeDir);
765
+ } catch (error) {
766
+ if (error instanceof CliError) throw error;
767
+ throw new CliError('Could not create the Claude governance directory.', 'REPOSITORY_CONFLICT', {
768
+ cause: error,
769
+ });
770
+ }
771
+ let files;
772
+ try {
773
+ files = fs.readdirSync(governanceSource).filter(file => file.endsWith('.md'));
774
+ } catch (error) {
775
+ throw new CliError('Could not read governance distribution data.', 'MISSING_CONFIGURATION', {
776
+ cause: error,
777
+ });
778
+ }
779
+ const copied = [];
780
+ for (const file of files) {
781
+ const source = join(governanceSource, file);
782
+ const destination = join(safeClaudeDir, file);
783
+ if (!fs.existsSync(destination)) {
784
+ try {
785
+ assertSourceTree(source, fs);
786
+ governanceGuard.assertPath(destination);
787
+ withPinnedTargetDirectory(safeClaudeDir, governanceIdentity, fs, () => {
788
+ const status = lstatIfExists(file, fs);
789
+ if (status?.isSymbolicLink()) throwSymlinkConflict();
790
+ if (status) {
791
+ throw new CliError('Governance destination changed during validation.', 'REPOSITORY_CONFLICT');
792
+ }
793
+ copyFileIntoPinnedDirectory(source, file, fs);
794
+ });
795
+ governanceGuard.assertPath(destination);
796
+ } catch (error) {
797
+ if (error instanceof CliError) throw error;
798
+ throw new CliError(`Failed to copy governance template '${file}'.`, 'REPOSITORY_CONFLICT', {
799
+ cause: error,
800
+ });
801
+ }
802
+ copied.push(file);
803
+ }
804
+ }
805
+ if (copied.length > 0) {
806
+ output.log(`\nCopied governance templates to ${displayClaudeDir}:`);
807
+ copied.forEach(file => output.log(` ${file}`));
808
+ output.log('Customize these files for your project.');
809
+ }
810
+ }
811
+
812
+ await checkForUpdate(dependencies);
813
+ return EXIT_CODES.SUCCESS;
814
+ }