@clossys/launcher 0.3.1 → 0.5.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 (290) hide show
  1. package/README.md +1373 -60
  2. package/contracts/conversation-contract.md +2 -2
  3. package/contracts/product-ci-workflow.yml +74 -0
  4. package/contracts/repository-inventory.json +53 -0
  5. package/dist/admission-fixture.d.ts +168 -0
  6. package/dist/admission-fixture.d.ts.map +1 -0
  7. package/dist/admission-fixture.js +467 -0
  8. package/dist/admission-fixture.js.map +1 -0
  9. package/dist/admission.d.ts +124 -0
  10. package/dist/admission.d.ts.map +1 -0
  11. package/dist/admission.js +804 -0
  12. package/dist/admission.js.map +1 -0
  13. package/dist/agents-guide.d.ts +9 -0
  14. package/dist/agents-guide.d.ts.map +1 -0
  15. package/dist/agents-guide.js +26 -0
  16. package/dist/agents-guide.js.map +1 -0
  17. package/dist/apply-command-options.check.d.ts +12 -0
  18. package/dist/apply-command-options.check.d.ts.map +1 -0
  19. package/dist/apply-command-options.check.js +20 -0
  20. package/dist/apply-command-options.check.js.map +1 -0
  21. package/dist/apply-plan-cli.d.ts +39 -1
  22. package/dist/apply-plan-cli.d.ts.map +1 -1
  23. package/dist/apply-plan-cli.js +432 -15
  24. package/dist/apply-plan-cli.js.map +1 -1
  25. package/dist/apply-plan.d.ts +46 -59
  26. package/dist/apply-plan.d.ts.map +1 -1
  27. package/dist/apply-plan.js +112 -97
  28. package/dist/apply-plan.js.map +1 -1
  29. package/dist/apply-step-fixture.d.ts +87 -0
  30. package/dist/apply-step-fixture.d.ts.map +1 -0
  31. package/dist/apply-step-fixture.js +199 -0
  32. package/dist/apply-step-fixture.js.map +1 -0
  33. package/dist/apply-store.d.ts +93 -0
  34. package/dist/apply-store.d.ts.map +1 -0
  35. package/dist/apply-store.js +625 -0
  36. package/dist/apply-store.js.map +1 -0
  37. package/dist/approval-sheet.d.ts +21 -0
  38. package/dist/approval-sheet.d.ts.map +1 -0
  39. package/dist/approval-sheet.js +163 -0
  40. package/dist/approval-sheet.js.map +1 -0
  41. package/dist/body-command.d.ts +42 -0
  42. package/dist/body-command.d.ts.map +1 -0
  43. package/dist/body-command.js +143 -0
  44. package/dist/body-command.js.map +1 -0
  45. package/dist/change-set-contract.d.ts +403 -0
  46. package/dist/change-set-contract.d.ts.map +1 -0
  47. package/dist/change-set-contract.js +781 -0
  48. package/dist/change-set-contract.js.map +1 -0
  49. package/dist/change-set-digest.d.ts +28 -0
  50. package/dist/change-set-digest.d.ts.map +1 -0
  51. package/dist/change-set-digest.js +65 -0
  52. package/dist/change-set-digest.js.map +1 -0
  53. package/dist/check-cli.d.ts.map +1 -1
  54. package/dist/check-cli.js +14 -3
  55. package/dist/check-cli.js.map +1 -1
  56. package/dist/cli.d.ts +17 -6
  57. package/dist/cli.d.ts.map +1 -1
  58. package/dist/cli.js +84 -23
  59. package/dist/cli.js.map +1 -1
  60. package/dist/core.d.ts +79 -22
  61. package/dist/core.d.ts.map +1 -1
  62. package/dist/core.js +843 -268
  63. package/dist/core.js.map +1 -1
  64. package/dist/dry-materialize.d.ts +63 -0
  65. package/dist/dry-materialize.d.ts.map +1 -0
  66. package/dist/dry-materialize.js +330 -0
  67. package/dist/dry-materialize.js.map +1 -0
  68. package/dist/existing-declaration-adoption.check.d.ts +2 -0
  69. package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
  70. package/dist/existing-declaration-adoption.check.js +10 -0
  71. package/dist/existing-declaration-adoption.check.js.map +1 -0
  72. package/dist/generated/contract-schema.generated.d.ts +97 -0
  73. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  74. package/dist/generated/contract-schema.generated.js +496 -0
  75. package/dist/generated/contract-schema.generated.js.map +1 -0
  76. package/dist/generated/package-scope.generated.d.ts +6 -0
  77. package/dist/generated/package-scope.generated.d.ts.map +1 -0
  78. package/dist/generated/package-scope.generated.js +10 -0
  79. package/dist/generated/package-scope.generated.js.map +1 -0
  80. package/dist/generated/plan-contracts.generated.d.ts +3 -0
  81. package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
  82. package/dist/generated/plan-contracts.generated.js +3101 -0
  83. package/dist/generated/plan-contracts.generated.js.map +1 -0
  84. package/dist/host.d.ts.map +1 -1
  85. package/dist/host.js +11 -0
  86. package/dist/host.js.map +1 -1
  87. package/dist/identity.d.ts +15 -0
  88. package/dist/identity.d.ts.map +1 -0
  89. package/dist/identity.js +48 -0
  90. package/dist/identity.js.map +1 -0
  91. package/dist/index.d.ts +35 -5
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +19 -2
  94. package/dist/index.js.map +1 -1
  95. package/dist/inventory-adoption.d.ts +24 -5
  96. package/dist/inventory-adoption.d.ts.map +1 -1
  97. package/dist/inventory-adoption.js +70 -25
  98. package/dist/inventory-adoption.js.map +1 -1
  99. package/dist/inventory-choice.d.ts +40 -0
  100. package/dist/inventory-choice.d.ts.map +1 -0
  101. package/dist/inventory-choice.js +156 -0
  102. package/dist/inventory-choice.js.map +1 -0
  103. package/dist/inventory-contract.d.ts +89 -0
  104. package/dist/inventory-contract.d.ts.map +1 -0
  105. package/dist/inventory-contract.js +121 -0
  106. package/dist/inventory-contract.js.map +1 -0
  107. package/dist/key-editor.d.ts +30 -0
  108. package/dist/key-editor.d.ts.map +1 -0
  109. package/dist/key-editor.js +445 -0
  110. package/dist/key-editor.js.map +1 -0
  111. package/dist/ledger-contract.d.ts +190 -0
  112. package/dist/ledger-contract.d.ts.map +1 -0
  113. package/dist/ledger-contract.js +555 -0
  114. package/dist/ledger-contract.js.map +1 -0
  115. package/dist/ledger-trust.d.ts +90 -0
  116. package/dist/ledger-trust.d.ts.map +1 -0
  117. package/dist/ledger-trust.js +203 -0
  118. package/dist/ledger-trust.js.map +1 -0
  119. package/dist/lockfile-invariants.d.ts +48 -0
  120. package/dist/lockfile-invariants.d.ts.map +1 -0
  121. package/dist/lockfile-invariants.js +375 -0
  122. package/dist/lockfile-invariants.js.map +1 -0
  123. package/dist/lockfile-readers.d.ts +72 -0
  124. package/dist/lockfile-readers.d.ts.map +1 -0
  125. package/dist/lockfile-readers.js +713 -0
  126. package/dist/lockfile-readers.js.map +1 -0
  127. package/dist/lockfile-regen.d.ts +106 -0
  128. package/dist/lockfile-regen.d.ts.map +1 -0
  129. package/dist/lockfile-regen.js +760 -0
  130. package/dist/lockfile-regen.js.map +1 -0
  131. package/dist/lockfile-tool-env.d.ts +29 -0
  132. package/dist/lockfile-tool-env.d.ts.map +1 -0
  133. package/dist/lockfile-tool-env.js +111 -0
  134. package/dist/lockfile-tool-env.js.map +1 -0
  135. package/dist/materialize.d.ts +113 -0
  136. package/dist/materialize.d.ts.map +1 -0
  137. package/dist/materialize.js +881 -0
  138. package/dist/materialize.js.map +1 -0
  139. package/dist/observe-repository.d.ts +90 -0
  140. package/dist/observe-repository.d.ts.map +1 -0
  141. package/dist/observe-repository.js +1367 -0
  142. package/dist/observe-repository.js.map +1 -0
  143. package/dist/plan-bundle-setup-fixture.d.ts +68 -0
  144. package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
  145. package/dist/plan-bundle-setup-fixture.js +167 -0
  146. package/dist/plan-bundle-setup-fixture.js.map +1 -0
  147. package/dist/plan-bundle.d.ts +256 -0
  148. package/dist/plan-bundle.d.ts.map +1 -0
  149. package/dist/plan-bundle.js +882 -0
  150. package/dist/plan-bundle.js.map +1 -0
  151. package/dist/plan-command.d.ts +29 -0
  152. package/dist/plan-command.d.ts.map +1 -0
  153. package/dist/plan-command.js +523 -0
  154. package/dist/plan-command.js.map +1 -0
  155. package/dist/plan-contract.d.ts +153 -0
  156. package/dist/plan-contract.d.ts.map +1 -0
  157. package/dist/plan-contract.js +61 -0
  158. package/dist/plan-contract.js.map +1 -0
  159. package/dist/plan-digest.d.ts +25 -0
  160. package/dist/plan-digest.d.ts.map +1 -0
  161. package/dist/plan-digest.js +106 -0
  162. package/dist/plan-digest.js.map +1 -0
  163. package/dist/plan-rules.d.ts +23 -0
  164. package/dist/plan-rules.d.ts.map +1 -0
  165. package/dist/plan-rules.js +177 -0
  166. package/dist/plan-rules.js.map +1 -0
  167. package/dist/planned-bundle.d.ts +20 -0
  168. package/dist/planned-bundle.d.ts.map +1 -0
  169. package/dist/planned-bundle.js +191 -0
  170. package/dist/planned-bundle.js.map +1 -0
  171. package/dist/product-repository.d.ts +4 -0
  172. package/dist/product-repository.d.ts.map +1 -1
  173. package/dist/product-repository.js +9 -1
  174. package/dist/product-repository.js.map +1 -1
  175. package/dist/provenance-gate.d.ts +48 -0
  176. package/dist/provenance-gate.d.ts.map +1 -0
  177. package/dist/provenance-gate.js +324 -0
  178. package/dist/provenance-gate.js.map +1 -0
  179. package/dist/pull-request-body.d.ts +45 -0
  180. package/dist/pull-request-body.d.ts.map +1 -0
  181. package/dist/pull-request-body.js +232 -0
  182. package/dist/pull-request-body.js.map +1 -0
  183. package/dist/registry-snapshot.d.ts +141 -0
  184. package/dist/registry-snapshot.d.ts.map +1 -0
  185. package/dist/registry-snapshot.js +483 -0
  186. package/dist/registry-snapshot.js.map +1 -0
  187. package/dist/release-age-edit.d.ts +52 -0
  188. package/dist/release-age-edit.d.ts.map +1 -0
  189. package/dist/release-age-edit.js +413 -0
  190. package/dist/release-age-edit.js.map +1 -0
  191. package/dist/root-entries.d.ts +36 -0
  192. package/dist/root-entries.d.ts.map +1 -0
  193. package/dist/root-entries.js +80 -0
  194. package/dist/root-entries.js.map +1 -0
  195. package/dist/setup-template-scripts.d.ts +36 -0
  196. package/dist/setup-template-scripts.d.ts.map +1 -0
  197. package/dist/setup-template-scripts.js +568 -0
  198. package/dist/setup-template-scripts.js.map +1 -0
  199. package/dist/setup-templates.d.ts +55 -0
  200. package/dist/setup-templates.d.ts.map +1 -0
  201. package/dist/setup-templates.js +438 -0
  202. package/dist/setup-templates.js.map +1 -0
  203. package/dist/skills.d.ts +34 -1
  204. package/dist/skills.d.ts.map +1 -1
  205. package/dist/skills.js +129 -17
  206. package/dist/skills.js.map +1 -1
  207. package/dist/status.d.ts +63 -0
  208. package/dist/status.d.ts.map +1 -0
  209. package/dist/status.js +539 -0
  210. package/dist/status.js.map +1 -0
  211. package/dist/types.d.ts +151 -13
  212. package/dist/types.d.ts.map +1 -1
  213. package/package.json +4 -4
  214. package/skeleton/README.md +14 -9
  215. package/skeleton/package.json +2 -1
  216. package/skill/SKILL.md +23 -7
  217. package/skill-catalogue/advisor/SKILL.md +59 -6
  218. package/skill-catalogue/architect/SKILL.md +2 -2
  219. package/skill-catalogue/bouncer/SKILL.md +2 -2
  220. package/skill-catalogue/builder/SKILL.md +2 -2
  221. package/skill-catalogue/butler/SKILL.md +2 -2
  222. package/skill-catalogue/controller/SKILL.md +2 -2
  223. package/skill-catalogue/customer/SKILL.md +2 -2
  224. package/skill-catalogue/designer/SKILL.md +4 -2
  225. package/skill-catalogue/giver/SKILL.md +2 -2
  226. package/skill-catalogue/influencer/SKILL.md +2 -2
  227. package/skill-catalogue/inspector/SKILL.md +2 -2
  228. package/skill-catalogue/integrator/SKILL.md +2 -2
  229. package/skill-catalogue/keeper/SKILL.md +2 -2
  230. package/skill-catalogue/launcher/SKILL.md +23 -7
  231. package/skill-catalogue/locksmith/SKILL.md +2 -2
  232. package/skill-catalogue/messenger/SKILL.md +2 -2
  233. package/skill-catalogue/observer/SKILL.md +2 -2
  234. package/skill-catalogue/publisher/SKILL.md +2 -2
  235. package/skill-catalogue/starter/SKILL.md +3 -2
  236. package/skill-catalogue/strategist/SKILL.md +12 -4
  237. package/skill-catalogue/writer/SKILL.md +2 -2
  238. package/src/admission-fixture.ts +585 -0
  239. package/src/admission.ts +819 -0
  240. package/src/agents-guide.ts +29 -0
  241. package/src/apply-command-options.check.ts +27 -0
  242. package/src/apply-plan-cli.ts +454 -14
  243. package/src/apply-plan.ts +112 -124
  244. package/src/apply-step-fixture.ts +236 -0
  245. package/src/apply-store.ts +584 -0
  246. package/src/approval-sheet.ts +170 -0
  247. package/src/body-command.ts +162 -0
  248. package/src/change-set-contract.ts +987 -0
  249. package/src/change-set-digest.ts +70 -0
  250. package/src/check-cli.ts +14 -3
  251. package/src/cli.ts +90 -22
  252. package/src/core.ts +973 -275
  253. package/src/dry-materialize.ts +353 -0
  254. package/src/existing-declaration-adoption.check.ts +12 -0
  255. package/src/generated/contract-schema.generated.ts +520 -0
  256. package/src/generated/package-scope.generated.ts +10 -0
  257. package/src/generated/plan-contracts.generated.ts +3101 -0
  258. package/src/host.ts +10 -0
  259. package/src/identity.ts +51 -0
  260. package/src/index.ts +74 -3
  261. package/src/inventory-adoption.ts +107 -29
  262. package/src/inventory-choice.ts +172 -0
  263. package/src/inventory-contract.ts +166 -0
  264. package/src/key-editor.ts +446 -0
  265. package/src/ledger-contract.ts +660 -0
  266. package/src/ledger-trust.ts +272 -0
  267. package/src/lockfile-invariants.ts +421 -0
  268. package/src/lockfile-readers.ts +749 -0
  269. package/src/lockfile-regen.ts +851 -0
  270. package/src/lockfile-tool-env.ts +131 -0
  271. package/src/materialize.ts +915 -0
  272. package/src/observe-repository.ts +1365 -0
  273. package/src/plan-bundle-setup-fixture.ts +200 -0
  274. package/src/plan-bundle.ts +1014 -0
  275. package/src/plan-command.ts +532 -0
  276. package/src/plan-contract.ts +179 -0
  277. package/src/plan-digest.ts +102 -0
  278. package/src/plan-rules.ts +188 -0
  279. package/src/planned-bundle.ts +211 -0
  280. package/src/product-repository.ts +10 -1
  281. package/src/provenance-gate.ts +352 -0
  282. package/src/pull-request-body.ts +261 -0
  283. package/src/registry-snapshot.ts +534 -0
  284. package/src/release-age-edit.ts +430 -0
  285. package/src/root-entries.ts +81 -0
  286. package/src/setup-template-scripts.ts +580 -0
  287. package/src/setup-templates.ts +479 -0
  288. package/src/skills.ts +161 -18
  289. package/src/status.ts +557 -0
  290. package/src/types.ts +148 -13
@@ -0,0 +1,580 @@
1
+ /**
2
+ * Pure renderers for the setup template acts' script and workflow files.
3
+ *
4
+ * Each renderer returns exact file bytes; path scope optionally takes the
5
+ * supported authoring agent provenance. With no input it preserves legacy
6
+ * bytes. Rendering is pure, so
7
+ * the text is a pure function of this module alone: no clock, no randomness,
8
+ * no filesystem, no environment. Everything that reads a clock or a
9
+ * repository does so inside the emitted script, when a workflow runs it, and
10
+ * never here.
11
+ *
12
+ * Files rendered here:
13
+ * - `.github/scripts/clossys-collect-adoption-snapshot.mjs`
14
+ * - `.github/workflows/clossys-adoption-evidence.yml`
15
+ * - `.github/workflows/clossys-path-scope.yml`
16
+ *
17
+ * The path-scope script is standalone, so it runs the same under a test as it
18
+ * does inside the workflow that embeds it.
19
+ */
20
+
21
+ /**
22
+ * The path patterns an apply pull request may own, in the change-set
23
+ * contract's order. The path-scope script embeds this list.
24
+ */
25
+ export const OWNED_PATH_PATTERNS: readonly string[] = [
26
+ "clossys/**",
27
+ ".agents/skills/clossys-*/**",
28
+ ".claude/skills/clossys-*",
29
+ ".cursor/skills/clossys-*",
30
+ "AGENTS.md",
31
+ "CLAUDE.md",
32
+ ".starter/request.json",
33
+ ".github/workflows/clossys-*",
34
+ ".github/scripts/clossys-*",
35
+ "package.json",
36
+ "package-lock.json",
37
+ "pnpm-lock.yaml",
38
+ "yarn.lock",
39
+ ".yarnrc.yml",
40
+ "pnpm-workspace.yaml",
41
+ "**/repository-profile.json",
42
+ "**/repository-declaration.json",
43
+ ];
44
+
45
+ const CHECKOUT = "actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0";
46
+ const UPLOAD_ARTIFACT = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7";
47
+ const HEREDOC_TERMINATOR = "CLOSSYS_PATH_SCOPE";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // The snapshot collector script
51
+ // ---------------------------------------------------------------------------
52
+
53
+ // The emitted scripts' module import lines are kept as quoted strings so that no
54
+ // line of this module's own source begins with an import statement.
55
+ const COLLECTOR_IMPORTS = [
56
+ 'import { createHash } from "node:crypto";',
57
+ 'import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";',
58
+ 'import { dirname, resolve, sep } from "node:path";',
59
+ ].join("\n");
60
+ const PATH_SCOPE_IMPORTS = 'import { execFileSync } from "node:child_process";';
61
+
62
+ const SNAPSHOT_COLLECTOR = String.raw`// Clossys adoption snapshot collector.
63
+ //
64
+ // This script is consumer-owned: it lives in this repository, and you review
65
+ // and change it like any other code here. It is uncredentialed. It reads no
66
+ // package or registry secret, installs nothing, and runs no package, Advisor
67
+ // or target command. The trusted decision job never reads or trusts what this
68
+ // script produces, because a pull request controls the workflow that runs it
69
+ // and can change this file.
70
+ //
71
+ // Usage: node clossys-collect-adoption-snapshot.mjs <request.json> <output-dir>
72
+ //
73
+ // From the request it takes snapshot.repository, evidence.assessment and
74
+ // evidence.targetInput. It copies exactly those two files, found relative to
75
+ // the working directory (the repository root), into <output-dir>, and writes
76
+ // <output-dir>/snapshot.json with the pull request facts from the environment
77
+ // (REPOSITORY, PR_NUMBER, BASE_SHA, HEAD_SHA, RUN_ID) and each file's size and
78
+ // SHA-256.
79
+ //
80
+ // It refuses, with exit code 2 and one line on stderr, a request or an
81
+ // environment value it cannot vouch for; an evidence path that is not a
82
+ // normalized relative path, is missing, is a symlink, is not a regular file, or
83
+ // is larger than 524288 bytes; and an output directory that already holds
84
+ // anything. A refusal leaves nothing half-written.
85
+ ${COLLECTOR_IMPORTS}
86
+
87
+ const NAME = "clossys-collect-adoption-snapshot";
88
+ const MAX_BYTES = 524288;
89
+ const COMMIT = /^[0-9a-f]{40}$/;
90
+
91
+ class Refusal extends Error {}
92
+
93
+ function refuse(message) {
94
+ throw new Refusal(message);
95
+ }
96
+
97
+ function show(text) {
98
+ return String(text).replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "?").slice(0, 200);
99
+ }
100
+
101
+ function env(name) {
102
+ const value = process.env[name];
103
+ return typeof value === "string" ? value : "";
104
+ }
105
+
106
+ function isRecord(value) {
107
+ return value !== null && typeof value === "object" && !Array.isArray(value);
108
+ }
109
+
110
+ function isNormalizedRelativePath(value) {
111
+ if (typeof value !== "string" || value.length === 0 || value.length > 240) return false;
112
+ if (value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value)) return false;
113
+ if (/[\u0000-\u001f\u007f]/.test(value)) return false;
114
+ return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
115
+ }
116
+
117
+ function sha256(data) {
118
+ return createHash("sha256").update(data).digest("hex");
119
+ }
120
+
121
+ function readBounded(path, label) {
122
+ let link;
123
+ try {
124
+ link = lstatSync(path);
125
+ } catch {
126
+ refuse(label + " is missing or unreadable");
127
+ }
128
+ if (link.isSymbolicLink() || !link.isFile()) refuse(label + " is not a regular non-symlink file");
129
+ if (link.size > MAX_BYTES) refuse(label + " is larger than " + MAX_BYTES + " bytes");
130
+ let fd;
131
+ try {
132
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
133
+ } catch {
134
+ refuse(label + " cannot be opened");
135
+ }
136
+ try {
137
+ const stat = fstatSync(fd);
138
+ if (!stat.isFile() || stat.size > MAX_BYTES) refuse(label + " is not a bounded regular file");
139
+ const content = readFileSync(fd);
140
+ if (content.length > MAX_BYTES) refuse(label + " is larger than " + MAX_BYTES + " bytes");
141
+ return content;
142
+ } finally {
143
+ closeSync(fd);
144
+ }
145
+ }
146
+
147
+ function readEvidence(root, path) {
148
+ const label = "evidence file " + show(path);
149
+ const candidate = resolve(root, path);
150
+ if (!candidate.startsWith(root + sep)) refuse(label + " is outside the working directory");
151
+ let real;
152
+ try {
153
+ real = realpathSync(candidate);
154
+ } catch {
155
+ refuse(label + " is missing or unreadable");
156
+ }
157
+ if (real !== candidate) refuse(label + " passes through a symbolic link");
158
+ return readBounded(candidate, label);
159
+ }
160
+
161
+ function readRequest(requestArgument) {
162
+ const content = readBounded(resolve(requestArgument), "request file");
163
+ let request;
164
+ try {
165
+ request = JSON.parse(content.toString("utf8"));
166
+ } catch {
167
+ refuse("the request is not valid JSON");
168
+ }
169
+ if (!isRecord(request) || !isRecord(request.snapshot) || typeof request.snapshot.repository !== "string" || request.snapshot.repository === "") {
170
+ refuse("the request has no snapshot.repository");
171
+ }
172
+ if (!isRecord(request.evidence)) refuse("the request has no evidence paths");
173
+ const assessment = request.evidence.assessment;
174
+ const targetInput = request.evidence.targetInput;
175
+ if (!isNormalizedRelativePath(assessment)) refuse("the request's evidence.assessment is not a normalized relative path");
176
+ if (!isNormalizedRelativePath(targetInput)) refuse("the request's evidence.targetInput is not a normalized relative path");
177
+ if (assessment === targetInput) refuse("the request names the same evidence path twice; the two must be distinct");
178
+ for (const path of [assessment, targetInput]) {
179
+ if (path.toLowerCase() === "snapshot.json") refuse("the request's evidence path " + show(path) + " collides with snapshot.json");
180
+ }
181
+ return { repository: request.snapshot.repository, paths: [assessment, targetInput] };
182
+ }
183
+
184
+ function readEnvironment(repository) {
185
+ if (env("REPOSITORY") !== repository) refuse("REPOSITORY does not equal the request's snapshot.repository");
186
+ const pullRequest = env("PR_NUMBER");
187
+ if (!/^[1-9][0-9]*$/.test(pullRequest) || !Number.isSafeInteger(Number(pullRequest))) refuse("PR_NUMBER is not a positive safe integer");
188
+ const baseSha = env("BASE_SHA");
189
+ if (!COMMIT.test(baseSha)) refuse("BASE_SHA is not a 40-character lowercase hex commit");
190
+ const headSha = env("HEAD_SHA");
191
+ if (!COMMIT.test(headSha)) refuse("HEAD_SHA is not a 40-character lowercase hex commit");
192
+ const runId = env("RUN_ID");
193
+ if (!/^[0-9]+$/.test(runId)) refuse("RUN_ID is not a non-empty run of digits");
194
+ return { pullRequestNumber: Number(pullRequest), baseSha, headSha, runId };
195
+ }
196
+
197
+ function checkOutputDirectory(out) {
198
+ let stat = null;
199
+ try {
200
+ stat = lstatSync(out);
201
+ } catch (error) {
202
+ if (!error || error.code !== "ENOENT") refuse("the output directory cannot be inspected");
203
+ }
204
+ if (stat === null) return;
205
+ if (stat.isSymbolicLink() || !stat.isDirectory()) refuse("the output path exists and is not a directory");
206
+ if (readdirSync(out).length > 0) refuse("the output directory already exists and is not empty");
207
+ }
208
+
209
+ function write(out, files, manifestText) {
210
+ let firstCreated;
211
+ try {
212
+ firstCreated = mkdirSync(out, { recursive: true });
213
+ for (const file of files) {
214
+ const target = resolve(out, file.path);
215
+ mkdirSync(dirname(target), { recursive: true });
216
+ writeFileSync(target, file.content, { flag: "wx" });
217
+ }
218
+ writeFileSync(resolve(out, "snapshot.json"), manifestText, { flag: "wx" });
219
+ } catch {
220
+ try {
221
+ if (firstCreated !== undefined) rmSync(firstCreated, { recursive: true, force: true });
222
+ else for (const name of readdirSync(out)) rmSync(resolve(out, name), { recursive: true, force: true });
223
+ } catch {
224
+ // Nothing more can be done; the refusal below still reports the failure.
225
+ }
226
+ refuse("the snapshot could not be written");
227
+ }
228
+ }
229
+
230
+ function main() {
231
+ const args = process.argv.slice(2);
232
+ if (args.length !== 2 || args[0] === "" || args[1] === "") refuse("usage: node clossys-collect-adoption-snapshot.mjs <request.json> <output-dir>");
233
+ const out = resolve(args[1]);
234
+ const request = readRequest(args[0]);
235
+ const facts = readEnvironment(request.repository);
236
+ checkOutputDirectory(out);
237
+
238
+ const root = realpathSync(process.cwd());
239
+ const files = request.paths.map((path) => ({ path, content: readEvidence(root, path) }));
240
+ files.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
241
+
242
+ const before = {
243
+ schemaVersion: 1,
244
+ provider: "github-actions",
245
+ eventName: "pull_request",
246
+ repository: request.repository,
247
+ pullRequestNumber: facts.pullRequestNumber,
248
+ baseSha: facts.baseSha,
249
+ headSha: facts.headSha,
250
+ workflowRunId: facts.runId,
251
+ artifactName: "adoption-snapshot-" + facts.runId,
252
+ };
253
+ const capturedAt = new Date().toISOString();
254
+ const listed = files.map((file) => ({ path: file.path, size: file.content.length, sha256: sha256(file.content) }));
255
+ const digest = sha256(Buffer.from(JSON.stringify({ ...before, capturedAt, files: listed }, null, 2), "utf8"));
256
+ const manifest = { ...before, digest, capturedAt, files: listed };
257
+
258
+ write(out, files, JSON.stringify(manifest, null, 2) + "\n");
259
+ console.log(NAME + ": wrote " + files.length + " evidence file(s) and snapshot.json");
260
+ return 0;
261
+ }
262
+
263
+ try {
264
+ process.exitCode = main();
265
+ } catch (error) {
266
+ console.error(NAME + ": " + (error instanceof Refusal ? show(error.message) : "unexpected failure"));
267
+ process.exitCode = 2;
268
+ }
269
+ `;
270
+
271
+ /** The bytes of `.github/scripts/clossys-collect-adoption-snapshot.mjs`. */
272
+ export function renderSnapshotCollector(): string {
273
+ return SNAPSHOT_COLLECTOR;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // The adoption evidence workflow
278
+ // ---------------------------------------------------------------------------
279
+
280
+ const ADOPTION_EVIDENCE_WORKFLOW = [
281
+ "# Clossys adoption evidence, written by Clossys Launcher.",
282
+ "#",
283
+ "# This is the uncredentialed half of the adoption check. It runs on",
284
+ "# pull_request, checks out the pull request head with no persisted credential,",
285
+ "# and uploads a small snapshot of two evidence files as an artifact. It holds no",
286
+ "# secret, installs nothing, and runs no package, Advisor or target command.",
287
+ "#",
288
+ "# The artifact this workflow uploads is NOT read by the trusted decision job.",
289
+ "# A pull request controls this workflow and can edit it, so what it produces is",
290
+ "# not evidence the decision trusts.",
291
+ "#",
292
+ "# The collector is a plain script in this repository:",
293
+ "# .github/scripts/clossys-collect-adoption-snapshot.mjs. It reads the two",
294
+ "# evidence paths named by .starter/request.json and copies exactly those files.",
295
+ "name: Clossys adoption evidence",
296
+ "",
297
+ "on:",
298
+ " pull_request:",
299
+ " types: [opened, synchronize, reopened]",
300
+ "",
301
+ "permissions:",
302
+ " contents: read",
303
+ "",
304
+ "jobs:",
305
+ " collect-adoption-evidence:",
306
+ " runs-on: ubuntu-latest",
307
+ " timeout-minutes: 10",
308
+ " steps:",
309
+ " - name: Check out the pull request head",
310
+ ` uses: ${CHECKOUT}`,
311
+ " with:",
312
+ " ref: ${{ github.event.pull_request.head.sha }}",
313
+ " persist-credentials: false",
314
+ "",
315
+ " # Values reach the script only through the environment, never through its",
316
+ " # text. The script runs no package, Advisor or target command.",
317
+ " - name: Collect the snapshot",
318
+ " env:",
319
+ " REPOSITORY: ${{ github.repository }}",
320
+ " PR_NUMBER: ${{ github.event.pull_request.number }}",
321
+ " BASE_SHA: ${{ github.event.pull_request.base.sha }}",
322
+ " HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
323
+ " RUN_ID: ${{ github.run_id }}",
324
+ " run: |",
325
+ " node .github/scripts/clossys-collect-adoption-snapshot.mjs \\",
326
+ " .starter/request.json \\",
327
+ ' "$RUNNER_TEMP/adoption-snapshot"',
328
+ "",
329
+ " - name: Upload the snapshot",
330
+ ` uses: ${UPLOAD_ARTIFACT}`,
331
+ " with:",
332
+ " name: adoption-snapshot-${{ github.run_id }}",
333
+ " path: ${{ runner.temp }}/adoption-snapshot",
334
+ " if-no-files-found: error",
335
+ " retention-days: 7",
336
+ "",
337
+ ].join("\n");
338
+
339
+ /** The bytes of `.github/workflows/clossys-adoption-evidence.yml`. */
340
+ export function renderAdoptionEvidenceWorkflow(): string {
341
+ return ADOPTION_EVIDENCE_WORKFLOW;
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // The path-scope script and workflow
346
+ // ---------------------------------------------------------------------------
347
+
348
+ const PATH_SCOPE_SCRIPT = String.raw`// Clossys path scope.
349
+ //
350
+ // Fails a Clossys apply pull request that changes a file Clossys does not own,
351
+ // or an owned file the pull request's own ledger does not name. It reads git
352
+ // only: git diff and git show, run without a shell. It never runs, loads or
353
+ // evaluates anything from the checked-out files.
354
+ //
355
+ // Inputs come from the environment only: HEAD_REF (the pull request head
356
+ // branch), BASE_SHA and HEAD_SHA (40 lowercase hex commits). Exit codes: 0
357
+ // passed (or not an apply pull request), 1 findings, 2 could not check. Every
358
+ // doubt is a 2.
359
+ ${PATH_SCOPE_IMPORTS}
360
+
361
+ const APPLY_PREFIX = "clossys/apply-";
362
+ const LEDGER_PATH = "clossys/.state/installed.json";
363
+ const EXEMPT = new Set([LEDGER_PATH, "package-lock.json", "pnpm-lock.yaml", "package.json"]);
364
+ const OWNED = ${JSON.stringify(OWNED_PATH_PATTERNS, null, 2)};
365
+ const MAX_PRINTED = 50;
366
+ const MAX_BUFFER = 64 * 1024 * 1024;
367
+ const COMMIT = /^[0-9a-f]{40}$/;
368
+ const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
369
+
370
+ // matcher:begin
371
+ const SAFE_PATH = /^(?:(?!\.\.?\/)[^/\\\u0000-\u001f]+\/)*(?!\.\.?$)[^/\\\u0000-\u001f]+$/u;
372
+
373
+ function isSafePath(path) {
374
+ return SAFE_PATH.test(path);
375
+ }
376
+
377
+ function isPattern(pattern) {
378
+ if (pattern === "**" || !isSafePath(pattern)) return false;
379
+ return pattern.split("/").every((segment) => segment === "**" || !segment.includes("**"));
380
+ }
381
+
382
+ function segmentMatches(segment, glob) {
383
+ const source = glob.split("*").map((part) => part.replace(/[.+?^$\{}()|[\]\\]/g, "\\$&")).join("[^/]*");
384
+ return new RegExp("^" + source + "$", "u").test(segment);
385
+ }
386
+
387
+ function segmentsMatch(path, pattern) {
388
+ if (pattern.length === 0) return path.length === 0;
389
+ const head = pattern[0];
390
+ const rest = pattern.slice(1);
391
+ if (head === "**") {
392
+ for (let skip = 0; skip <= path.length; skip += 1) if (segmentsMatch(path.slice(skip), rest)) return true;
393
+ return false;
394
+ }
395
+ return path.length > 0 && segmentMatches(path[0], head) && segmentsMatch(path.slice(1), rest);
396
+ }
397
+
398
+ function matches(path, pattern) {
399
+ if (!isSafePath(path) || !isPattern(pattern)) return false;
400
+ return segmentsMatch(path.split("/"), pattern.split("/"));
401
+ }
402
+ // matcher:end
403
+
404
+ class Refusal extends Error {}
405
+
406
+ function refuse(message) {
407
+ throw new Refusal(message);
408
+ }
409
+
410
+ function clean(text) {
411
+ return String(text).replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "?").slice(0, 300);
412
+ }
413
+
414
+ function isRecord(value) {
415
+ return value !== null && typeof value === "object" && !Array.isArray(value);
416
+ }
417
+
418
+ function gitOutput(args, what) {
419
+ try {
420
+ return execFileSync("git", args, { maxBuffer: MAX_BUFFER, stdio: ["ignore", "pipe", "ignore"] });
421
+ } catch {
422
+ return refuse(what + " failed");
423
+ }
424
+ }
425
+
426
+ function decode(buffer, what) {
427
+ try {
428
+ return DECODER.decode(buffer);
429
+ } catch {
430
+ return refuse(what + " is not valid UTF-8");
431
+ }
432
+ }
433
+
434
+ function changedPaths(baseSha, headSha) {
435
+ const output = gitOutput(
436
+ ["diff", "--name-only", "-z", "--no-renames", "--no-ext-diff", "--diff-filter=ACDMRTUXB", baseSha + "..." + headSha],
437
+ "git diff",
438
+ );
439
+ const parts = decode(output, "the changed path list").split("\u0000");
440
+ if (parts.pop() !== "") refuse("the changed path list is not NUL-terminated");
441
+ return parts;
442
+ }
443
+
444
+ function namedPaths(headSha) {
445
+ const text = decode(gitOutput(["show", "--no-textconv", headSha + ":" + LEDGER_PATH], "reading the head ledger"), "the head ledger");
446
+ let ledger;
447
+ try {
448
+ ledger = JSON.parse(text);
449
+ } catch {
450
+ return refuse("the head ledger is not valid JSON");
451
+ }
452
+ if (!isRecord(ledger) || !Array.isArray(ledger.files)) refuse("the head ledger has no files array");
453
+ const named = new Set();
454
+ for (const row of ledger.files) {
455
+ if (!isRecord(row) || typeof row.path !== "string") refuse("a row in the head ledger's files has no string path");
456
+ named.add(row.path);
457
+ }
458
+ for (const member of ["entries", "keys"]) {
459
+ if (!Object.hasOwn(ledger, member)) continue;
460
+ if (!Array.isArray(ledger[member])) refuse("the head ledger's " + member + " is not an array");
461
+ for (const row of ledger[member]) {
462
+ if (!isRecord(row) || typeof row.file !== "string") refuse("a row in the head ledger's " + member + " has no string file");
463
+ named.add(row.file);
464
+ }
465
+ }
466
+ return named;
467
+ }
468
+
469
+ function main() {
470
+ const headRef = process.env.HEAD_REF;
471
+ if (typeof headRef !== "string" || headRef === "") refuse("HEAD_REF is not set");
472
+ if (!headRef.startsWith(APPLY_PREFIX)) {
473
+ console.log("path-scope: not a Clossys apply pull request (the head branch does not start with " + APPLY_PREFIX + "); nothing to check");
474
+ return 0;
475
+ }
476
+ const baseSha = process.env.BASE_SHA;
477
+ const headSha = process.env.HEAD_SHA;
478
+ if (typeof baseSha !== "string" || !COMMIT.test(baseSha)) refuse("BASE_SHA is not a 40-character lowercase hex commit");
479
+ if (typeof headSha !== "string" || !COMMIT.test(headSha)) refuse("HEAD_SHA is not a 40-character lowercase hex commit");
480
+
481
+ const changed = changedPaths(baseSha, headSha);
482
+ const named = namedPaths(headSha);
483
+ const findings = [];
484
+ for (const path of changed) {
485
+ if (!OWNED.some((pattern) => matches(path, pattern))) findings.push("outside-owned-scope: " + clean(path));
486
+ if (!EXEMPT.has(path) && !named.has(path)) findings.push("not-named-by-ledger: " + clean(path));
487
+ }
488
+ if (findings.length === 0) {
489
+ console.log("path-scope: " + changed.length + " path(s) checked");
490
+ return 0;
491
+ }
492
+ for (const finding of findings.slice(0, MAX_PRINTED)) console.error(finding);
493
+ const shown = Math.min(findings.length, MAX_PRINTED);
494
+ console.error("path-scope: " + findings.length + " finding(s) in " + changed.length + " changed path(s)" + (shown < findings.length ? "; first " + shown + " shown" : ""));
495
+ return 1;
496
+ }
497
+
498
+ try {
499
+ process.exitCode = main();
500
+ } catch (error) {
501
+ console.error("path-scope: refused: " + (error instanceof Refusal ? error.message : "unexpected failure"));
502
+ process.exitCode = 2;
503
+ }
504
+ `;
505
+
506
+ /**
507
+ * The standalone path-scope script (its text ends in one LF). The
508
+ * path-scope workflow embeds it; a test runs it with `node` directly.
509
+ */
510
+ export function renderPathScopeScript(agentProvenance: "codex" | "claude" | "cursor" | undefined = undefined): string {
511
+ if (agentProvenance === undefined) return PATH_SCOPE_SCRIPT;
512
+ if (!["codex", "claude", "cursor"].includes(agentProvenance)) throw new TypeError("unsupported agent provenance");
513
+ // Keep legacy bytes unchanged. Explicit provenance opts into the expanded
514
+ // branch recognizer; every supported namespace still runs the full checks.
515
+ return PATH_SCOPE_SCRIPT
516
+ .replace('const APPLY_PREFIX = "clossys/apply-";', 'const APPLY_PREFIX = "a supported apply branch";\nconst APPLY_BRANCH = /^(clossys|codex|claude|cursor)\\/apply-[0-9a-f]{12}$/;')
517
+ .replace('if (!headRef.startsWith(APPLY_PREFIX)) {', 'if (!APPLY_BRANCH.test(headRef)) {\n if (headRef.includes("/apply")) refuse("unsupported or malformed apply branch");');
518
+ }
519
+
520
+ const PATH_SCOPE_WORKFLOW_HEAD = [
521
+ "# Clossys path scope, written by Clossys Launcher.",
522
+ "#",
523
+ "# This applies to pull requests whose head branch starts with clossys/apply-.",
524
+ "# For those it lists every file the pull request changes, deletions included,",
525
+ "# and fails unless each one matches a path Clossys may own and, apart from the",
526
+ "# ledger, package.json and the lockfiles, is named by the pull request's own",
527
+ "# clossys/.state/installed.json. Any other pull request passes with a notice.",
528
+ "# That decision is made inside the script, not by a job condition, so this job",
529
+ "# and its check always run.",
530
+ "#",
531
+ "# This workflow runs in the pull request's own context, so a pull request can",
532
+ "# edit it. It catches an agent's mistakes, not a hostile author. The trusted",
533
+ "# admission job is the separate workflow_run workflow, which runs from the",
534
+ "# protected base.",
535
+ "#",
536
+ "# The script only runs git diff and git show. It never runs, loads or evaluates",
537
+ "# anything from the checked-out files, and the branch name reaches it only",
538
+ "# through the environment.",
539
+ "name: Clossys path scope",
540
+ "",
541
+ "on:",
542
+ " pull_request:",
543
+ " types: [opened, synchronize, reopened]",
544
+ "",
545
+ "permissions:",
546
+ " contents: read",
547
+ "",
548
+ "jobs:",
549
+ " path-scope:",
550
+ " runs-on: ubuntu-latest",
551
+ " timeout-minutes: 10",
552
+ " steps:",
553
+ " - name: Check out the pull request head",
554
+ ` uses: ${CHECKOUT}`,
555
+ " with:",
556
+ " ref: ${{ github.event.pull_request.head.sha }}",
557
+ " fetch-depth: 0",
558
+ " persist-credentials: false",
559
+ "",
560
+ " # The branch name and both commits reach the script only through the",
561
+ " # environment, never through its text.",
562
+ " - name: Check the changed paths",
563
+ " env:",
564
+ " HEAD_REF: ${{ github.event.pull_request.head.ref }}",
565
+ " BASE_SHA: ${{ github.event.pull_request.base.sha }}",
566
+ " HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
567
+ " run: |",
568
+ ` node --input-type=module - <<'${HEREDOC_TERMINATOR}'`,
569
+ ];
570
+
571
+ const RUN_BODY_INDENT = " ";
572
+
573
+ /** The bytes of `.github/workflows/clossys-path-scope.yml`: the script embedded in a quoted heredoc. */
574
+ export function renderPathScopeWorkflow(agentProvenance: "codex" | "claude" | "cursor" | undefined = undefined): string {
575
+ const body = renderPathScopeScript(agentProvenance).slice(0, -1)
576
+ .split("\n")
577
+ .map((line) => (line === "" ? "" : `${RUN_BODY_INDENT}${line}`));
578
+ const head = agentProvenance === undefined ? PATH_SCOPE_WORKFLOW_HEAD : PATH_SCOPE_WORKFLOW_HEAD.map((line) => line.replace("head branch starts with clossys/apply-.", "head branch is a legacy or supported agent apply branch."));
579
+ return [...head, ...body, `${RUN_BODY_INDENT}${HEREDOC_TERMINATOR}`, ""].join("\n");
580
+ }