@clossys/launcher 0.3.0 → 0.4.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 (286) hide show
  1. package/README.md +1328 -60
  2. package/contracts/conversation-contract.md +2 -1
  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 +453 -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 +799 -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 +157 -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 +381 -0
  46. package/dist/change-set-contract.d.ts.map +1 -0
  47. package/dist/change-set-contract.js +738 -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/generated/contract-schema.generated.d.ts +97 -0
  69. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  70. package/dist/generated/contract-schema.generated.js +496 -0
  71. package/dist/generated/contract-schema.generated.js.map +1 -0
  72. package/dist/generated/package-scope.generated.d.ts +6 -0
  73. package/dist/generated/package-scope.generated.d.ts.map +1 -0
  74. package/dist/generated/package-scope.generated.js +10 -0
  75. package/dist/generated/package-scope.generated.js.map +1 -0
  76. package/dist/generated/plan-contracts.generated.d.ts +3 -0
  77. package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
  78. package/dist/generated/plan-contracts.generated.js +2840 -0
  79. package/dist/generated/plan-contracts.generated.js.map +1 -0
  80. package/dist/host.d.ts.map +1 -1
  81. package/dist/host.js +11 -0
  82. package/dist/host.js.map +1 -1
  83. package/dist/identity.d.ts +15 -0
  84. package/dist/identity.d.ts.map +1 -0
  85. package/dist/identity.js +48 -0
  86. package/dist/identity.js.map +1 -0
  87. package/dist/index.d.ts +34 -5
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +19 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/inventory-adoption.d.ts +24 -5
  92. package/dist/inventory-adoption.d.ts.map +1 -1
  93. package/dist/inventory-adoption.js +70 -25
  94. package/dist/inventory-adoption.js.map +1 -1
  95. package/dist/inventory-choice.d.ts +40 -0
  96. package/dist/inventory-choice.d.ts.map +1 -0
  97. package/dist/inventory-choice.js +156 -0
  98. package/dist/inventory-choice.js.map +1 -0
  99. package/dist/inventory-contract.d.ts +89 -0
  100. package/dist/inventory-contract.d.ts.map +1 -0
  101. package/dist/inventory-contract.js +121 -0
  102. package/dist/inventory-contract.js.map +1 -0
  103. package/dist/key-editor.d.ts +30 -0
  104. package/dist/key-editor.d.ts.map +1 -0
  105. package/dist/key-editor.js +445 -0
  106. package/dist/key-editor.js.map +1 -0
  107. package/dist/ledger-contract.d.ts +187 -0
  108. package/dist/ledger-contract.d.ts.map +1 -0
  109. package/dist/ledger-contract.js +532 -0
  110. package/dist/ledger-contract.js.map +1 -0
  111. package/dist/ledger-trust.d.ts +90 -0
  112. package/dist/ledger-trust.d.ts.map +1 -0
  113. package/dist/ledger-trust.js +198 -0
  114. package/dist/ledger-trust.js.map +1 -0
  115. package/dist/lockfile-invariants.d.ts +48 -0
  116. package/dist/lockfile-invariants.d.ts.map +1 -0
  117. package/dist/lockfile-invariants.js +375 -0
  118. package/dist/lockfile-invariants.js.map +1 -0
  119. package/dist/lockfile-readers.d.ts +72 -0
  120. package/dist/lockfile-readers.d.ts.map +1 -0
  121. package/dist/lockfile-readers.js +713 -0
  122. package/dist/lockfile-readers.js.map +1 -0
  123. package/dist/lockfile-regen.d.ts +106 -0
  124. package/dist/lockfile-regen.d.ts.map +1 -0
  125. package/dist/lockfile-regen.js +760 -0
  126. package/dist/lockfile-regen.js.map +1 -0
  127. package/dist/lockfile-tool-env.d.ts +29 -0
  128. package/dist/lockfile-tool-env.d.ts.map +1 -0
  129. package/dist/lockfile-tool-env.js +111 -0
  130. package/dist/lockfile-tool-env.js.map +1 -0
  131. package/dist/materialize.d.ts +113 -0
  132. package/dist/materialize.d.ts.map +1 -0
  133. package/dist/materialize.js +840 -0
  134. package/dist/materialize.js.map +1 -0
  135. package/dist/observe-repository.d.ts +90 -0
  136. package/dist/observe-repository.d.ts.map +1 -0
  137. package/dist/observe-repository.js +1367 -0
  138. package/dist/observe-repository.js.map +1 -0
  139. package/dist/plan-bundle-setup-fixture.d.ts +68 -0
  140. package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
  141. package/dist/plan-bundle-setup-fixture.js +167 -0
  142. package/dist/plan-bundle-setup-fixture.js.map +1 -0
  143. package/dist/plan-bundle.d.ts +250 -0
  144. package/dist/plan-bundle.d.ts.map +1 -0
  145. package/dist/plan-bundle.js +827 -0
  146. package/dist/plan-bundle.js.map +1 -0
  147. package/dist/plan-command.d.ts +29 -0
  148. package/dist/plan-command.d.ts.map +1 -0
  149. package/dist/plan-command.js +493 -0
  150. package/dist/plan-command.js.map +1 -0
  151. package/dist/plan-contract.d.ts +153 -0
  152. package/dist/plan-contract.d.ts.map +1 -0
  153. package/dist/plan-contract.js +61 -0
  154. package/dist/plan-contract.js.map +1 -0
  155. package/dist/plan-digest.d.ts +25 -0
  156. package/dist/plan-digest.d.ts.map +1 -0
  157. package/dist/plan-digest.js +106 -0
  158. package/dist/plan-digest.js.map +1 -0
  159. package/dist/plan-rules.d.ts +23 -0
  160. package/dist/plan-rules.d.ts.map +1 -0
  161. package/dist/plan-rules.js +177 -0
  162. package/dist/plan-rules.js.map +1 -0
  163. package/dist/planned-bundle.d.ts +20 -0
  164. package/dist/planned-bundle.d.ts.map +1 -0
  165. package/dist/planned-bundle.js +191 -0
  166. package/dist/planned-bundle.js.map +1 -0
  167. package/dist/product-repository.d.ts +4 -0
  168. package/dist/product-repository.d.ts.map +1 -1
  169. package/dist/product-repository.js +9 -1
  170. package/dist/product-repository.js.map +1 -1
  171. package/dist/provenance-gate.d.ts +48 -0
  172. package/dist/provenance-gate.d.ts.map +1 -0
  173. package/dist/provenance-gate.js +324 -0
  174. package/dist/provenance-gate.js.map +1 -0
  175. package/dist/pull-request-body.d.ts +45 -0
  176. package/dist/pull-request-body.d.ts.map +1 -0
  177. package/dist/pull-request-body.js +232 -0
  178. package/dist/pull-request-body.js.map +1 -0
  179. package/dist/registry-snapshot.d.ts +141 -0
  180. package/dist/registry-snapshot.d.ts.map +1 -0
  181. package/dist/registry-snapshot.js +483 -0
  182. package/dist/registry-snapshot.js.map +1 -0
  183. package/dist/release-age-edit.d.ts +52 -0
  184. package/dist/release-age-edit.d.ts.map +1 -0
  185. package/dist/release-age-edit.js +413 -0
  186. package/dist/release-age-edit.js.map +1 -0
  187. package/dist/root-entries.d.ts +36 -0
  188. package/dist/root-entries.d.ts.map +1 -0
  189. package/dist/root-entries.js +80 -0
  190. package/dist/root-entries.js.map +1 -0
  191. package/dist/setup-template-scripts.d.ts +34 -0
  192. package/dist/setup-template-scripts.d.ts.map +1 -0
  193. package/dist/setup-template-scripts.js +557 -0
  194. package/dist/setup-template-scripts.js.map +1 -0
  195. package/dist/setup-templates.d.ts +54 -0
  196. package/dist/setup-templates.d.ts.map +1 -0
  197. package/dist/setup-templates.js +427 -0
  198. package/dist/setup-templates.js.map +1 -0
  199. package/dist/skills.d.ts +34 -1
  200. package/dist/skills.d.ts.map +1 -1
  201. package/dist/skills.js +129 -17
  202. package/dist/skills.js.map +1 -1
  203. package/dist/status.d.ts +63 -0
  204. package/dist/status.d.ts.map +1 -0
  205. package/dist/status.js +539 -0
  206. package/dist/status.js.map +1 -0
  207. package/dist/types.d.ts +151 -13
  208. package/dist/types.d.ts.map +1 -1
  209. package/package.json +4 -5
  210. package/skeleton/README.md +14 -9
  211. package/skeleton/package.json +2 -1
  212. package/skill/SKILL.md +20 -16
  213. package/skill-catalogue/advisor/SKILL.md +100 -11
  214. package/skill-catalogue/architect/SKILL.md +2 -13
  215. package/skill-catalogue/bouncer/SKILL.md +2 -13
  216. package/skill-catalogue/builder/SKILL.md +2 -13
  217. package/skill-catalogue/butler/SKILL.md +2 -13
  218. package/skill-catalogue/controller/SKILL.md +2 -13
  219. package/skill-catalogue/customer/SKILL.md +4 -13
  220. package/skill-catalogue/designer/SKILL.md +9 -14
  221. package/skill-catalogue/giver/SKILL.md +2 -13
  222. package/skill-catalogue/influencer/SKILL.md +2 -13
  223. package/skill-catalogue/inspector/SKILL.md +2 -13
  224. package/skill-catalogue/integrator/SKILL.md +2 -13
  225. package/skill-catalogue/keeper/SKILL.md +2 -13
  226. package/skill-catalogue/launcher/SKILL.md +20 -16
  227. package/skill-catalogue/locksmith/SKILL.md +2 -13
  228. package/skill-catalogue/messenger/SKILL.md +2 -13
  229. package/skill-catalogue/observer/SKILL.md +2 -13
  230. package/skill-catalogue/publisher/SKILL.md +11 -17
  231. package/skill-catalogue/starter/SKILL.md +3 -13
  232. package/skill-catalogue/strategist/SKILL.md +14 -17
  233. package/skill-catalogue/writer/SKILL.md +7 -14
  234. package/src/admission-fixture.ts +572 -0
  235. package/src/admission.ts +816 -0
  236. package/src/agents-guide.ts +29 -0
  237. package/src/apply-command-options.check.ts +27 -0
  238. package/src/apply-plan-cli.ts +454 -14
  239. package/src/apply-plan.ts +112 -124
  240. package/src/apply-step-fixture.ts +236 -0
  241. package/src/apply-store.ts +584 -0
  242. package/src/approval-sheet.ts +164 -0
  243. package/src/body-command.ts +162 -0
  244. package/src/change-set-contract.ts +937 -0
  245. package/src/change-set-digest.ts +70 -0
  246. package/src/check-cli.ts +14 -3
  247. package/src/cli.ts +90 -22
  248. package/src/core.ts +973 -275
  249. package/src/dry-materialize.ts +353 -0
  250. package/src/generated/contract-schema.generated.ts +520 -0
  251. package/src/generated/package-scope.generated.ts +10 -0
  252. package/src/generated/plan-contracts.generated.ts +2840 -0
  253. package/src/host.ts +10 -0
  254. package/src/identity.ts +51 -0
  255. package/src/index.ts +72 -3
  256. package/src/inventory-adoption.ts +107 -29
  257. package/src/inventory-choice.ts +172 -0
  258. package/src/inventory-contract.ts +166 -0
  259. package/src/key-editor.ts +446 -0
  260. package/src/ledger-contract.ts +637 -0
  261. package/src/ledger-trust.ts +267 -0
  262. package/src/lockfile-invariants.ts +421 -0
  263. package/src/lockfile-readers.ts +749 -0
  264. package/src/lockfile-regen.ts +851 -0
  265. package/src/lockfile-tool-env.ts +131 -0
  266. package/src/materialize.ts +886 -0
  267. package/src/observe-repository.ts +1365 -0
  268. package/src/plan-bundle-setup-fixture.ts +200 -0
  269. package/src/plan-bundle.ts +964 -0
  270. package/src/plan-command.ts +509 -0
  271. package/src/plan-contract.ts +179 -0
  272. package/src/plan-digest.ts +102 -0
  273. package/src/plan-rules.ts +188 -0
  274. package/src/planned-bundle.ts +211 -0
  275. package/src/product-repository.ts +10 -1
  276. package/src/provenance-gate.ts +352 -0
  277. package/src/pull-request-body.ts +261 -0
  278. package/src/registry-snapshot.ts +534 -0
  279. package/src/release-age-edit.ts +430 -0
  280. package/src/root-entries.ts +81 -0
  281. package/src/setup-template-scripts.ts +571 -0
  282. package/src/setup-templates.ts +471 -0
  283. package/src/skills.ts +161 -18
  284. package/src/status.ts +557 -0
  285. package/src/types.ts +148 -13
  286. package/CHANGELOG.md +0 -131
@@ -0,0 +1,2840 @@
1
+ // AUTO-GENERATED by this package's build-time packer (issues #1475, #1178).
2
+ // Do not edit by hand -- edits are overwritten on the next `npm run build`.
3
+ // Source of truth: the shared plan, brief and inventory contracts, the
4
+ // registry snapshot contract, and the repository change-set, apply-bundle
5
+ // and installed-state ledger contracts, in this package's source repository
6
+ // (not shipped in this package). The plan, brief and inventory contracts
7
+ // here are the same data every package that validates a plan, a brief or an
8
+ // inventory packs.
9
+ /** The shared plan, brief, inventory and registry snapshot contracts, and the change-set, bundle and ledger contracts, keyed by their file name in docs/contracts/. */
10
+ export const PLAN_CONTRACTS = {
11
+ "advisor-plan.json": {
12
+ "$schema": "http://json-schema.org/draft-07/schema#",
13
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/advisor-plan.json",
14
+ "title": "Advisor plan",
15
+ "description": "Issue #1175 and #1475: the plan record at clossys/advisor/plan.json that Advisor writes and renders into its STATUS document, that Launcher reads before it applies an approved plan, and that Writer reads when it resolves copy a delegate approved for production (issue #1586). packages/advisor/src/status.ts's AdvisorPlan type is the shape's owner; this file is the one definition @clossys/advisor, @clossys/launcher and @clossys/writer validate against (each package packs it at build time, so none depends on another). Every object is closed: a key this contract does not declare is refused, never ignored, because an unknown key is either a typo or a field some other reader would silently drop. A later field is added here first, in the same change as its reader. Every time is a real calendar time, asserted field by field through `format` (see definitions.dateTime and definitions.date), never by shape alone, so each one parses and can be ordered. The canonical plan digest, which an approval binds, is defined in docs/contracts/advisor-plan-digest.md; its fixture corpus is docs/contracts/advisor-plan-digest.fixture.json. CODE RULES (issue #1178). The keywords above cannot relate one field to another, so the rules below are defined here, once, and checked in code by each of those packages after the schema passes; a plan that breaks one is invalid exactly as a plan that breaks the schema is, and so has no digest. Their shared corpus is docs/contracts/advisor-plan-rules.fixture.json, and each of them is tested against every plan case in it. A refusal names the rule and the position of the field at fault (for example staffing[1].repository), never its value. R1: no two staffing entries name the same repository; repository ids compare case-insensitively, as the repository inventory compares them. R2: when staffing is present, every staffing[].roles entry is one of mandate.roles, and every mandate.roles entry that is not a hub-only role (definitions.hubOnlyRoles) appears in at least one staffing entry. R3: every packages[].repository is spelled exactly as some staffing[].repository. R4: no two packages share a planItem. R5: no two packages share both a repository (compared case-insensitively) and a name; when R1 and R3 both hold (staffing ids are distinct case-insensitively, and every package's repository is spelled exactly as one of them), the case-insensitive comparison changes nothing, and it keeps R5 true on its own when either does not. R6: resolution is present exactly when packages is present. R7: no two kits share an id. R8: no role appears twice in one staffing entry's roles. R9: no role appears twice in mandate.roles. R10: a repository has at most one pin-starter act (repositories compared case-insensitively), and a pin-starter act's placement is devDependencies. R11: no staffing[].roles entry is a hub-only role (definitions.hubOnlyRoles). A hub-only role may be in mandate.roles and works from the hub, but it is never staffed in a repository, so no package act installs its package there; Advisor's package resolution also refuses a staffed hub-only role (hub-only-package), but R11 refuses such a plan first, because resolution reads only a valid plan. So a plan whose mandate.roles are all hub-only roles does no work in any product repository: it has no staffing (an empty staffing is refused by minItems, and any entry would break R2 or R11), and so it has no packages. R12: every packages[].planItem is exactly that act's repository, a colon and its name, in the same letter case. So a planItem carries no plan text, and it is the key the repository change set (C16) and the installed-state ledger (L10) use. APPROVAL BINDING. An approval binds bytes only through decisions[].subjectDigest. The latest decision is found by `at` as described under `decisions`; it approves something only when it has chosen \"approved\" and carries a subjectDigest, and the subject it approves is that digest, which names the exact change the approver was shown. When several decisions share the latest instant, they approve only if every one of them has chosen \"approved\" with the same subjectDigest; when any decision time does not parse, nothing is approved. An approval with no subjectDigest binds nothing. FREEZE. From an approving decision until every repository it covers has been applied, or a new plan replaces it, Advisor changes no field the plan digest covers. Recording the approval appends a decision and may update asOf, and the digest excludes both, so the digest at approval equals the digest at apply. DELEGATED COPY APPROVAL (issue #1586). delegatedCopyApproval declares that copy a delegate approved is accepted on production. It grants that only through an approval that binds this plan: @clossys/writer accepts it only when the plan it is given validates (schema and code rules), declares delegatedCopyApproval, and has a latest decision, found as APPROVAL BINDING describes, that has chosen \"approved\" with a subjectDigest equal to this plan's own canonical digest. When scopes is present, only an entry whose id is inside one of those namespaces is covered. The digest covers delegatedCopyApproval, so adding it, removing it or changing its scopes after an approval leaves the plan unapproved for this purpose until a new approval names the new digest. An approval whose subjectDigest names anything other than this plan's digest, such as another plan or an apply bundle, grants nothing here.",
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": [
19
+ "schemaVersion",
20
+ "asOf",
21
+ "mandate",
22
+ "whereWeAre",
23
+ "recommendedNext",
24
+ "decisions",
25
+ "blockers"
26
+ ],
27
+ "properties": {
28
+ "schemaVersion": {
29
+ "const": 1
30
+ },
31
+ "asOf": {
32
+ "$ref": "#/definitions/dateTime",
33
+ "description": "When this plan record was last written. Excluded from the canonical plan digest."
34
+ },
35
+ "mandate": {
36
+ "$ref": "#/definitions/mandate"
37
+ },
38
+ "whereWeAre": {
39
+ "type": "array",
40
+ "description": "A few plain-language status lines.",
41
+ "items": {
42
+ "$ref": "#/definitions/nonBlankString"
43
+ }
44
+ },
45
+ "recommendedNext": {
46
+ "description": "The one pending step for the whole plan, or null once nothing is pending.",
47
+ "oneOf": [
48
+ {
49
+ "type": "null"
50
+ },
51
+ {
52
+ "$ref": "#/definitions/recommendedNext"
53
+ }
54
+ ]
55
+ },
56
+ "decisions": {
57
+ "type": "array",
58
+ "description": "What was recommended, what was chosen, when, and by whom. The latest decision is the entry with the latest `at`, never the last one in the array; it approves the plan only when it has chosen \"approved\", and it binds an approval to bytes only through its subjectDigest (see APPROVAL BINDING in this contract's description). Append a decision; never rewrite an earlier one. Excluded from the canonical plan digest, because an approval is itself recorded here.",
59
+ "items": {
60
+ "$ref": "#/definitions/decision"
61
+ }
62
+ },
63
+ "blockers": {
64
+ "type": "array",
65
+ "items": {
66
+ "$ref": "#/definitions/blocker"
67
+ }
68
+ },
69
+ "kits": {
70
+ "type": "array",
71
+ "minItems": 1,
72
+ "description": "Optional. The kits Advisor recommends for this plan. Code rule R7.",
73
+ "items": {
74
+ "$ref": "#/definitions/kit"
75
+ }
76
+ },
77
+ "staffing": {
78
+ "type": "array",
79
+ "minItems": 1,
80
+ "description": "Optional. Which roles work in which repository: one entry per repository, named by its repository inventory id, never a URL or a path. Code rules R1, R2, R8 and R11.",
81
+ "items": {
82
+ "$ref": "#/definitions/staffingEntry"
83
+ }
84
+ },
85
+ "packages": {
86
+ "type": "array",
87
+ "minItems": 1,
88
+ "description": "Optional. The exact package acts this plan authorizes, each one version and one integrity value, never a range or a tag. Code rules R3, R4, R5, R6 and R10.",
89
+ "items": {
90
+ "$ref": "#/definitions/packageAct"
91
+ }
92
+ },
93
+ "resolution": {
94
+ "$ref": "#/definitions/resolution",
95
+ "description": "Optional. Where the exact versions in packages came from. Code rule R6."
96
+ },
97
+ "delegatedCopyApproval": {
98
+ "$ref": "#/definitions/delegatedCopyApproval",
99
+ "description": "Optional (issue #1586). Declares that copy a delegate approved is accepted on production, in every copy entry-id namespace or only in scopes. @clossys/writer reads it only through an approval that binds this plan: see DELEGATED COPY APPROVAL in this contract's description. The plan digest covers it, so FREEZE applies to it."
100
+ }
101
+ },
102
+ "definitions": {
103
+ "nonBlankString": {
104
+ "title": "a string with at least one non-whitespace character",
105
+ "type": "string",
106
+ "pattern": "\\S"
107
+ },
108
+ "dateTime": {
109
+ "title": "a real ISO 8601 date-time with a time zone, such as 2026-09-24T12:00:00Z",
110
+ "description": "format date-time, asserted field by field: YYYY-MM-DDThh:mm, optional :ss and fraction, then Z or +hh:mm/-hh:mm. Month 01-12; a day that exists in that month of that year (leap years included); hours 00-23; minutes and seconds 00-59 (no leap second); offset hours 00-23 and minutes 00-59; and Date.parse must give a finite time. So 2026-02-30, 2026-09-24T24:30:00Z and +24:00 are refused, and every accepted time can be ordered.",
111
+ "type": "string",
112
+ "format": "date-time"
113
+ },
114
+ "date": {
115
+ "title": "a real ISO 8601 date such as 2026-09-24",
116
+ "description": "format date, asserted field by field: YYYY-MM-DD with month 01-12 and a day that exists in that month of that year (leap years included), and a finite Date.parse.",
117
+ "type": "string",
118
+ "format": "date"
119
+ },
120
+ "dateOrDateTime": {
121
+ "title": "a real ISO 8601 date such as 2026-09-24, or a date-time with a time zone such as 2026-09-24T12:00:00Z",
122
+ "oneOf": [
123
+ {
124
+ "$ref": "#/definitions/date"
125
+ },
126
+ {
127
+ "$ref": "#/definitions/dateTime"
128
+ }
129
+ ]
130
+ },
131
+ "mandate": {
132
+ "type": "object",
133
+ "additionalProperties": false,
134
+ "required": [
135
+ "problem",
136
+ "primaryProblemId",
137
+ "roles"
138
+ ],
139
+ "properties": {
140
+ "problem": {
141
+ "$ref": "#/definitions/nonBlankString",
142
+ "description": "The client's problem, in their own words."
143
+ },
144
+ "primaryProblemId": {
145
+ "$ref": "#/definitions/nonBlankString"
146
+ },
147
+ "roles": {
148
+ "type": "array",
149
+ "items": {
150
+ "$ref": "#/definitions/nonBlankString"
151
+ }
152
+ }
153
+ }
154
+ },
155
+ "recommendedNext": {
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "required": [
159
+ "action",
160
+ "owner"
161
+ ],
162
+ "properties": {
163
+ "action": {
164
+ "$ref": "#/definitions/nonBlankString"
165
+ },
166
+ "owner": {
167
+ "$ref": "#/definitions/nonBlankString"
168
+ },
169
+ "due": {
170
+ "$ref": "#/definitions/dateOrDateTime",
171
+ "description": "Optional."
172
+ }
173
+ }
174
+ },
175
+ "decision": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": [
179
+ "at",
180
+ "recommended",
181
+ "chosen",
182
+ "by"
183
+ ],
184
+ "properties": {
185
+ "at": {
186
+ "$ref": "#/definitions/dateTime"
187
+ },
188
+ "recommended": {
189
+ "$ref": "#/definitions/nonBlankString"
190
+ },
191
+ "chosen": {
192
+ "$ref": "#/definitions/nonBlankString"
193
+ },
194
+ "by": {
195
+ "$ref": "#/definitions/nonBlankString"
196
+ },
197
+ "subjectDigest": {
198
+ "$ref": "#/definitions/sha256Digest",
199
+ "description": "Optional. On an approving decision, the digest of the exact change the approver was shown. An approval without it binds nothing (see APPROVAL BINDING in this contract's description)."
200
+ }
201
+ }
202
+ },
203
+ "sha256Digest": {
204
+ "title": "sha256: followed by 64 lowercase hexadecimal digits",
205
+ "type": "string",
206
+ "pattern": "^sha256:[0-9a-f]{64}$"
207
+ },
208
+ "sha512Integrity": {
209
+ "title": "one sha512 integrity value: sha512- followed by the canonical base64 of 64 bytes (88 characters ending in ==)",
210
+ "description": "Canonical base64 only: the last character before == carries two bits of data and four zero bits, so it is one of A, Q, g or w. Any other character there would decode to the same bytes as one of those, so a second spelling of one value is refused.",
211
+ "type": "string",
212
+ "pattern": "^sha512-[A-Za-z0-9+/]{85}[AQgw]==$"
213
+ },
214
+ "exactVersion": {
215
+ "title": "an exact release version such as 1.2.3, with no range, tag, prerelease or build suffix, and at most 16 digits in each part",
216
+ "description": "Each part is 0 or has no leading zero, and has at most 16 digits, the longest numeric part semver accepts.",
217
+ "type": "string",
218
+ "pattern": "^(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})$"
219
+ },
220
+ "repositoryId": {
221
+ "title": "a repository inventory id: a bare repository name, or owner/name",
222
+ "description": "The repository inventory's id rule (docs/contracts/repository-inventory.json) as one pattern: an optional GitHub owner (letters, digits and single inner hyphens, at most 39 characters) and a slash, then a repository name of letters, digits, '.', '_' and '-' that is not exactly '.' or '..'. No whitespace, no empty segment, at most one slash.",
223
+ "type": "string",
224
+ "pattern": "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/)?(?!\\.\\.?$)[A-Za-z0-9._-]+$"
225
+ },
226
+ "packageName": {
227
+ "title": "a scoped package name in lowercase, such as @scope/name, of at most 214 characters",
228
+ "description": "npm's limit is 214 characters, asserted by the lookahead because the checker has no maxLength. Neither the scope nor the name may start with '.', '_' or '-'.",
229
+ "type": "string",
230
+ "pattern": "^(?=.{1,214}$)@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$"
231
+ },
232
+ "kit": {
233
+ "type": "object",
234
+ "additionalProperties": false,
235
+ "required": [
236
+ "id",
237
+ "source",
238
+ "verdict"
239
+ ],
240
+ "properties": {
241
+ "id": {
242
+ "$ref": "#/definitions/nonBlankString",
243
+ "description": "The kit's id: a curated preset's id, or the id Advisor gave a composed kit."
244
+ },
245
+ "source": {
246
+ "enum": [
247
+ "preset",
248
+ "composed"
249
+ ]
250
+ },
251
+ "verdict": {
252
+ "enum": [
253
+ "recommended"
254
+ ],
255
+ "description": "One value for now. A later verdict widens this list, and a reader that predates it refuses the new value rather than guessing its meaning."
256
+ }
257
+ }
258
+ },
259
+ "staffingEntry": {
260
+ "type": "object",
261
+ "additionalProperties": false,
262
+ "required": [
263
+ "repository",
264
+ "roles"
265
+ ],
266
+ "properties": {
267
+ "repository": {
268
+ "$ref": "#/definitions/repositoryId"
269
+ },
270
+ "roles": {
271
+ "type": "array",
272
+ "minItems": 1,
273
+ "description": "Role directories (short names) staffed in this repository, in the order the brief lists them.",
274
+ "items": {
275
+ "$ref": "#/definitions/nonBlankString"
276
+ }
277
+ }
278
+ }
279
+ },
280
+ "hubOnlyRoles": {
281
+ "title": "the roles whose package lives in the engagement hub only",
282
+ "description": "Data, not a schema that any field uses: the list code rules R2 and R11 read, so that every package that validates a plan reads the one list. The apply-approved-plan RFC (issue #1178) places each role here in the hub: its package is pinned exactly once, in the hub, and a product repository runs its commands through npx at the hub's exact version, pinning nothing. Advisor is placed there by decision D24 and Integrator by decision D23; Launcher's hub skeleton and resume pin both Advisor and Integrator, exactly, in the hub's devDependencies. Such a role is never staffed in a repository (R11).",
283
+ "const": [
284
+ "advisor",
285
+ "integrator"
286
+ ]
287
+ },
288
+ "packageAct": {
289
+ "type": "object",
290
+ "additionalProperties": false,
291
+ "required": [
292
+ "planItem",
293
+ "repository",
294
+ "act",
295
+ "name",
296
+ "version",
297
+ "integrity",
298
+ "placement"
299
+ ],
300
+ "properties": {
301
+ "planItem": {
302
+ "$ref": "#/definitions/nonBlankString",
303
+ "description": "The act's repository, a colon and its name, exactly (R12); unique in the plan (R4)."
304
+ },
305
+ "repository": {
306
+ "$ref": "#/definitions/repositoryId",
307
+ "description": "One of staffing[].repository, spelled exactly the same (R3)."
308
+ },
309
+ "act": {
310
+ "enum": [
311
+ "install",
312
+ "pin-starter"
313
+ ],
314
+ "description": "install adds a role's package; pin-starter pins the package that checks this repository's pull requests, at most once per repository and always as a devDependency (R10). A later act widens this list, and a reader that predates it refuses the new value."
315
+ },
316
+ "name": {
317
+ "$ref": "#/definitions/packageName"
318
+ },
319
+ "version": {
320
+ "$ref": "#/definitions/exactVersion"
321
+ },
322
+ "integrity": {
323
+ "$ref": "#/definitions/sha512Integrity"
324
+ },
325
+ "placement": {
326
+ "enum": [
327
+ "dependencies",
328
+ "devDependencies"
329
+ ]
330
+ }
331
+ }
332
+ },
333
+ "resolution": {
334
+ "type": "object",
335
+ "additionalProperties": false,
336
+ "required": [
337
+ "snapshotDigest"
338
+ ],
339
+ "properties": {
340
+ "snapshotDigest": {
341
+ "$ref": "#/definitions/sha256Digest",
342
+ "description": "The digest of the registry snapshot the exact versions and integrity values in packages were read from."
343
+ }
344
+ }
345
+ },
346
+ "delegatedCopyApproval": {
347
+ "type": "object",
348
+ "additionalProperties": false,
349
+ "required": [
350
+ "target"
351
+ ],
352
+ "properties": {
353
+ "target": {
354
+ "enum": [
355
+ "production"
356
+ ],
357
+ "description": "The resolution target on which a delegate's approval is accepted. One value for now: a delegate's approval already resolves on preview with no declaration. A later target widens this list, and a reader that predates it refuses the new value rather than guessing its meaning."
358
+ },
359
+ "scopes": {
360
+ "type": "array",
361
+ "minItems": 1,
362
+ "description": "Optional. The copy entry-id namespaces this declaration covers: an entry is covered when its id equals an item or starts with the item and a dot, so site.home covers site.home and site.home.title but not site.homepage.title. Absent, it covers every entry. An empty list is refused (omit the member instead). A repeated item is redundant, not refused.",
363
+ "items": {
364
+ "$ref": "#/definitions/copyDelegateScope"
365
+ }
366
+ }
367
+ }
368
+ },
369
+ "copyDelegateScope": {
370
+ "title": "a copy entry-id namespace such as home or site.home: dot-separated segments of lowercase letters and digits with single inner hyphens, and no wildcard",
371
+ "description": "The same rule as @clossys/writer's delegate scope item: one or more segments separated by single dots, each one or more lowercase ASCII letters or digits, with single hyphens only between them.",
372
+ "type": "string",
373
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*(\\.[a-z0-9]+(-[a-z0-9]+)*)*$"
374
+ },
375
+ "blockerKind": {
376
+ "description": "The five blocker kinds of the Controller loop record (docs/contracts/loop.json), in their fixed order.",
377
+ "enum": [
378
+ "missing-input",
379
+ "missing-authority",
380
+ "failing-evidence",
381
+ "unavailable-environment",
382
+ "contradiction"
383
+ ]
384
+ },
385
+ "blocker": {
386
+ "type": "object",
387
+ "description": "One capability at rest with exactly one next action. Field for field the same shape as the Controller loop record's Blocker.",
388
+ "additionalProperties": false,
389
+ "required": [
390
+ "capabilityId",
391
+ "kind",
392
+ "owner",
393
+ "nextAction",
394
+ "since"
395
+ ],
396
+ "properties": {
397
+ "capabilityId": {
398
+ "$ref": "#/definitions/nonBlankString"
399
+ },
400
+ "kind": {
401
+ "$ref": "#/definitions/blockerKind"
402
+ },
403
+ "owner": {
404
+ "$ref": "#/definitions/nonBlankString"
405
+ },
406
+ "nextAction": {
407
+ "type": "object",
408
+ "additionalProperties": false,
409
+ "required": [
410
+ "who",
411
+ "how",
412
+ "byWhen"
413
+ ],
414
+ "properties": {
415
+ "who": {
416
+ "$ref": "#/definitions/nonBlankString"
417
+ },
418
+ "how": {
419
+ "$ref": "#/definitions/nonBlankString"
420
+ },
421
+ "byWhen": {
422
+ "$ref": "#/definitions/dateOrDateTime"
423
+ }
424
+ }
425
+ },
426
+ "since": {
427
+ "$ref": "#/definitions/dateTime",
428
+ "description": "When the blocker was recorded."
429
+ }
430
+ }
431
+ }
432
+ }
433
+ },
434
+ "engagement-brief.json": {
435
+ "$schema": "http://json-schema.org/draft-07/schema#",
436
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/engagement-brief.json",
437
+ "title": "Engagement brief",
438
+ "description": "Issue #1176 (owner redirect, 2026-09-22): the kit output shape a composed or proposed kit reduces to -- the client's problem, which roles are staffed and why, what the client gets, and how the roles hand off to each other. Matches packages/advisor/src/engagement-brief.ts's EngagementBrief type and its toEngagementBrief() transform. Issue #1475: this file, with engagement-context.json for `context`, is the one definition @clossys/advisor (validateEngagementBrief()), @clossys/launcher, and @clossys/strategist validate a brief against; each package packs it at build time. Launcher writes this shape to clossys/brief.json in each staffed repository (issues #1175, #1178), and refuses a brief that does not validate, including a key this contract does not declare. Strategist (issue #1173) only reads an already-written clossys/brief.json, to seed its own audience intake from the brief's context snapshot when the whole brief validates; it never writes one. Advisor does not write files. PER-REPOSITORY PROJECTION (issue #1178), owned here so every reader agrees. Advisor writes only the hub brief (clossys/advisor/brief.json), which has no staffedHere. The one producer of a per-repository brief is Launcher's apply planner, planApplyBundle(), which derives each one mechanically from the hub brief and the plan, as set out here, and records the digest of its bytes in that repository's change set. The planner writes no file: no step writes a projected brief into a repository yet, and the only brief writer today is the legacy brief-only path described at the end of this section, which applies none of this projection. For a repository whose inventory id is R: (1) its staffing entry is the plan's staffing entry whose repository is spelled exactly as R, the same exact match R3 uses; a repository with no such entry gets no brief. (2) staffedHere is that entry's roles, in the order the plan lists them. (3) problem is replaced by exactly the text of definitions.publicProblemPlaceholder unless the repository's observed visibility is exactly private; an unknown visibility counts as not private. (4) Every other member is copied from the hub brief unchanged. (5) The brief is written as UTF-8 JSON with two-space indentation and one trailing newline, its members in this order: schemaVersion, problem, roles, sequence, deliverables, staffedHere, context (context only when the hub brief has one); each roles entry as role, why, goal (metric, direction), inputsFrom, outputsTo; and context as schemaVersion, fields, each field as id, state, value (value only when known). The legacy brief-only path (launcher-apply-plan --plan --brief --repo, applyEngagementBrief()) is not this projection: it does not know a repository's visibility and commits the brief it is given unchanged, problem included, whatever the visibility. The apply step will supersede it once built. CODE RULES, checked in code by both packages after the schema passes and proved by the shared corpus docs/contracts/advisor-plan-rules.fixture.json (a refusal names the rule and position, never a value): B1: every staffedHere entry is one of roles[].role. B2: no role appears twice in staffedHere.",
439
+ "type": "object",
440
+ "additionalProperties": false,
441
+ "required": [
442
+ "schemaVersion",
443
+ "problem",
444
+ "roles",
445
+ "sequence",
446
+ "deliverables"
447
+ ],
448
+ "properties": {
449
+ "schemaVersion": {
450
+ "const": 1
451
+ },
452
+ "problem": {
453
+ "$ref": "#/definitions/nonBlankString",
454
+ "description": "The client's problem, in their own words."
455
+ },
456
+ "roles": {
457
+ "type": "array",
458
+ "minItems": 1,
459
+ "items": {
460
+ "$ref": "#/definitions/role"
461
+ }
462
+ },
463
+ "sequence": {
464
+ "type": "array",
465
+ "description": "Role directories in handoff order: a producer role always precedes the consumer whose need it satisfies.",
466
+ "items": {
467
+ "type": "string",
468
+ "minLength": 1
469
+ }
470
+ },
471
+ "deliverables": {
472
+ "type": "array",
473
+ "description": "What the client gets, one line per staffed role, grounded in that role's own boundary.owns from docs/contracts/role-loop-archetypes.json -- never invented copy.",
474
+ "items": {
475
+ "type": "string",
476
+ "minLength": 1
477
+ }
478
+ },
479
+ "staffedHere": {
480
+ "type": "array",
481
+ "minItems": 1,
482
+ "description": "Optional. The roles staffed in the repository this brief is written to, in plan order; absent in the hub brief. Code rules B1 and B2.",
483
+ "items": {
484
+ "$ref": "#/definitions/nonBlankString"
485
+ }
486
+ },
487
+ "context": {
488
+ "$ref": "engagement-context.json",
489
+ "description": "Issue #1173 follow-up (docs/DECISIONS.md decision 28): a contract-shaped snapshot of the hub's clossys/advisor/context.json, taken when the brief is written: one entry per field id, and a known value is always one of that field's fixed choice ids, never founder text. The hub record stays the single source of truth and is written only by Advisor; this copy is how a role running in a product repository -- which has no hub checkout -- reads what the founder already answered. Refreshed by re-applying the plan, never edited in place. Optional: when absent, every context field is read as unknown (packages/advisor/src/engagement-brief.ts's contextFromBrief()), never invented."
490
+ }
491
+ },
492
+ "definitions": {
493
+ "publicProblemPlaceholder": {
494
+ "description": "The fixed text Launcher's apply planner, planApplyBundle(), puts in a brief's problem for a repository whose visibility is not private (see PER-REPOSITORY PROJECTION in this contract's description). The planner computes that brief but writes no file, and the only brief writer today, the legacy brief-only path, commits problem unchanged. It says where the problem is kept, and nothing about what it is.",
495
+ "const": "Held in the engagement hub; not published here."
496
+ },
497
+ "nonBlankString": {
498
+ "title": "a string with at least one non-whitespace character",
499
+ "type": "string",
500
+ "pattern": "\\S"
501
+ },
502
+ "role": {
503
+ "type": "object",
504
+ "additionalProperties": false,
505
+ "required": [
506
+ "role",
507
+ "why",
508
+ "goal",
509
+ "inputsFrom",
510
+ "outputsTo"
511
+ ],
512
+ "properties": {
513
+ "role": {
514
+ "$ref": "#/definitions/nonBlankString",
515
+ "description": "Package directory (short name), e.g. \"publisher\"."
516
+ },
517
+ "why": {
518
+ "$ref": "#/definitions/nonBlankString",
519
+ "description": "For an explicitly chosen role, its own job question. For a role pulled in only to satisfy another role's need, a mechanical explanation naming who needed it."
520
+ },
521
+ "goal": {
522
+ "type": "object",
523
+ "additionalProperties": false,
524
+ "required": [
525
+ "metric",
526
+ "direction"
527
+ ],
528
+ "properties": {
529
+ "metric": {
530
+ "$ref": "#/definitions/nonBlankString",
531
+ "description": "Must match a metric.name in docs/contracts/role-loop-archetypes.json."
532
+ },
533
+ "direction": {
534
+ "enum": [
535
+ "increase",
536
+ "decrease",
537
+ "maintain",
538
+ "target-range"
539
+ ]
540
+ }
541
+ }
542
+ },
543
+ "inputsFrom": {
544
+ "type": "array",
545
+ "items": {
546
+ "type": "string",
547
+ "minLength": 1
548
+ }
549
+ },
550
+ "outputsTo": {
551
+ "type": "array",
552
+ "items": {
553
+ "type": "string",
554
+ "minLength": 1
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+ },
561
+ "engagement-context.json": {
562
+ "$schema": "http://json-schema.org/draft-07/schema#",
563
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/engagement-context.json",
564
+ "title": "Engagement context record",
565
+ "description": "Issue #1173: the business questions a non-technical founder answers once (business, product, audience, stage, intent, constraints), so no later role intake asks again what this record already answers. Lives at clossys/advisor/context.json in a hub (layout approved in issue #1171). Captured through Advisor's question cards (packages/advisor/src/context-questions.ts: nextContextQuestion()/applyContextChoice()), which extend the existing nextSponsorQuestion()/applySponsorChoice() pattern. An unanswered field stays unknown and is never invented. Technical facts (languages, frameworks, installed packages, hosting) never live here -- they come from reading the repository, not from asking the founder. How a role reads it (docs/DECISIONS.md decision 28): never from this hub path directly -- every role, on the hub or in a product repository, reads the snapshot carried in its own repository's clossys/brief.json `context` property (docs/contracts/engagement-brief.json). The field ids below are also the reserved intake question ids: no role's intake card may reuse one (docs/contracts/intake-question-cards.json `engagementContext`).",
566
+ "type": "object",
567
+ "additionalProperties": false,
568
+ "required": [
569
+ "schemaVersion",
570
+ "fields"
571
+ ],
572
+ "properties": {
573
+ "schemaVersion": {
574
+ "const": 1
575
+ },
576
+ "fields": {
577
+ "type": "array",
578
+ "description": "Exactly one entry per field id below; every id appears once, in any order. The schema states it without a uniqueness keyword (draft-07 has none for a property across items): at most six entries, and one entry for each of the six ids, leaves no room for a duplicate. packages/advisor/src/engagement-brief.ts's toEngagementBrief() refuses a duplicate id and writes a field the supplied context lacks as unknown. A reader treats a missing id as unknown (contextFromBrief()), never as an error to guess past.",
579
+ "maxItems": 6,
580
+ "items": {
581
+ "$ref": "#/definitions/field"
582
+ },
583
+ "allOf": [
584
+ {
585
+ "contains": {
586
+ "type": "object",
587
+ "required": [
588
+ "id"
589
+ ],
590
+ "properties": {
591
+ "id": {
592
+ "const": "business"
593
+ }
594
+ }
595
+ }
596
+ },
597
+ {
598
+ "contains": {
599
+ "type": "object",
600
+ "required": [
601
+ "id"
602
+ ],
603
+ "properties": {
604
+ "id": {
605
+ "const": "product"
606
+ }
607
+ }
608
+ }
609
+ },
610
+ {
611
+ "contains": {
612
+ "type": "object",
613
+ "required": [
614
+ "id"
615
+ ],
616
+ "properties": {
617
+ "id": {
618
+ "const": "audience"
619
+ }
620
+ }
621
+ }
622
+ },
623
+ {
624
+ "contains": {
625
+ "type": "object",
626
+ "required": [
627
+ "id"
628
+ ],
629
+ "properties": {
630
+ "id": {
631
+ "const": "stage"
632
+ }
633
+ }
634
+ }
635
+ },
636
+ {
637
+ "contains": {
638
+ "type": "object",
639
+ "required": [
640
+ "id"
641
+ ],
642
+ "properties": {
643
+ "id": {
644
+ "const": "intent"
645
+ }
646
+ }
647
+ }
648
+ },
649
+ {
650
+ "contains": {
651
+ "type": "object",
652
+ "required": [
653
+ "id"
654
+ ],
655
+ "properties": {
656
+ "id": {
657
+ "const": "constraints"
658
+ }
659
+ }
660
+ }
661
+ }
662
+ ]
663
+ }
664
+ },
665
+ "definitions": {
666
+ "fieldId": {
667
+ "enum": [
668
+ "business",
669
+ "product",
670
+ "audience",
671
+ "stage",
672
+ "intent",
673
+ "constraints"
674
+ ]
675
+ },
676
+ "field": {
677
+ "type": "object",
678
+ "description": "An unknown field, or a known field whose value is one of that field's own fixed choice ids -- the recommended and alternative choices on its Advisor question card (packages/advisor/src/context-questions.ts), listed per field below. Never freeform text, and never a slug that is not one of those ids: this record is copied into clossys/brief.json, which is committed in every staffed repository -- including a product repository that may be public while the hub is not (docs/DECISIONS.md decision 28). \"unknown\" and \"something-else\" are not known values: they are not in any enum. toEngagementBrief() applies the same rule in code (applyContextChoice() must return kind \"known\") before copying a snapshot, and a test keeps these enums equal to the card choices. additionalProperties is false inside each oneOf branch, not beside the oneOf: in draft-07 a sibling additionalProperties sees only sibling properties, so at that level it would reject id, state, and value alike.",
679
+ "oneOf": [
680
+ {
681
+ "additionalProperties": false,
682
+ "required": [
683
+ "id",
684
+ "state"
685
+ ],
686
+ "properties": {
687
+ "id": {
688
+ "$ref": "#/definitions/fieldId"
689
+ },
690
+ "state": {
691
+ "const": "unknown"
692
+ }
693
+ }
694
+ },
695
+ {
696
+ "additionalProperties": false,
697
+ "required": [
698
+ "id",
699
+ "state",
700
+ "value"
701
+ ],
702
+ "properties": {
703
+ "id": {
704
+ "const": "business"
705
+ },
706
+ "state": {
707
+ "const": "known"
708
+ },
709
+ "value": {
710
+ "enum": [
711
+ "product-or-service",
712
+ "agency-or-services"
713
+ ]
714
+ }
715
+ }
716
+ },
717
+ {
718
+ "additionalProperties": false,
719
+ "required": [
720
+ "id",
721
+ "state",
722
+ "value"
723
+ ],
724
+ "properties": {
725
+ "id": {
726
+ "const": "product"
727
+ },
728
+ "state": {
729
+ "const": "known"
730
+ },
731
+ "value": {
732
+ "enum": [
733
+ "software",
734
+ "physical-or-in-person"
735
+ ]
736
+ }
737
+ }
738
+ },
739
+ {
740
+ "additionalProperties": false,
741
+ "required": [
742
+ "id",
743
+ "state",
744
+ "value"
745
+ ],
746
+ "properties": {
747
+ "id": {
748
+ "const": "audience"
749
+ },
750
+ "state": {
751
+ "const": "known"
752
+ },
753
+ "value": {
754
+ "enum": [
755
+ "consumers",
756
+ "businesses"
757
+ ]
758
+ }
759
+ }
760
+ },
761
+ {
762
+ "additionalProperties": false,
763
+ "required": [
764
+ "id",
765
+ "state",
766
+ "value"
767
+ ],
768
+ "properties": {
769
+ "id": {
770
+ "const": "stage"
771
+ },
772
+ "state": {
773
+ "const": "known"
774
+ },
775
+ "value": {
776
+ "enum": [
777
+ "building",
778
+ "established"
779
+ ]
780
+ }
781
+ }
782
+ },
783
+ {
784
+ "additionalProperties": false,
785
+ "required": [
786
+ "id",
787
+ "state",
788
+ "value"
789
+ ],
790
+ "properties": {
791
+ "id": {
792
+ "const": "intent"
793
+ },
794
+ "state": {
795
+ "const": "known"
796
+ },
797
+ "value": {
798
+ "enum": [
799
+ "validate",
800
+ "grow"
801
+ ]
802
+ }
803
+ }
804
+ },
805
+ {
806
+ "additionalProperties": false,
807
+ "required": [
808
+ "id",
809
+ "state",
810
+ "value"
811
+ ],
812
+ "properties": {
813
+ "id": {
814
+ "const": "constraints"
815
+ },
816
+ "state": {
817
+ "const": "known"
818
+ },
819
+ "value": {
820
+ "enum": [
821
+ "none-known",
822
+ "tight-budget-or-time"
823
+ ]
824
+ }
825
+ }
826
+ }
827
+ ]
828
+ }
829
+ }
830
+ },
831
+ "repository-inventory.json": {
832
+ "$schema": "http://json-schema.org/draft-07/schema#",
833
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/repository-inventory.json",
834
+ "title": "hub repository inventory",
835
+ "description": "Issues #996, #1334 and #1179: the one v1 shape of a hub's repository inventory. Declared location: clossys/.state/inventory.json in the hub. That file is machine state, not a hand-edited record (see docs/contracts/consumer-layout.json's `clossys/.state/` entry). @clossys/launcher writes it on appoint -- from the repositories a founder chose on @clossys/advisor's repository-choice card (`launcher --repositories`), from a supplied --inventory document, or from a merge of an --inventory document with the on-disk inventory -- and reads it back on every resume. Every inventory file is read as its exact bytes by the shared strict reader (readContractDocument()), never decoded to text first, so bytes that are not valid UTF-8 are refused rather than replaced. Both packages pack this file at build time and check it with the one shared contract checker (packages/advisor/src/contract-schema.ts), so neither depends on the other. Launcher's validateInventoryDocument() checks every inventory document against it, on write and on read; Advisor's repositoryChoiceCard() checks the repository ids it offers against definitions/repositoryId. A document at the declared location that fails this contract is reported as invalid, with the offending field named, never silently treated as though it were merely empty and never adopted as though it validated. One rule is not expressible in this schema and every reader applies it in code: two entries whose ids are the same repository under a different letter case (GitHub owner and repository names are case-insensitive) are a duplicate and are refused, not silently kept as two entries. Where the hub's owner is known -- every read Launcher makes of a hub's inventory, and every set of ids chosen for one -- a bare id also names that owner's repository, so `app` and `<owner>/app` are the same repository and are refused the same way, by position. Launcher decides whether two ids are the same repository by that one identity everywhere (packages/launcher/src/identity.ts, in this public repository). Provenance (#1334): launcher's --inventory once accepted any JSON document that merely carried a repositories-shaped array (for example a governance record whose entries also carried role, visibility, status, and notes) and wrote it as the hub inventory with no schema check; every object here is therefore closed. Future, not built: the apply-plan work (#1178) wants a per-entry `status` field and a GitHub repository id distinct from the human-chosen `id`. Those arrive as a v2 of this contract (a new schemaVersion value, additive fields, or both -- not decided here); this v1 contract does not reserve or accept either field, and a document that includes one now is refused like any other unrecognized key.",
836
+ "type": "object",
837
+ "additionalProperties": false,
838
+ "required": [
839
+ "schemaVersion",
840
+ "repositories"
841
+ ],
842
+ "properties": {
843
+ "schemaVersion": {
844
+ "const": 1
845
+ },
846
+ "repositories": {
847
+ "type": "array",
848
+ "description": "Every repository in the hub's scope, in the order they were chosen. May be empty (a freshly created hub); appointing an existing repository as the hub requires at least one entry, which @clossys/launcher enforces.",
849
+ "items": {
850
+ "$ref": "#/definitions/repository"
851
+ }
852
+ }
853
+ },
854
+ "definitions": {
855
+ "repositoryId": {
856
+ "title": "a bare repository name or owner/name, with no \".\" or \"..\" segment, empty segment, whitespace, or more than one \"/\"",
857
+ "description": "Identifies one repository: a bare repository name (read as belonging to the hub's own owner), or `owner/name`. The owner segment follows this pattern's own owner rule (letters, digits and hyphens, never leading or trailing, at most 39 characters -- this pattern does not reject a run of consecutive hyphens) and the name segment GitHub's repository rule (letters, digits, `.`, `_`, `-`), the same rules @clossys/launcher applies when it resolves a sibling clone beside the hub -- so a document's ids and the repositories Launcher clones or composes skills into are governed by one rule. Valid: `app`, `acme/app`. Refused: `\"\"`, ` app`, `app `, `acme/app/extra`, `acme/`, `/app`, `.`, `..`, `acme/..`.",
858
+ "type": "string",
859
+ "pattern": "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/)?(?!\\.\\.?$)[A-Za-z0-9._-]+$"
860
+ },
861
+ "repository": {
862
+ "type": "object",
863
+ "additionalProperties": false,
864
+ "required": [
865
+ "id"
866
+ ],
867
+ "properties": {
868
+ "id": {
869
+ "$ref": "#/definitions/repositoryId"
870
+ },
871
+ "packages": {
872
+ "type": "array",
873
+ "description": "Optional. Exactly the shape of @clossys/integrator's InventoryPackageEntry (#996, packages/integrator/src/delta.ts, documented in packages/integrator/README.md's \"Per-repository currency delta\" section), which Integrator's emitCurrencyDelta() produces -- one definition read by both packages, not a launcher-owned approximation of it.",
874
+ "items": {
875
+ "$ref": "#/definitions/packageEntry"
876
+ }
877
+ }
878
+ }
879
+ },
880
+ "packageEntry": {
881
+ "type": "object",
882
+ "additionalProperties": false,
883
+ "required": [
884
+ "name"
885
+ ],
886
+ "properties": {
887
+ "name": {
888
+ "$ref": "#/definitions/nonBlankString"
889
+ },
890
+ "version": {
891
+ "$ref": "#/definitions/nonBlankString",
892
+ "description": "Present only where a currency judgment named a target version."
893
+ },
894
+ "wiring": {
895
+ "enum": [
896
+ "dependencies",
897
+ "devDependencies",
898
+ "optionalDependencies",
899
+ "peerDependencies",
900
+ "unknown"
901
+ ]
902
+ }
903
+ }
904
+ },
905
+ "nonBlankString": {
906
+ "title": "a string with at least one non-whitespace character",
907
+ "type": "string",
908
+ "pattern": "\\S"
909
+ }
910
+ }
911
+ },
912
+ "registry-snapshot.json": {
913
+ "$schema": "http://json-schema.org/draft-07/schema#",
914
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/registry-snapshot.json",
915
+ "title": "Registry snapshot",
916
+ "description": "Issue #1178: what the public package registry said, at one moment, about the packages a plan asks for. It is the file clossys/.state/apply/registry-snapshot.json in an engagement hub. It is a projection of each package's registry document, never the document itself. @clossys/advisor reads it to write a plan's exact package acts (advisor-resolve-packages) and never fetches it: Advisor makes no network call. Only Launcher's snapshot step, launcher-apply-plan snapshot, is meant to write it, and no other package code in this repository writes one. Every object is closed: a key this contract does not declare is refused, never ignored. The registry named here must equal the registry in this repository's package-scope.json; a reader checks that in code, against its own packed copy of that file, rather than trusting a value written here. CODE RULES. The keywords above cannot relate one field to another, so these rules are defined here, once, and checked in code after the schema passes; a snapshot that breaks one is invalid exactly as one that breaks the schema is, and has no digest. The shared corpus is docs/contracts/registry-snapshot.fixture.json. A refusal names the rule and the position of the field at fault, never its value. N1: no two packages share a name. N2: no two entries in one package's versions share a version. N3: a package whose status is not-found has latest null and no versions. SNAPSHOT DIGEST. A plan's resolution.snapshotDigest names the snapshot its exact versions were read from. It is the string sha256: followed by the 64 lowercase hexadecimal digits of the SHA-256 hash of the UTF-8 bytes of canonical(subject), where canonical is the JSON Canonicalization Scheme of RFC 8785, exactly as docs/contracts/advisor-plan-digest.md defines it for the plan digest, and subject is the object {registry, packages} built from the snapshot as follows: registry is kept as it is; packages holds every package, sorted by name, each reduced to exactly its name, status, latest and versions; and each package's versions are sorted by version, each version entry kept whole. Sorting compares strings as sequences of UTF-16 code units, the order RFC 8785 sorts keys in; N1 and N2 make both sorts total. Only a snapshot read strictly (invalid UTF-8 and repeated keys refused) that validates against this contract, code rules included, has a digest; an implementation refuses rather than digest any other. Left out of the digest: fetchedAt and fetchedBy, which change on every fetch; each package's responseSha256, which hashes the registry's whole response, and that response changes whenever anything else about the package changes, such as a newer publish or another dist-tag; and schemaVersion and kind, which are constants. So fetching the same selection again gives the same digest, and a plan resolved from it needs no fresh approval, while a change to any field a resolution reads changes the digest. The order packages and versions are written in does not matter. CANONICAL ORDER. The same sorting defines the snapshot's canonical order: packages sorted by name, and each package's versions sorted by version. A reader that names a position in a valid snapshot, such as packages[2].versions[0].hasAttestations in a resolution finding, names it in this order, so the position is the same however a fetch listed the packages. A violation of this contract names positions as the file lists them, because an invalid snapshot has no canonical order. WHAT A SNAPSHOT PROVES. Only what the fetching step recorded. It is a selection record, not evidence of where a package came from: an edited snapshot can at most select different bytes, and whether those bytes are genuine has to be shown by verifying the package's provenance, which nothing in this file does.",
917
+ "type": "object",
918
+ "additionalProperties": false,
919
+ "required": [
920
+ "schemaVersion",
921
+ "kind",
922
+ "registry",
923
+ "fetchedAt",
924
+ "fetchedBy",
925
+ "packages"
926
+ ],
927
+ "properties": {
928
+ "schemaVersion": {
929
+ "const": 1
930
+ },
931
+ "kind": {
932
+ "const": "clossys.registry-snapshot"
933
+ },
934
+ "registry": {
935
+ "$ref": "#/definitions/registryUrl",
936
+ "description": "The registry every package was fetched from. A reader refuses a snapshot whose registry is not the one in its packed package-scope.json."
937
+ },
938
+ "fetchedAt": {
939
+ "$ref": "#/definitions/dateTime",
940
+ "description": "When the fetch finished. Excluded from the snapshot digest."
941
+ },
942
+ "fetchedBy": {
943
+ "type": "object",
944
+ "additionalProperties": false,
945
+ "required": [
946
+ "name",
947
+ "version"
948
+ ],
949
+ "description": "The package that fetched the snapshot, and its version. Excluded from the snapshot digest.",
950
+ "properties": {
951
+ "name": {
952
+ "$ref": "#/definitions/packageName"
953
+ },
954
+ "version": {
955
+ "$ref": "#/definitions/semver"
956
+ }
957
+ }
958
+ },
959
+ "packages": {
960
+ "type": "array",
961
+ "minItems": 1,
962
+ "description": "One entry per package asked for, in any order. Code rules N1 and N3.",
963
+ "items": {
964
+ "$ref": "#/definitions/packageEntry"
965
+ }
966
+ }
967
+ },
968
+ "definitions": {
969
+ "nonBlankString": {
970
+ "title": "a string with at least one non-whitespace character",
971
+ "type": "string",
972
+ "pattern": "\\S"
973
+ },
974
+ "dateTime": {
975
+ "title": "a real ISO 8601 date-time with a time zone, such as 2026-09-24T12:00:00Z",
976
+ "description": "format date-time, asserted field by field exactly as in the plan contract (docs/contracts/advisor-plan.json).",
977
+ "type": "string",
978
+ "format": "date-time"
979
+ },
980
+ "sha256Digest": {
981
+ "title": "sha256: followed by 64 lowercase hexadecimal digits",
982
+ "type": "string",
983
+ "pattern": "^sha256:[0-9a-f]{64}$"
984
+ },
985
+ "registryUrl": {
986
+ "title": "an https registry origin with no path and no trailing slash, such as https://registry.example.com",
987
+ "type": "string",
988
+ "pattern": "^https://[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?$"
989
+ },
990
+ "packageName": {
991
+ "title": "a scoped package name in lowercase, such as @scope/name, of at most 214 characters",
992
+ "description": "The same rule as the plan contract's packageName (docs/contracts/advisor-plan.json): npm's limit is 214 characters, asserted by the lookahead because the checker has no maxLength.",
993
+ "type": "string",
994
+ "pattern": "^(?=.{1,214}$)@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$"
995
+ },
996
+ "semver": {
997
+ "title": "a semantic version such as 1.2.3, which may carry a prerelease or build suffix, with at most 16 digits in each of its three numbers",
998
+ "description": "Semantic Versioning 2.0.0's grammar, with the plan contract's limit of 16 digits on each of major, minor and patch (docs/contracts/advisor-plan.json, exactVersion), so a release version recorded here always fits a plan. A prerelease or build suffix is accepted here so that a resolver can refuse it by name, rather than the snapshot failing its shape.",
999
+ "type": "string",
1000
+ "pattern": "^(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
1001
+ },
1002
+ "packageEntry": {
1003
+ "type": "object",
1004
+ "additionalProperties": false,
1005
+ "required": [
1006
+ "name",
1007
+ "status",
1008
+ "latest",
1009
+ "versions",
1010
+ "responseSha256"
1011
+ ],
1012
+ "properties": {
1013
+ "name": {
1014
+ "$ref": "#/definitions/packageName"
1015
+ },
1016
+ "status": {
1017
+ "enum": [
1018
+ "found",
1019
+ "not-found"
1020
+ ],
1021
+ "description": "found when the registry served the package's document; not-found when it answered that no such package exists. A fetch that gets any other answer writes no snapshot."
1022
+ },
1023
+ "latest": {
1024
+ "description": "The version the registry's latest dist-tag named, or null when it named none. Code rule N3.",
1025
+ "oneOf": [
1026
+ {
1027
+ "type": "null"
1028
+ },
1029
+ {
1030
+ "$ref": "#/definitions/semver"
1031
+ }
1032
+ ]
1033
+ },
1034
+ "versions": {
1035
+ "type": "array",
1036
+ "description": "Version entries, in any order. A snapshot taken to resolve a plan records the version latest names, when the registry lists it; a later reader may record more. Code rules N2 and N3.",
1037
+ "items": {
1038
+ "$ref": "#/definitions/versionEntry"
1039
+ }
1040
+ },
1041
+ "responseSha256": {
1042
+ "$ref": "#/definitions/sha256Digest",
1043
+ "description": "The SHA-256 of the registry response this entry was projected from, kept for audit. Excluded from the snapshot digest."
1044
+ }
1045
+ }
1046
+ },
1047
+ "versionEntry": {
1048
+ "type": "object",
1049
+ "additionalProperties": false,
1050
+ "required": [
1051
+ "version",
1052
+ "integrity",
1053
+ "tarball",
1054
+ "deprecated",
1055
+ "publishedAt",
1056
+ "hasAttestations"
1057
+ ],
1058
+ "properties": {
1059
+ "version": {
1060
+ "$ref": "#/definitions/semver"
1061
+ },
1062
+ "integrity": {
1063
+ "description": "The integrity value the registry serves for this version's tarball, exactly as served, or null when it serves none. It is recorded even when it is not one sha512 value, so that a resolver can refuse it by name.",
1064
+ "oneOf": [
1065
+ {
1066
+ "type": "null"
1067
+ },
1068
+ {
1069
+ "$ref": "#/definitions/nonBlankString"
1070
+ }
1071
+ ]
1072
+ },
1073
+ "tarball": {
1074
+ "$ref": "#/definitions/nonBlankString",
1075
+ "description": "The tarball URL the registry serves for this version, exactly as served."
1076
+ },
1077
+ "deprecated": {
1078
+ "type": "boolean",
1079
+ "description": "true when the registry marks this version deprecated. The deprecation message is not recorded."
1080
+ },
1081
+ "publishedAt": {
1082
+ "description": "When the registry says this version was published, or null when it does not say.",
1083
+ "oneOf": [
1084
+ {
1085
+ "type": "null"
1086
+ },
1087
+ {
1088
+ "$ref": "#/definitions/dateTime"
1089
+ }
1090
+ ]
1091
+ },
1092
+ "hasAttestations": {
1093
+ "type": "boolean",
1094
+ "description": "true when the registry lists attestations for this version. It records presence only; nothing here verifies them."
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ },
1100
+ "repository-change-set.json": {
1101
+ "$schema": "http://json-schema.org/draft-07/schema#",
1102
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/repository-change-set.json",
1103
+ "title": "Repository change set",
1104
+ "description": "Issue #1178: everything one pull request would change in one repository when an approved plan is applied, computed from the plan and from observations of the repository's default branch, never from a working tree. @clossys/launcher computes it (planApplyBundle()) and validates it against this file, which its build packs. It is a computed record, not a claim that anything was written: nothing in it says a file exists, a pull request is open, or a change was applied, and none of the repository states (planned, materialized, proposed, applied and so on) appear in it. Those are derived later, from evidence. PUBLIC TEXT. Parts of a set reach the product repository, which may be public, through its files, its ledger, its branch and its pull request, so no member that does is plan or brief text: roles are lowercase id tokens, a planItem is derived from the repository id and the package name (C16), and root names come from a fixed list (C13). The one file that carries plan text by design is clossys/brief.json, whose problem is replaced by a fixed placeholder unless the repository is private. It carries no approval either: which approval binds a set is recorded in the bundle (apply-bundle.json) and in the installed-state ledger (installed-ledger.json), never here, because every member but the six the digest excludes is inside the digest an approval names. No field holds a time, so computing the same set from the same inputs gives the same bytes. Every object is closed: a key this contract does not declare is refused. A map is written as an array of records, because the contract checker implements additionalProperties only as a boolean. Every item act the apply flow will use is declared here, including acts no package computes yet (add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job, exempt-release-age, declare-root-entry, and the agents-pointer and claude-loader records), so a later producer of those acts needs no contract change. DIGEST. changeSetDigest is defined in apply-change-set-digest.md, beside this file, with its corpus apply-change-set-digest.fixture.json. It covers every member except changeSetDigest, branch, bundle, pullRequest, inverse and tooling, and it reduces a derived file to path, mode, derived, item and invariants; that page gives the reason for each exclusion. Because a derived file's bytes are outside the digest, only two files may be derived (code rule C7): the installed-state ledger and the repository's lockfile. PATHS. A path is relative to the repository root, uses / between segments, and has no empty, . or .. segment. A pathAllowList entry is one of the patterns the apply flow may own, listed in definitions.ownedPattern: * matches any characters within one segment, and a segment that is exactly ** matches any number of whole segments, including none; no other segment contains **. DISCOVERY LINKS. A discovery link is a file of mode 120000, a symbolic link as git stores it: its bytes are its target. For a role, it is <root>/clossys-<role> for each discovery root, .claude/skills and .cursor/skills, and its target is ../../.agents/skills/clossys-<role>. REPOSITORY PROFILE. A repository may declare a Controller repository profile (repository-profile.json, or the known alternate name repository-declaration.json; Controller checks governance/repository-profile.json first and otherwise searches the tree for either name). A profile of schema version 3 with a non-empty rootEntries list is a closed vocabulary of the repository's direct children, and Controller's repository-profile check fails on any direct child it does not declare, or declares as prohibited. observed.repositoryProfile records the profile Controller would locate, or null when there is none: its path; rootVocabulary, which is none when the profile has no root vocabulary Controller checks (schema version 1 or 2, or an empty rootEntries), checked when it has one, and unparseable when the profile cannot be read as a profile with a well-formed rootEntries; and, when checked, undeclaredRoots and prohibitedRoots, the root names this set introduces (direct children absent from the default branch) that the vocabulary does not declare, or declares as prohibited. WRITE KINDS. Every whole file an item names has one write kind, fixed by the act (code rule C15), and no act deletes a file: a removal set, when one exists, adds a delete kind with its own rules. write (write-record, compose-skills' SKILL.md and clossys/.state/skills.json, add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job): after is not null; before may be null (create), equal to after (keep), or another digest (update). link (compose-skills' discovery links): after is not null, and before is null or equal to after, because a link's target is fixed by its role. create-or-edit (exempt-release-age): after is not null and differs from before, which may be null (the file is created with only its entry). edit (declare-root-entry): before and after are both not null and differ: the act changes a file the default branch has, and never creates or deletes it. install, pin-starter and write-ledger write no whole file (code rules C4, C7 and C9). LOCKFILE PATH. A repository's lockfile path is observed.lockfile, or, when that is none, the file its package manager writes: package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for yarn. A repository whose packageManager is none has no lockfile path. CANONICAL ORDER. Arrays whose order carries no meaning are written in one order, so the same change always has the same bytes and the same digest. Strings compare by UTF-16 code units, and a pair compares its first member, then its second. CODE RULES, checked in code after the schema passes, because the keywords above cannot relate one field to another. A refusal names the rule and the position of the field at fault, never its value. C1: no two items share an id. C2: every files[].item, keys[].item and refused[].item, and the item of every package invariant, is the id of an item in items. C3: no two files share a path and no two keys share a pointer; paths compare case-insensitively, so two paths that differ only in letter case are the same path. No path is both in files and in refused, and no pointer is both in keys and in refused. Every path the set touches is matched by some pathAllowList entry: every files[].path, the file of every keys[] entry, and the path of every item that names one (exempt-release-age). C4: ledger.generation is 0 or more; exactly one item has act write-ledger, and exactly one file names it: a derived file at clossys/.state/installed.json whose invariants are exactly one ledgerGeneration equal to ledger.generation plus 1. C5: changeSetDigest is the digest apply-change-set-digest.md defines, branch is clossys/apply- followed by the first 12 hexadecimal digits of changeSetDigest, and pullRequest.title ends with the same 12 digits. C6: no planItem appears twice across items and deferred. C7: every derived file is either the ledger (C4) or the lockfile: at most one derived file is at the repository's lockfile path (see LOCKFILE PATH), and every one of its invariants is a package invariant; no other file is derived, and no whole file is at the ledger path, at package.json, or at any lockfile name. C8: items are sorted by id; files by path; keys by file, then pointer; deferred by planItem; each file's invariants by package name; and, each with no entry repeated, refused by path (or file), then pointer (a path refusal has none, which sorts first), pathAllowList by value, observed.releaseAgeSurfaces by surface, then path, observed.symlinkedSkillRoots by value, and tooling by tool. C9: each item's writes match the item. A file naming an item that is not an install, pin-starter or write-ledger item is a whole file. A whole file has mode 100644, except a discovery link, which has mode 120000, and a discovery link's after, unless null, is the content digest of its target text, with no line feed. Each of the following items is named by exactly one whole file or one path refusal at each path it binds, and by nothing else: a write-record item binds clossys/brief.json for source engagement-brief, AGENTS.md for agents-pointer, CLAUDE.md for claude-loader (by path only: the pointer and loader files' bytes are not checked here), and clossys/AGENTS.md for agents-guide (by path only: the step that writes and verifies the file checks its bytes); a compose-skills item names each of its roles once, with no role repeated, and binds, for each role, .agents/skills/clossys-<role>/SKILL.md, and, for each role whose SKILL.md is named by a whole file, the discovery link under each discovery root that observed.symlinkedSkillRoots does not list, and binds clossys/.state/skills.json once (a role whose SKILL.md is refused gets no discovery link, because a link would expose a skill the flow does not own); an add-caller-workflow item binds .github/workflows/clossys-adoption-evidence.yml, .github/workflows/clossys-adoption-decision.yml and .github/scripts/clossys-collect-adoption-snapshot.mjs; a write-starter-request item binds .starter/request.json; an add-ci-template item binds .github/workflows/clossys-ci.yml; and an add-path-scope-job item binds .github/workflows/clossys-path-scope.yml. An exempt-release-age item is named by at most one whole file or path refusal, at its own path, and by nothing else: by none when the default branch already lists its entry (which needs the file's bytes, so it is not checked here). A declare-root-entry item is named by exactly one whole file or path refusal, at its own path, and by nothing else. The write-ledger item is named by no refusal: the ledger is always written. So every refusal names a path or key its item binds, and no other: a path refusal only at a path the item binds above, and a key refusal only for an install or pin-starter item, at /dependencies/ or /devDependencies/ and that item's package name. No key and no key refusal names an item that is not an install or pin-starter item. An install or pin-starter item is never named by a whole file or a path refusal, and a pin-starter item's placement is devDependencies. Every keys[] entry and every package invariant names such an item: a key's pointer is /<placement>/<name with / written ~1> and its after is the item's version; an invariant's name, version and integrity are the item's. An item with satisfiedInBase true is named by no key, invariant or refusal; one with satisfiedInBase false is named either by exactly one key and exactly one invariant and no refusal, or by at least one key refusal and no key or invariant. A derived lockfile's item is the item of its first invariant. C10: a setup set has no install item (an install waits in deferred), an apply set defers nothing, and a set has at most one pin-starter item. C11: a setup set has exactly one item of each act add-caller-workflow, write-starter-request, add-ci-template and add-path-scope-job, exactly one pin-starter item, and exactly one exempt-release-age item when observed.packageManager is pnpm or yarn, and none otherwise, because npm has no key that exempts a scope from its release-age window. C12: an exempt-release-age item's surface is pnpm-workspace with path pnpm-workspace.yaml in a repository whose observed.packageManager is pnpm, or yarnrc with path .yarnrc.yml in one whose packageManager is yarn; and its scope is the publishing scope the validating package packs from this repository's package-scope.json. This contract sees content digests, not bytes, so it does not check that the file an exempt-release-age item writes differs from its before only by that one entry; that check needs the file's bytes and belongs to the step that writes them. C13: a set has at most one declare-root-entry item, and has one exactly when observed.repositoryProfile is not null and its rootVocabulary is unparseable, or is checked with a non-empty undeclaredRoots or prohibitedRoots. Its path is observed.repositoryProfile.path, and its entries' names are exactly undeclaredRoots, in that order. When rootVocabulary is unparseable, its entries are empty and it is named by a path refusal with reason root-vocabulary-unknown; when prohibitedRoots is not empty, it is named by a path refusal with reason root-entry-prohibited, because the flow never overrides a name the repository prohibits; otherwise it is named by a whole file (whose write kind, edit, C15 checks). undeclaredRoots and prohibitedRoots are empty unless rootVocabulary is checked, share no name, and each name in them is the first segment of a path the set creates: a whole file whose before is null, or a derived file. A refused path is not written, and a key's file or an edited file already exists on the default branch, so none of them introduces a root name. Each of those names, and each entry's name, is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), so it is a fixed name no plan or brief text can supply, and well within Controller's 255 UTF-16 code units. When no declare-root-entry item is needed, as when the profile already declares every root name the set introduces, the set has none. This contract sees content digests, not bytes, so it does not check that the profile's after differs from its before only by those entries; that check belongs to the step that writes them. C14: every path in observed.linkedAgentsPaths is .agents, .agents/skills, or .agents/skills/clossys-<role> for a role of a compose-skills item; a SKILL.md at or under such a path is never written, and is named by a path refusal with reason skills-root-is-link; and no other path refusal has that reason. A write through a symbolic link would land wherever it points. observed.linkedAgentsPaths covers .agents only: a symbolic link at another root the set writes under, such as clossys/, .github/ or .starter/, is refused by the step that writes the files, which this contract does not describe. C15: every whole file obeys its item's write kind (WRITE KINDS). C16: every package item's planItem is the repository id, a colon and the package name, `${repository.id}:${package.name}`, exactly and in the same letter case, and every deferral's planItem is the repository id, a colon and a package name; a planItem is written into the installed-state ledger, which may be public, so it is never free text.",
1105
+ "type": "object",
1106
+ "additionalProperties": false,
1107
+ "required": [
1108
+ "schemaVersion",
1109
+ "kind",
1110
+ "producer",
1111
+ "planDigest",
1112
+ "repository",
1113
+ "ledger",
1114
+ "phase",
1115
+ "engine",
1116
+ "integrator",
1117
+ "observed",
1118
+ "items",
1119
+ "files",
1120
+ "keys",
1121
+ "refused",
1122
+ "deferred",
1123
+ "pathAllowList",
1124
+ "branch",
1125
+ "bundle",
1126
+ "pullRequest",
1127
+ "changeSetDigest"
1128
+ ],
1129
+ "properties": {
1130
+ "schemaVersion": {
1131
+ "const": 1
1132
+ },
1133
+ "kind": {
1134
+ "const": "clossys.repository-change-set"
1135
+ },
1136
+ "producer": {
1137
+ "$ref": "#/definitions/packageVersion",
1138
+ "description": "The package and exact version that computed this set. Covered by the digest: composed skill bytes depend on it, so a different producer version is a different set."
1139
+ },
1140
+ "planDigest": {
1141
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
1142
+ "description": "The canonical digest of the plan this set applies (advisor-plan-digest.md)."
1143
+ },
1144
+ "repository": {
1145
+ "$ref": "#/definitions/repository"
1146
+ },
1147
+ "ledger": {
1148
+ "type": "object",
1149
+ "additionalProperties": false,
1150
+ "required": [
1151
+ "generation"
1152
+ ],
1153
+ "description": "The installed-state ledger this set starts from. generation is 0 when the repository has none yet (code rule C4).",
1154
+ "properties": {
1155
+ "generation": {
1156
+ "type": "integer"
1157
+ }
1158
+ }
1159
+ },
1160
+ "phase": {
1161
+ "enum": [
1162
+ "setup",
1163
+ "apply"
1164
+ ],
1165
+ "description": "setup when the repository's default branch does not yet carry what proves a later pull request; apply once it does."
1166
+ },
1167
+ "engine": {
1168
+ "$ref": "#/definitions/pinnedPackage",
1169
+ "description": "The exact Advisor package the hub pins, whose bins decide what the plan authorizes."
1170
+ },
1171
+ "integrator": {
1172
+ "$ref": "#/definitions/pinnedPackage",
1173
+ "description": "The exact Integrator package the hub pins. A product repository's CI runs its provenance check by this exact version, so it is covered by the digest as well as written into that workflow's bytes."
1174
+ },
1175
+ "observed": {
1176
+ "$ref": "#/definitions/observed"
1177
+ },
1178
+ "items": {
1179
+ "type": "array",
1180
+ "description": "What this set does, one act per item (code rules C1 and C6).",
1181
+ "items": {
1182
+ "$ref": "#/definitions/item"
1183
+ }
1184
+ },
1185
+ "files": {
1186
+ "type": "array",
1187
+ "description": "Every whole file this set writes, and every derived file it changes (code rules C2 to C4).",
1188
+ "items": {
1189
+ "$ref": "#/definitions/file"
1190
+ }
1191
+ },
1192
+ "keys": {
1193
+ "type": "array",
1194
+ "description": "Every key this set writes inside a file the repository owns: a JSON pointer and its value, never the whole file (code rules C2 and C3).",
1195
+ "items": {
1196
+ "$ref": "#/definitions/key"
1197
+ }
1198
+ },
1199
+ "refused": {
1200
+ "type": "array",
1201
+ "description": "Every path or key this set would have written but will not, and why. A set with a refusal writes nothing until someone decides.",
1202
+ "items": {
1203
+ "$ref": "#/definitions/refusal"
1204
+ }
1205
+ },
1206
+ "deferred": {
1207
+ "type": "array",
1208
+ "description": "Package acts the plan authorizes for this repository that belong to a later set, and why. Together with the package items they account for every package act the plan names for this repository (code rule C6).",
1209
+ "items": {
1210
+ "$ref": "#/definitions/deferral"
1211
+ }
1212
+ },
1213
+ "pathAllowList": {
1214
+ "type": "array",
1215
+ "minItems": 1,
1216
+ "description": "Every path this set may touch, each an owned pattern (definitions.ownedPattern). A path outside it is refused (code rules C3 and C8).",
1217
+ "items": {
1218
+ "$ref": "#/definitions/ownedPattern"
1219
+ }
1220
+ },
1221
+ "branch": {
1222
+ "type": "string",
1223
+ "pattern": "^clossys/apply-[0-9a-f]{12}$",
1224
+ "description": "The branch this set is proposed from. Excluded from the digest: it is a function of it (code rule C5)."
1225
+ },
1226
+ "bundle": {
1227
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
1228
+ "description": "The digest of the bundle this set belongs to. Excluded from the digest: the bundle digest covers this set's digest."
1229
+ },
1230
+ "pullRequest": {
1231
+ "type": "object",
1232
+ "additionalProperties": false,
1233
+ "required": [
1234
+ "title"
1235
+ ],
1236
+ "description": "The pull request text, rendered from ids and digests only. Excluded from the digest: the title carries the digest's first 12 digits and the body carries the digest itself.",
1237
+ "properties": {
1238
+ "title": {
1239
+ "type": "string",
1240
+ "pattern": "^Clossys: apply plan [0-9a-f]{12}$"
1241
+ },
1242
+ "bodySha256": {
1243
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
1244
+ "description": "Optional until the body is rendered."
1245
+ }
1246
+ }
1247
+ },
1248
+ "inverse": {
1249
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
1250
+ "description": "Optional. The digest of the change set that reverts this one. Excluded from the digest: it is computed from this set."
1251
+ },
1252
+ "texts": {
1253
+ "type": "array",
1254
+ "description": "Optional. The exact bytes of each whole file the set writes, recorded when the set is stored so materialize can write brief, skill and discovery-link files without recomputing them. Excluded from the digest: the set already names each file's after digest; texts carry the bytes those digests name. Sorted by path (code rule C8). Each path must name a whole file in files whose after digest equals the UTF-8 digest of the text (code rule C17).",
1255
+ "items": {
1256
+ "type": "object",
1257
+ "additionalProperties": false,
1258
+ "required": [
1259
+ "path",
1260
+ "text"
1261
+ ],
1262
+ "properties": {
1263
+ "path": {
1264
+ "type": "string"
1265
+ },
1266
+ "text": {
1267
+ "type": "string"
1268
+ }
1269
+ }
1270
+ }
1271
+ },
1272
+ "tooling": {
1273
+ "type": "array",
1274
+ "description": "Optional. The tool versions that materialized the set, recorded for diagnosis when a derived file's bytes are regenerated (for example the npm version that rewrote the lockfile). Excluded from the digest: tooling is expected to vary between machines, and the derived files' invariants, not their bytes, are what the set promises. Sorted by tool (code rule C8).",
1275
+ "items": {
1276
+ "type": "object",
1277
+ "additionalProperties": false,
1278
+ "required": [
1279
+ "tool",
1280
+ "version"
1281
+ ],
1282
+ "properties": {
1283
+ "tool": {
1284
+ "enum": [
1285
+ "node",
1286
+ "npm",
1287
+ "pnpm",
1288
+ "yarn"
1289
+ ]
1290
+ },
1291
+ "version": {
1292
+ "title": "a release version such as 10.9.2",
1293
+ "type": "string",
1294
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$"
1295
+ }
1296
+ }
1297
+ }
1298
+ },
1299
+ "changeSetDigest": {
1300
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
1301
+ "description": "This set's digest (apply-change-set-digest.md; code rule C5)."
1302
+ }
1303
+ },
1304
+ "definitions": {
1305
+ "nonBlankString": {
1306
+ "title": "a string with at least one non-whitespace character",
1307
+ "type": "string",
1308
+ "pattern": "\\S"
1309
+ },
1310
+ "idToken": {
1311
+ "title": "a lowercase id such as unowned-existing",
1312
+ "type": "string",
1313
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
1314
+ },
1315
+ "safePath": {
1316
+ "title": "a relative path with / between segments and no empty, . or .. segment",
1317
+ "type": "string",
1318
+ "pattern": "^(?:(?!\\.\\.?/)[^/\\\\\\u0000-\\u001f]+/)*(?!\\.\\.?$)[^/\\\\\\u0000-\\u001f]+$"
1319
+ },
1320
+ "pathPattern": {
1321
+ "title": "a path pattern: a relative path whose segments may use *, where a segment containing ** is exactly **, and which is not ** alone",
1322
+ "type": "string",
1323
+ "pattern": "^(?!\\*\\*$)(?:(?:\\*\\*|(?!\\.\\.?/)(?:(?!\\*\\*)[^/\\\\\\u0000-\\u001f])+)/)*(?:\\*\\*|(?!\\.\\.?$)(?:(?!\\*\\*)[^/\\\\\\u0000-\\u001f])+)$"
1324
+ },
1325
+ "packageVersion": {
1326
+ "type": "object",
1327
+ "additionalProperties": false,
1328
+ "required": [
1329
+ "name",
1330
+ "version"
1331
+ ],
1332
+ "properties": {
1333
+ "name": {
1334
+ "$ref": "advisor-plan.json#/definitions/packageName"
1335
+ },
1336
+ "version": {
1337
+ "$ref": "advisor-plan.json#/definitions/exactVersion"
1338
+ }
1339
+ }
1340
+ },
1341
+ "pinnedPackage": {
1342
+ "type": "object",
1343
+ "additionalProperties": false,
1344
+ "required": [
1345
+ "name",
1346
+ "version",
1347
+ "integrity"
1348
+ ],
1349
+ "properties": {
1350
+ "name": {
1351
+ "$ref": "advisor-plan.json#/definitions/packageName"
1352
+ },
1353
+ "version": {
1354
+ "$ref": "advisor-plan.json#/definitions/exactVersion"
1355
+ },
1356
+ "integrity": {
1357
+ "$ref": "advisor-plan.json#/definitions/sha512Integrity"
1358
+ }
1359
+ }
1360
+ },
1361
+ "repository": {
1362
+ "type": "object",
1363
+ "additionalProperties": false,
1364
+ "required": [
1365
+ "id",
1366
+ "nodeId",
1367
+ "visibility",
1368
+ "defaultBranch",
1369
+ "baseCommit"
1370
+ ],
1371
+ "properties": {
1372
+ "id": {
1373
+ "$ref": "advisor-plan.json#/definitions/repositoryId",
1374
+ "description": "The repository inventory id, spelled as in the plan's staffing."
1375
+ },
1376
+ "nodeId": {
1377
+ "title": "an opaque GitHub node id",
1378
+ "type": "string",
1379
+ "pattern": "^[A-Za-z0-9_=+/-]+$",
1380
+ "description": "GitHub's immutable id for the repository, observed when the set was computed, so a renamed repository is never mistaken for another one with the old name."
1381
+ },
1382
+ "visibility": {
1383
+ "enum": [
1384
+ "private",
1385
+ "internal",
1386
+ "public"
1387
+ ],
1388
+ "description": "Observed. Anything but private gets the brief's public problem placeholder. Covered by the digest."
1389
+ },
1390
+ "defaultBranch": {
1391
+ "$ref": "#/definitions/nonBlankString"
1392
+ },
1393
+ "baseCommit": {
1394
+ "title": "a full commit id in lowercase hexadecimal",
1395
+ "type": "string",
1396
+ "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$",
1397
+ "description": "The default branch's head the set was computed against. Covered by the digest, so a moved base is a new set."
1398
+ }
1399
+ }
1400
+ },
1401
+ "observed": {
1402
+ "type": "object",
1403
+ "additionalProperties": false,
1404
+ "required": [
1405
+ "packageManager",
1406
+ "lockfile",
1407
+ "releaseAgeSurfaces",
1408
+ "consumerCi",
1409
+ "symlinkedSkillRoots",
1410
+ "repositoryProfile",
1411
+ "linkedAgentsPaths"
1412
+ ],
1413
+ "description": "Technical facts read from the repository's default branch.",
1414
+ "properties": {
1415
+ "packageManager": {
1416
+ "enum": [
1417
+ "npm",
1418
+ "pnpm",
1419
+ "yarn",
1420
+ "none"
1421
+ ],
1422
+ "description": "none when the repository has no package.json."
1423
+ },
1424
+ "lockfile": {
1425
+ "enum": [
1426
+ "package-lock.json",
1427
+ "pnpm-lock.yaml",
1428
+ "yarn.lock",
1429
+ "none"
1430
+ ]
1431
+ },
1432
+ "releaseAgeSurfaces": {
1433
+ "type": "array",
1434
+ "description": "Which package-manager configuration files that can hold a minimum release age exist.",
1435
+ "items": {
1436
+ "type": "object",
1437
+ "additionalProperties": false,
1438
+ "required": [
1439
+ "surface",
1440
+ "path"
1441
+ ],
1442
+ "properties": {
1443
+ "surface": {
1444
+ "enum": [
1445
+ "pnpm-workspace",
1446
+ "yarnrc",
1447
+ "npmrc"
1448
+ ]
1449
+ },
1450
+ "path": {
1451
+ "$ref": "#/definitions/safePath"
1452
+ }
1453
+ }
1454
+ }
1455
+ },
1456
+ "consumerCi": {
1457
+ "type": "boolean",
1458
+ "description": "True when the default branch has a workflow under .github/workflows/ whose file name does not start with clossys-: the repository runs CI of its own."
1459
+ },
1460
+ "symlinkedSkillRoots": {
1461
+ "type": "array",
1462
+ "description": "The discovery roots that are, or lie under, a symbolic link on the default branch. No discovery link is written under such a root, because a write through it would land in the directory it points at (code rule C9). Sorted, with no entry repeated (code rule C8).",
1463
+ "items": {
1464
+ "enum": [
1465
+ ".claude/skills",
1466
+ ".cursor/skills"
1467
+ ]
1468
+ }
1469
+ },
1470
+ "repositoryProfile": {
1471
+ "description": "The Controller repository profile the default branch declares, or null when it declares none (see REPOSITORY PROFILE, and code rule C13). Covered by the digest.",
1472
+ "oneOf": [
1473
+ {
1474
+ "type": "null"
1475
+ },
1476
+ {
1477
+ "$ref": "#/definitions/repositoryProfile"
1478
+ }
1479
+ ]
1480
+ },
1481
+ "linkedAgentsPaths": {
1482
+ "type": "array",
1483
+ "description": "Which of .agents, .agents/skills and each .agents/skills/clossys-<role> the set would write under is a symbolic link on the default branch (code rule C14). It covers .agents only; a link at another root the set writes under is refused by the step that writes the files. Sorted, with no entry repeated (code rule C8). Covered by the digest.",
1484
+ "items": {
1485
+ "title": "a path of .agents, .agents/skills or .agents/skills/clossys-<role>",
1486
+ "type": "string",
1487
+ "pattern": "^\\.agents(?:/skills(?:/clossys-[^/\\\\\\u0000-\\u001f]+)?)?$"
1488
+ }
1489
+ }
1490
+ }
1491
+ },
1492
+ "item": {
1493
+ "oneOf": [
1494
+ {
1495
+ "$ref": "#/definitions/writeRecordItem"
1496
+ },
1497
+ {
1498
+ "$ref": "#/definitions/composeSkillsItem"
1499
+ },
1500
+ {
1501
+ "$ref": "#/definitions/packageItem"
1502
+ },
1503
+ {
1504
+ "$ref": "#/definitions/exemptReleaseAgeItem"
1505
+ },
1506
+ {
1507
+ "$ref": "#/definitions/declareRootEntryItem"
1508
+ },
1509
+ {
1510
+ "$ref": "#/definitions/plainItem"
1511
+ }
1512
+ ]
1513
+ },
1514
+ "writeRecordItem": {
1515
+ "type": "object",
1516
+ "additionalProperties": false,
1517
+ "required": [
1518
+ "id",
1519
+ "act",
1520
+ "source"
1521
+ ],
1522
+ "properties": {
1523
+ "id": {
1524
+ "$ref": "#/definitions/nonBlankString"
1525
+ },
1526
+ "act": {
1527
+ "const": "write-record"
1528
+ },
1529
+ "source": {
1530
+ "enum": [
1531
+ "engagement-brief",
1532
+ "agents-pointer",
1533
+ "claude-loader",
1534
+ "agents-guide"
1535
+ ],
1536
+ "description": "engagement-brief writes the repository's projection of the hub brief to clossys/brief.json; agents-pointer writes the fixed agent pointer text to AGENTS.md; claude-loader writes the fixed loader text to CLAUDE.md; agents-guide writes the Launcher's fixed guide text to clossys/AGENTS.md (code rule C9)."
1537
+ }
1538
+ }
1539
+ },
1540
+ "composeSkillsItem": {
1541
+ "type": "object",
1542
+ "additionalProperties": false,
1543
+ "required": [
1544
+ "id",
1545
+ "act",
1546
+ "roles"
1547
+ ],
1548
+ "properties": {
1549
+ "id": {
1550
+ "$ref": "#/definitions/nonBlankString"
1551
+ },
1552
+ "act": {
1553
+ "const": "compose-skills"
1554
+ },
1555
+ "roles": {
1556
+ "type": "array",
1557
+ "minItems": 1,
1558
+ "description": "The roles staffed in this repository, in plan order. Each is a lowercase id token, because it becomes part of paths the ledger records.",
1559
+ "items": {
1560
+ "$ref": "#/definitions/idToken"
1561
+ }
1562
+ }
1563
+ }
1564
+ },
1565
+ "packageItem": {
1566
+ "type": "object",
1567
+ "additionalProperties": false,
1568
+ "required": [
1569
+ "id",
1570
+ "act",
1571
+ "planItem",
1572
+ "package",
1573
+ "placement",
1574
+ "satisfiedInBase"
1575
+ ],
1576
+ "properties": {
1577
+ "id": {
1578
+ "$ref": "#/definitions/nonBlankString"
1579
+ },
1580
+ "act": {
1581
+ "enum": [
1582
+ "install",
1583
+ "pin-starter"
1584
+ ],
1585
+ "description": "The plan's own package act, carried unchanged."
1586
+ },
1587
+ "planItem": {
1588
+ "$ref": "#/definitions/nonBlankString",
1589
+ "description": "The plan's packages[].planItem this item carries out."
1590
+ },
1591
+ "package": {
1592
+ "$ref": "#/definitions/pinnedPackage"
1593
+ },
1594
+ "placement": {
1595
+ "enum": [
1596
+ "dependencies",
1597
+ "devDependencies"
1598
+ ]
1599
+ },
1600
+ "satisfiedInBase": {
1601
+ "type": "boolean",
1602
+ "description": "True when the default branch already has this exact version, with this exact integrity, at this placement. Such an item writes nothing: it is kept, not dropped, so every act the plan authorizes is accounted for."
1603
+ }
1604
+ }
1605
+ },
1606
+ "exemptReleaseAgeItem": {
1607
+ "type": "object",
1608
+ "additionalProperties": false,
1609
+ "required": [
1610
+ "id",
1611
+ "act",
1612
+ "scope",
1613
+ "surface",
1614
+ "path"
1615
+ ],
1616
+ "properties": {
1617
+ "id": {
1618
+ "$ref": "#/definitions/nonBlankString"
1619
+ },
1620
+ "act": {
1621
+ "const": "exempt-release-age"
1622
+ },
1623
+ "scope": {
1624
+ "type": "string",
1625
+ "pattern": "^@[a-z0-9][a-z0-9._-]*$",
1626
+ "description": "The publishing scope whose packages are exempt (code rule C12)."
1627
+ },
1628
+ "surface": {
1629
+ "enum": [
1630
+ "pnpm-workspace",
1631
+ "yarnrc"
1632
+ ],
1633
+ "description": "The surface that holds the exemption list, which fixes the path (code rule C12). npm has no exemption key, so no item names .npmrc."
1634
+ },
1635
+ "path": {
1636
+ "$ref": "#/definitions/safePath"
1637
+ }
1638
+ }
1639
+ },
1640
+ "plainItem": {
1641
+ "type": "object",
1642
+ "additionalProperties": false,
1643
+ "required": [
1644
+ "id",
1645
+ "act"
1646
+ ],
1647
+ "description": "An act whose bytes are all in files: the files naming this item say exactly what it writes.",
1648
+ "properties": {
1649
+ "id": {
1650
+ "$ref": "#/definitions/nonBlankString"
1651
+ },
1652
+ "act": {
1653
+ "enum": [
1654
+ "write-ledger",
1655
+ "add-caller-workflow",
1656
+ "write-starter-request",
1657
+ "add-ci-template",
1658
+ "add-path-scope-job"
1659
+ ]
1660
+ }
1661
+ }
1662
+ },
1663
+ "fileMode": {
1664
+ "enum": [
1665
+ "100644",
1666
+ "120000"
1667
+ ],
1668
+ "description": "100644 for a regular file, 120000 for a discovery link (code rule C9)."
1669
+ },
1670
+ "contentDigest": {
1671
+ "oneOf": [
1672
+ {
1673
+ "type": "null"
1674
+ },
1675
+ {
1676
+ "$ref": "advisor-plan.json#/definitions/sha256Digest"
1677
+ }
1678
+ ]
1679
+ },
1680
+ "file": {
1681
+ "oneOf": [
1682
+ {
1683
+ "$ref": "#/definitions/wholeFile"
1684
+ },
1685
+ {
1686
+ "$ref": "#/definitions/derivedFile"
1687
+ }
1688
+ ]
1689
+ },
1690
+ "wholeFile": {
1691
+ "type": "object",
1692
+ "additionalProperties": false,
1693
+ "required": [
1694
+ "path",
1695
+ "mode",
1696
+ "before",
1697
+ "after",
1698
+ "item"
1699
+ ],
1700
+ "description": "A file whose exact bytes this set writes. before is the default branch's content digest (null when absent); after is the content digest written (null to remove). A content digest is sha256: and the hex SHA-256 of the file's bytes.",
1701
+ "properties": {
1702
+ "path": {
1703
+ "$ref": "#/definitions/safePath"
1704
+ },
1705
+ "mode": {
1706
+ "$ref": "#/definitions/fileMode"
1707
+ },
1708
+ "before": {
1709
+ "$ref": "#/definitions/contentDigest"
1710
+ },
1711
+ "after": {
1712
+ "$ref": "#/definitions/contentDigest"
1713
+ },
1714
+ "item": {
1715
+ "$ref": "#/definitions/nonBlankString"
1716
+ }
1717
+ }
1718
+ },
1719
+ "derivedFile": {
1720
+ "type": "object",
1721
+ "additionalProperties": false,
1722
+ "required": [
1723
+ "path",
1724
+ "mode",
1725
+ "derived",
1726
+ "item",
1727
+ "invariants"
1728
+ ],
1729
+ "description": "A file whose bytes depend on tooling or on this set's own digest -- the installed-state ledger, or the lockfile -- checked by its invariants, never by byte equality. before and after are optional and excluded from the digest, which is why no other file may be derived (code rule C7).",
1730
+ "properties": {
1731
+ "path": {
1732
+ "enum": [
1733
+ "clossys/.state/installed.json",
1734
+ "package-lock.json",
1735
+ "pnpm-lock.yaml",
1736
+ "yarn.lock"
1737
+ ],
1738
+ "description": "The ledger or the lockfile, and nothing else (code rule C7)."
1739
+ },
1740
+ "mode": {
1741
+ "const": "100644"
1742
+ },
1743
+ "derived": {
1744
+ "const": true
1745
+ },
1746
+ "item": {
1747
+ "$ref": "#/definitions/nonBlankString"
1748
+ },
1749
+ "invariants": {
1750
+ "type": "array",
1751
+ "minItems": 1,
1752
+ "items": {
1753
+ "$ref": "#/definitions/invariant"
1754
+ }
1755
+ },
1756
+ "before": {
1757
+ "$ref": "#/definitions/contentDigest"
1758
+ },
1759
+ "after": {
1760
+ "$ref": "#/definitions/contentDigest"
1761
+ }
1762
+ }
1763
+ },
1764
+ "invariant": {
1765
+ "oneOf": [
1766
+ {
1767
+ "type": "object",
1768
+ "additionalProperties": false,
1769
+ "required": [
1770
+ "item",
1771
+ "name",
1772
+ "version",
1773
+ "integrity"
1774
+ ],
1775
+ "description": "The lockfile resolves this package to this exact version with this exact integrity, whichever package manager wrote it.",
1776
+ "properties": {
1777
+ "item": {
1778
+ "$ref": "#/definitions/nonBlankString"
1779
+ },
1780
+ "name": {
1781
+ "$ref": "advisor-plan.json#/definitions/packageName"
1782
+ },
1783
+ "version": {
1784
+ "$ref": "advisor-plan.json#/definitions/exactVersion"
1785
+ },
1786
+ "integrity": {
1787
+ "$ref": "advisor-plan.json#/definitions/sha512Integrity"
1788
+ }
1789
+ }
1790
+ },
1791
+ {
1792
+ "type": "object",
1793
+ "additionalProperties": false,
1794
+ "required": [
1795
+ "ledgerGeneration"
1796
+ ],
1797
+ "description": "The ledger this set writes has this generation (code rule C4).",
1798
+ "properties": {
1799
+ "ledgerGeneration": {
1800
+ "type": "integer"
1801
+ }
1802
+ }
1803
+ }
1804
+ ]
1805
+ },
1806
+ "key": {
1807
+ "type": "object",
1808
+ "additionalProperties": false,
1809
+ "required": [
1810
+ "file",
1811
+ "pointer",
1812
+ "before",
1813
+ "after",
1814
+ "item"
1815
+ ],
1816
+ "properties": {
1817
+ "file": {
1818
+ "const": "package.json"
1819
+ },
1820
+ "pointer": {
1821
+ "title": "a JSON pointer to one dependency entry, such as /devDependencies/@scope~1name",
1822
+ "type": "string",
1823
+ "pattern": "^/(?:dependencies|devDependencies)/@[a-z0-9][a-z0-9._-]*~1[a-z0-9][a-z0-9._-]*$"
1824
+ },
1825
+ "before": {
1826
+ "description": "The default branch's value at the pointer, or null when the key is absent.",
1827
+ "oneOf": [
1828
+ {
1829
+ "type": "null"
1830
+ },
1831
+ {
1832
+ "type": "string",
1833
+ "minLength": 1
1834
+ }
1835
+ ]
1836
+ },
1837
+ "after": {
1838
+ "description": "The value written, or null to remove the key.",
1839
+ "oneOf": [
1840
+ {
1841
+ "type": "null"
1842
+ },
1843
+ {
1844
+ "$ref": "advisor-plan.json#/definitions/exactVersion"
1845
+ }
1846
+ ]
1847
+ },
1848
+ "item": {
1849
+ "$ref": "#/definitions/nonBlankString"
1850
+ }
1851
+ }
1852
+ },
1853
+ "refusal": {
1854
+ "oneOf": [
1855
+ {
1856
+ "type": "object",
1857
+ "additionalProperties": false,
1858
+ "required": [
1859
+ "path",
1860
+ "reason",
1861
+ "item"
1862
+ ],
1863
+ "properties": {
1864
+ "path": {
1865
+ "type": "string",
1866
+ "minLength": 1,
1867
+ "description": "The path as computed. It may be unsafe: that can be why it is refused."
1868
+ },
1869
+ "reason": {
1870
+ "$ref": "#/definitions/refusalReason"
1871
+ },
1872
+ "item": {
1873
+ "$ref": "#/definitions/nonBlankString"
1874
+ }
1875
+ }
1876
+ },
1877
+ {
1878
+ "type": "object",
1879
+ "additionalProperties": false,
1880
+ "required": [
1881
+ "file",
1882
+ "pointer",
1883
+ "reason",
1884
+ "item"
1885
+ ],
1886
+ "properties": {
1887
+ "file": {
1888
+ "const": "package.json"
1889
+ },
1890
+ "pointer": {
1891
+ "type": "string",
1892
+ "minLength": 1
1893
+ },
1894
+ "reason": {
1895
+ "$ref": "#/definitions/refusalReason"
1896
+ },
1897
+ "item": {
1898
+ "$ref": "#/definitions/nonBlankString"
1899
+ }
1900
+ }
1901
+ }
1902
+ ]
1903
+ },
1904
+ "refusalReason": {
1905
+ "enum": [
1906
+ "unowned-existing",
1907
+ "client-edited",
1908
+ "deleted",
1909
+ "manifest-absent",
1910
+ "unsafe-path",
1911
+ "release-age-surface-conflict",
1912
+ "release-age-surface-unparseable",
1913
+ "root-vocabulary-unknown",
1914
+ "skills-root-is-link",
1915
+ "root-entry-prohibited"
1916
+ ],
1917
+ "description": "unowned-existing: the default branch already has the path or key, and nothing shows the apply flow wrote it, so the flow will not take it over. client-edited: the flow wrote it, and it has changed since. deleted: the flow wrote it and the ledger still names it, but the default branch no longer has it; this drift is reported, never folded into another change. manifest-absent: a key would go in a package.json the repository does not have. unsafe-path: the computed path is not a safe relative path, or is outside pathAllowList. release-age-surface-conflict: the repository already sets the same exemption in another surface, which the list written here would silently replace. release-age-surface-unparseable: the release-age surface has a shape the restricted editor does not edit, such as a flow sequence, an anchor, a tag, or a comment inside the list. root-vocabulary-unknown: the repository's Controller profile cannot be read as a profile with a well-formed root vocabulary, so which root names it allows is unknown. skills-root-is-link: .agents, .agents/skills or a role's skill directory is a symbolic link on the default branch, and the flow never writes through one. root-entry-prohibited: the repository's Controller profile declares, as prohibited, a root name the set introduces."
1918
+ },
1919
+ "deferral": {
1920
+ "type": "object",
1921
+ "additionalProperties": false,
1922
+ "required": [
1923
+ "planItem",
1924
+ "reason"
1925
+ ],
1926
+ "properties": {
1927
+ "planItem": {
1928
+ "$ref": "#/definitions/nonBlankString"
1929
+ },
1930
+ "reason": {
1931
+ "enum": [
1932
+ "after-setup"
1933
+ ],
1934
+ "description": "after-setup: an install waits for the apply-phase set, because a setup set only prepares what proves later pull requests."
1935
+ }
1936
+ }
1937
+ },
1938
+ "ownedPattern": {
1939
+ "description": "A path pattern the apply flow may own, per the apply design's ownership table: Launcher's clossys/ folder, composed skills and their discovery links, the agent pointer files, the Starter request, keys in package.json, the lockfile, a created-only-when-absent Clossys workflow or workflow script (both named clossys-*), a release-age surface, and a Controller repository profile, of which the flow edits only the root entries it adds. installed-ledger.json holds a copy of this list.",
1940
+ "allOf": [
1941
+ {
1942
+ "$ref": "#/definitions/pathPattern"
1943
+ },
1944
+ {
1945
+ "enum": [
1946
+ "clossys/**",
1947
+ ".agents/skills/clossys-*/**",
1948
+ ".claude/skills/clossys-*",
1949
+ ".cursor/skills/clossys-*",
1950
+ "AGENTS.md",
1951
+ "CLAUDE.md",
1952
+ ".starter/request.json",
1953
+ ".github/workflows/clossys-*",
1954
+ ".github/scripts/clossys-*",
1955
+ "package.json",
1956
+ "package-lock.json",
1957
+ "pnpm-lock.yaml",
1958
+ "yarn.lock",
1959
+ ".yarnrc.yml",
1960
+ "pnpm-workspace.yaml",
1961
+ "**/repository-profile.json",
1962
+ "**/repository-declaration.json"
1963
+ ]
1964
+ }
1965
+ ]
1966
+ },
1967
+ "repositoryProfile": {
1968
+ "type": "object",
1969
+ "additionalProperties": false,
1970
+ "required": [
1971
+ "path",
1972
+ "rootVocabulary",
1973
+ "undeclaredRoots",
1974
+ "prohibitedRoots"
1975
+ ],
1976
+ "properties": {
1977
+ "path": {
1978
+ "allOf": [
1979
+ {
1980
+ "$ref": "#/definitions/safePath"
1981
+ },
1982
+ {
1983
+ "$ref": "#/definitions/profileName"
1984
+ }
1985
+ ]
1986
+ },
1987
+ "rootVocabulary": {
1988
+ "enum": [
1989
+ "none",
1990
+ "checked",
1991
+ "unparseable"
1992
+ ]
1993
+ },
1994
+ "undeclaredRoots": {
1995
+ "type": "array",
1996
+ "description": "Sorted, with no entry repeated (code rule C8).",
1997
+ "items": {
1998
+ "$ref": "#/definitions/rootEntryName"
1999
+ }
2000
+ },
2001
+ "prohibitedRoots": {
2002
+ "type": "array",
2003
+ "description": "Sorted, with no entry repeated (code rule C8).",
2004
+ "items": {
2005
+ "$ref": "#/definitions/rootEntryName"
2006
+ }
2007
+ }
2008
+ }
2009
+ },
2010
+ "rootEntryName": {
2011
+ "title": "one direct-child name: 1 to 255 characters (code rules bound it to 255 UTF-16 code units), no / or \\, no control character, not . or .., and no leading or trailing whitespace",
2012
+ "type": "string",
2013
+ "pattern": "^(?!\\.\\.?$)(?=[^\\s])(?=[\\s\\S]*[^\\s]$)[^/\\\\\\u0000-\\u001f\\u007f]{1,255}$"
2014
+ },
2015
+ "declareRootEntryItem": {
2016
+ "type": "object",
2017
+ "additionalProperties": false,
2018
+ "required": [
2019
+ "id",
2020
+ "act",
2021
+ "path",
2022
+ "entries"
2023
+ ],
2024
+ "description": "Adds a root entry to the repository's Controller profile for each root name the set introduces that the profile's vocabulary does not declare, changing nothing else in the file (code rules C9 and C13).",
2025
+ "properties": {
2026
+ "id": {
2027
+ "$ref": "#/definitions/nonBlankString"
2028
+ },
2029
+ "act": {
2030
+ "const": "declare-root-entry"
2031
+ },
2032
+ "path": {
2033
+ "$ref": "#/definitions/safePath"
2034
+ },
2035
+ "entries": {
2036
+ "type": "array",
2037
+ "description": "The entries added, in the order of observed.repositoryProfile.undeclaredRoots. Each is an extension the repository allows, never requires: the flow adds these names, and must be able to remove them.",
2038
+ "items": {
2039
+ "type": "object",
2040
+ "additionalProperties": false,
2041
+ "required": [
2042
+ "name",
2043
+ "classification",
2044
+ "disposition"
2045
+ ],
2046
+ "properties": {
2047
+ "name": {
2048
+ "$ref": "#/definitions/rootEntryName"
2049
+ },
2050
+ "classification": {
2051
+ "const": "extension"
2052
+ },
2053
+ "disposition": {
2054
+ "const": "allowed"
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+ }
2060
+ },
2061
+ "profileName": {
2062
+ "title": "a path whose last segment is repository-profile.json or repository-declaration.json, under none of the directories Controller's profile search skips (.git, node_modules, dist, build, .next, coverage, .turbo, .cache), and not under .github/workflows/, where the flow owns only clossys-* workflows",
2063
+ "type": "string",
2064
+ "pattern": "^(?!(?:[^/]*/)*(?:\\.git|node_modules|dist|build|\\.next|coverage|\\.turbo|\\.cache)/)(?!\\.github/workflows/)(?:[^/]+/)*repository-(?:profile|declaration)\\.json$"
2065
+ }
2066
+ }
2067
+ },
2068
+ "apply-bundle.json": {
2069
+ "$schema": "http://json-schema.org/draft-07/schema#",
2070
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/apply-bundle.json",
2071
+ "title": "Apply bundle",
2072
+ "description": "Issue #1178: one attempt to apply an approved plan across the repositories it staffs -- which change set each repository would get, which repositories were skipped and why, and the one digest an approval binds. @clossys/launcher computes it (planApplyBundle()) and validates it against this file, which its build packs. MODES. A report bundle says what was computed and what the checks found, and claims nothing about any repository: none of its repositories has a state or a binding (code rule A5). A planned bundle is for a run that has checked every pre-apply validation, V1 to V9, against a committed plan: a computed repository that passed all nine and whose change set an approval binds carries state planned and that binding, and one that did not carries neither (code rules A6 and A7). planApplyBundle() writes report bundles only; the launcher's plan command writes a planned bundle when the hub's committed plan carries an approval whose bundle the hub holds. BINDING. A computed repository's binding (installed-ledger.json, definitions.binding) says which approval binds its change set: approved when its changeSetDigest is a member of the bundle the latest committed decision approved, whose digest is subjectDigest (so a later run whose bundle digest differs only because a sibling moved on is still bound); admitted when it is an apply set that follows the approved setup set setupChangeSet under the one-approval rule. The rule that decides it needs the hub's decision and the bundles it holds, so this contract records the result and checks only its consistency with the checks. Every object is closed: a key this contract does not declare is refused. BUNDLE DIGEST. bundleDigest is defined in apply-change-set-digest.md, beside this file: the canonical digest of the plan digest and, sorted by id, the id and change-set digest of every repository that has a change set. It covers nothing else -- not the authorization, the time, the checks, the verdicts, the states, the bindings or the skipped repositories -- so it can be recomputed from digests alone, without the plan's text, and a plan's approval binds it through decisions[].subjectDigest. CODE RULES, checked in code after the schema passes. A refusal names the rule and the position of the field at fault, never its value. A1: no two repositories share an id; ids compare case-insensitively, as the repository inventory compares them. A2: bundleDigest is the digest apply-change-set-digest.md defines, over plan.digest and every repositories entry that has a changeSet. A3: each repository's verdict is the worst of its checks' verdicts (violated, then indeterminate, then satisfied); a computed repository with no checks is satisfied, and a skipped repository with no checks keeps its own verdict. A4: when authorization is not null and its planDigest is not plan.digest, the authorization is for another plan, and every computed repository carries the check V3 with verdict violated and rule authorization-plan-mismatch; when they are equal, or authorization is null, no repository carries that rule. When snapshot is not null and authorization is null, the plan has package acts (a snapshot is recorded exactly when it does, by the plan contract's R6) and nothing authorizes them, so every computed repository carries the check V3 with verdict violated and rule authorization-absent; otherwise no repository carries that rule. A5: in a report bundle, no repository has a state or a binding. A6: in a planned bundle, plan.committed is true, and a computed repository has state exactly when its verdict is satisfied and it has a binding; a repository with state carries, for each of V1 to V9, a check with verdict satisfied. A7: in a planned bundle, a computed repository has a binding exactly when it carries a V3 check and every V3 check it carries is satisfied; an admitted binding is only on an apply repository, its setupChangeSet is not that repository's own changeSet, and its subjectDigest is not this bundle's own bundleDigest, because the approved bundle held the setup set, not this apply set; every binding in one planned bundle names the same subjectDigest, the one approval the latest committed decision gives; and a bundle whose plan has package acts (its snapshot is not null) and that records a binding has an authorization, because V3 passes for package acts only with a current execution authorization; a plan with no package acts is bound by its approving decision alone (the RFC's D2), so its bundle may record a binding with no authorization.",
2073
+ "type": "object",
2074
+ "additionalProperties": false,
2075
+ "required": [
2076
+ "schemaVersion",
2077
+ "kind",
2078
+ "mode",
2079
+ "plan",
2080
+ "snapshot",
2081
+ "engine",
2082
+ "authorization",
2083
+ "computedAt",
2084
+ "repositories",
2085
+ "bundleDigest"
2086
+ ],
2087
+ "properties": {
2088
+ "schemaVersion": {
2089
+ "const": 1
2090
+ },
2091
+ "kind": {
2092
+ "const": "clossys.apply-bundle"
2093
+ },
2094
+ "mode": {
2095
+ "enum": [
2096
+ "report",
2097
+ "planned"
2098
+ ],
2099
+ "description": "report: computed and checked, nothing written to any repository, and no repository state claimed. planned: V1 to V9 were checked, and a repository that passed them all and is bound by an approval is planned (code rules A5 to A7)."
2100
+ },
2101
+ "plan": {
2102
+ "type": "object",
2103
+ "additionalProperties": false,
2104
+ "required": [
2105
+ "path",
2106
+ "digest",
2107
+ "committed"
2108
+ ],
2109
+ "properties": {
2110
+ "path": {
2111
+ "const": "clossys/advisor/plan.json"
2112
+ },
2113
+ "digest": {
2114
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
2115
+ "description": "The canonical plan digest (advisor-plan-digest.md)."
2116
+ },
2117
+ "committed": {
2118
+ "type": "boolean",
2119
+ "description": "Whether the plan read was the one committed at the hub's default-branch head."
2120
+ }
2121
+ }
2122
+ },
2123
+ "snapshot": {
2124
+ "description": "The registry snapshot the plan's exact versions were resolved from, as the plan's resolution names it; null when the plan has no package acts.",
2125
+ "oneOf": [
2126
+ {
2127
+ "type": "null"
2128
+ },
2129
+ {
2130
+ "type": "object",
2131
+ "additionalProperties": false,
2132
+ "required": [
2133
+ "path",
2134
+ "digest"
2135
+ ],
2136
+ "properties": {
2137
+ "path": {
2138
+ "const": "clossys/.state/apply/registry-snapshot.json"
2139
+ },
2140
+ "digest": {
2141
+ "$ref": "advisor-plan.json#/definitions/sha256Digest"
2142
+ }
2143
+ }
2144
+ }
2145
+ ]
2146
+ },
2147
+ "engine": {
2148
+ "type": "object",
2149
+ "additionalProperties": false,
2150
+ "required": [
2151
+ "name",
2152
+ "version",
2153
+ "integrity"
2154
+ ],
2155
+ "description": "The exact Advisor package the hub pins.",
2156
+ "properties": {
2157
+ "name": {
2158
+ "$ref": "advisor-plan.json#/definitions/packageName"
2159
+ },
2160
+ "version": {
2161
+ "$ref": "advisor-plan.json#/definitions/exactVersion"
2162
+ },
2163
+ "integrity": {
2164
+ "$ref": "advisor-plan.json#/definitions/sha512Integrity"
2165
+ }
2166
+ }
2167
+ },
2168
+ "authorization": {
2169
+ "description": "The execution authorization that permits the plan's package acts; null when the bundle has none, as for a bundle that only writes briefs and skills. Excluded from bundleDigest: re-approving the same bytes keeps the same digest.",
2170
+ "oneOf": [
2171
+ {
2172
+ "type": "null"
2173
+ },
2174
+ {
2175
+ "type": "object",
2176
+ "additionalProperties": false,
2177
+ "required": [
2178
+ "planDigest",
2179
+ "expiresAt"
2180
+ ],
2181
+ "properties": {
2182
+ "planDigest": {
2183
+ "$ref": "advisor-plan.json#/definitions/sha256Digest"
2184
+ },
2185
+ "expiresAt": {
2186
+ "$ref": "advisor-plan.json#/definitions/dateTime"
2187
+ }
2188
+ }
2189
+ }
2190
+ ]
2191
+ },
2192
+ "computedAt": {
2193
+ "$ref": "advisor-plan.json#/definitions/dateTime",
2194
+ "description": "When the bundle was computed. Excluded from bundleDigest."
2195
+ },
2196
+ "repositories": {
2197
+ "type": "array",
2198
+ "description": "One entry per repository the plan staffs, each id once (code rule A1). The planner writes them in the plan's staffing order; the bundle does not carry the plan's staffing, so this contract cannot check that order.",
2199
+ "items": {
2200
+ "oneOf": [
2201
+ {
2202
+ "$ref": "#/definitions/computedRepository"
2203
+ },
2204
+ {
2205
+ "$ref": "#/definitions/skippedRepository"
2206
+ }
2207
+ ]
2208
+ }
2209
+ },
2210
+ "bundleDigest": {
2211
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
2212
+ "description": "The digest an approval binds (code rule A2)."
2213
+ }
2214
+ },
2215
+ "definitions": {
2216
+ "idToken": {
2217
+ "title": "a lowercase id such as not-in-inventory",
2218
+ "type": "string",
2219
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
2220
+ },
2221
+ "verdict": {
2222
+ "enum": [
2223
+ "satisfied",
2224
+ "violated",
2225
+ "indeterminate"
2226
+ ]
2227
+ },
2228
+ "check": {
2229
+ "type": "object",
2230
+ "additionalProperties": false,
2231
+ "required": [
2232
+ "check",
2233
+ "verdict"
2234
+ ],
2235
+ "properties": {
2236
+ "check": {
2237
+ "enum": [
2238
+ "V1",
2239
+ "V2",
2240
+ "V3",
2241
+ "V4",
2242
+ "V5",
2243
+ "V6",
2244
+ "V7",
2245
+ "V8",
2246
+ "V9"
2247
+ ],
2248
+ "description": "Which pre-apply validation this entry reports."
2249
+ },
2250
+ "verdict": {
2251
+ "$ref": "#/definitions/verdict"
2252
+ },
2253
+ "rule": {
2254
+ "$ref": "#/definitions/idToken",
2255
+ "description": "Optional. Why, as an id; never prose from the plan or the brief."
2256
+ }
2257
+ }
2258
+ },
2259
+ "computedRepository": {
2260
+ "type": "object",
2261
+ "additionalProperties": false,
2262
+ "required": [
2263
+ "id",
2264
+ "verdict",
2265
+ "phase",
2266
+ "changeSet",
2267
+ "checks"
2268
+ ],
2269
+ "properties": {
2270
+ "id": {
2271
+ "$ref": "advisor-plan.json#/definitions/repositoryId"
2272
+ },
2273
+ "verdict": {
2274
+ "$ref": "#/definitions/verdict",
2275
+ "description": "The worst verdict among checks: violated, then indeterminate, then satisfied; satisfied when there are none (code rule A3)."
2276
+ },
2277
+ "phase": {
2278
+ "enum": [
2279
+ "setup",
2280
+ "apply"
2281
+ ]
2282
+ },
2283
+ "changeSet": {
2284
+ "$ref": "advisor-plan.json#/definitions/sha256Digest",
2285
+ "description": "That repository's changeSetDigest."
2286
+ },
2287
+ "checks": {
2288
+ "type": "array",
2289
+ "items": {
2290
+ "$ref": "#/definitions/check"
2291
+ }
2292
+ },
2293
+ "state": {
2294
+ "const": "planned",
2295
+ "description": "Optional, and only in a planned bundle: every check V1 to V9 passed and an approval binds the change set (code rule A6). Excluded from bundleDigest. Every later state is derived from evidence, never written here."
2296
+ },
2297
+ "binding": {
2298
+ "$ref": "installed-ledger.json#/definitions/binding",
2299
+ "description": "Optional, and only in a planned bundle: which approval binds the change set (code rule A7). Excluded from bundleDigest, which is what an approval binds, so the binding never contains its own result."
2300
+ }
2301
+ }
2302
+ },
2303
+ "skippedRepository": {
2304
+ "type": "object",
2305
+ "additionalProperties": false,
2306
+ "required": [
2307
+ "id",
2308
+ "verdict",
2309
+ "reason",
2310
+ "checks"
2311
+ ],
2312
+ "description": "A staffed repository no change set was computed for. It is not in bundleDigest.",
2313
+ "properties": {
2314
+ "id": {
2315
+ "$ref": "advisor-plan.json#/definitions/repositoryId"
2316
+ },
2317
+ "verdict": {
2318
+ "enum": [
2319
+ "violated",
2320
+ "indeterminate"
2321
+ ]
2322
+ },
2323
+ "reason": {
2324
+ "$ref": "#/definitions/idToken"
2325
+ },
2326
+ "checks": {
2327
+ "type": "array",
2328
+ "items": {
2329
+ "$ref": "#/definitions/check"
2330
+ }
2331
+ }
2332
+ }
2333
+ }
2334
+ }
2335
+ },
2336
+ "installed-ledger.json": {
2337
+ "$schema": "http://json-schema.org/draft-07/schema#",
2338
+ "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/installed-ledger.json",
2339
+ "title": "Installed-state ledger",
2340
+ "description": "Issue #1178: the installed-state ledger, clossys/.state/installed.json in a product repository. It records what the apply flow wrote there, one generation per merged change set, and on whose approval each generation was written. Every change set writes the next generation as a derived file (repository-change-set.json code rules C4 and C7), so the ledger changes only through a pull request the client merges. @clossys/launcher validates a ledger against this file, which its build packs; nothing writes one yet. Every object is closed: a key this contract does not declare is refused. The contract is self-contained so a package can pack it alone: the definitions it shares with advisor-plan.json and repository-change-set.json are copies, and Launcher's tests hold each copy equal to its source. PUBLIC SURFACE. The ledger is committed to the product repository, which may be public, so it holds only digests, versions, integrity values, paths the flow owns, and this repository's own id -- never a sibling repository's id, never plan or brief prose, never a person. No string in it is plan or brief text: a planItem is exactly this repository's id, a colon and the package name (code rule L10), a role appears only as a lowercase id token in a skill path (L5), and a root entry is one of the fixed names an owned pattern can introduce (L9). TRUST. Passing this contract makes a ledger well formed, not true: it is a claim anyone with write access could have edited. A reader that holds the hub trusts a row only when the change set the row names is one the hub holds as a self-verifying file (clossys/.state/apply/change-sets/<digest>.json whose recomputed changeSetDigest equals its name) and, for a files row, that change set's files hold the row's path with the row's after; a row that fails is ignored as unowned (ledger-foreign-row). The reader also requires every history[].changeSet to be such a file (else ledger-chain) and repository.nodeId to equal the observed repository's immutable id (else identity). A reader without the hub, such as a product repository's own CI, trusts no row this way; it may only compare a pull request's ledger with its base's, under SUCCESSION below, where the base is authenticated by the client's merge into its protected default branch. BINDING. history[].binding says on what authority each generation was written. approved: the change set was a member of the bundle the founder approved; subjectDigest is the approving decision's subjectDigest (advisor-plan.json, APPROVAL BINDING). It need not equal the entry's bundle: bundle records the run that computed the set, and a later run's bundle differs whenever a sibling repository's set moved on, while the set itself is still a member of the approved bundle. admitted: an apply set written under the one-approval rule with no second approval; subjectDigest is the approved bundle's digest, and setupChangeSet is the setup set this apply set follows. Neither is inside any digest: the ledger is a derived file, outside its own change set's digest (apply-change-set-digest.md), so it can name the digests an approval binds without containing its own result, and no whole file carries a binding. RENDER. The ledger a change set X, with digest d and binding b, writes over the previous ledger P (none at generation 0) is: repository from X.repository's id and nodeId; generation X.ledger.generation plus 1; history P's history followed by { generation, changeSet: d, phase: X.phase, planDigest: X.planDigest, bundle: X.bundle, baseCommit: X.repository.baseCommit, binding: b }; files P's rows, then for each whole file of X: after null removes the row at its path, a file whose before equals its after keeps P's row at that path unchanged when P has one with that after, and every other file sets the row { path, mode, after, changeSet: d }. In an apply set the only file with a before of null that RENDER writes a row for is the Launcher guide (path clossys/AGENTS.md, mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53, the digest of the guide's bytes), and only where a previous ledger P exists and holds no row at that path in any letter case, for an install set up before the guide existed; an apply set's add at any other path, with any other mode or after, with no previous ledger, or at that path where P holds a row, is refused, so no ledger is rendered for it. A row for a file the default branch already had before the flow edited it -- the Controller profile, or a release-age surface -- is a compare-and-swap record of the bytes the flow last wrote, not ownership of the whole file: the flow owns only the entries rows it added there; keys P's rows, then for each key of X: after null removes the row at its pointer, any other value sets { file, pointer, value: after, changeSet: d }; entries P's rows, then for each exempt-release-age item of X that no path refusal names, the row { file: the item's path, key: minimumReleaseAgeExclude for pnpm-workspace or npmPreapprovedPackages for yarnrc, value: the item's scope and /*, changeSet: d }, keeping P's row unchanged when it already holds that file, key and value, and for each declare-root-entry item of X that no path refusal names, one row per entry { file: the item's path, key: rootEntries, value: the entry's name, changeSet: d }, keeping P's identical row unchanged; packages P's rows, then for each install or pin-starter item of X that no key refusal names, the row { planItem, act, name, version, integrity, placement, changeSet: d } replacing any row with that planItem or that name, or P's row unchanged when it already has that planItem and identity; deferred exactly X's deferred, each with the plan's identity for that planItem ({ planItem, act, name, version, integrity, placement, reason, changeSet: d }). An item satisfied in the base is recorded in packages like any other, because packages lists the package acts in effect, and keys lists only the keys the flow wrote. No row names the ledger or the lockfile. Arrays are written in the canonical order of code rule L8, and every object's members in the order this contract declares them, at every depth. The bytes are the UTF-8 of JSON.stringify(ledger, null, 2) and one line feed. The corpus installed-ledger.fixture.json, beside this file, holds ledgers with the SHA-256 of these bytes, computed independently of any package. CODE RULES, checked in code after the schema passes. A refusal names the rule and the position of the field at fault, never its value. L1: generation is 1 or more and equals the number of history entries, and history[i].generation is i plus 1. L2: no two history entries name the same changeSet, so no two share a bundle and changeSet pair either: a change set is written once. L3: a setup entry's binding is approved; an admitted binding is on an apply entry that is not the first, and the entry immediately before it is the one it names: that entry's changeSet equals setupChangeSet, its phase is setup, its binding is approved, and it has the same planDigest and the same subjectDigest. L4: every row's changeSet, in files, keys, entries, packages and deferred, is the changeSet of some history entry; every deferred row's changeSet is the last history entry's; and when the last entry's phase is apply, deferred is empty. L5: no files row is at the ledger's own path, at package.json or at a lockfile name (package-lock.json, pnpm-lock.yaml, yarn.lock), compared case-insensitively; every files row's path is matched by some definitions.ownedPattern entry, where * matches any characters within one segment and a segment that is exactly ** matches any number of whole segments; a files row has mode 120000 exactly when its path is a discovery link, .claude/skills/clossys-<role> or .cursor/skills/clossys-<role> with <role> one segment; and the <role> of every path under .agents/skills/clossys-<role>/ and of every discovery link is a lowercase id token. L6: every keys row's pointer is /<placement>/<name with ~ written ~0 and / written ~1> of exactly one packages row, and its value is that row's version. L7: no planItem and no package name appears twice across packages and deferred together. L8: files are sorted by path, keys by file then pointer, entries by file, then key, then value, packages by planItem and deferred by planItem, strings comparing by UTF-16 code units, with no entry repeated; files paths compare case-insensitively for repetition. L9: every rootEntries row names the same file, the one Controller profile the flow edits, and each value is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), well within Controller's 255 UTF-16 code units. L10: every packages and deferred row's planItem is exactly repository.id, a colon and the row's name, in the same letter case. SUCCESSION, checked by comparing a pull request's head ledger with its base's ledger (the base's is absent before generation 1), each read from its bytes: each must be valid under this contract, and its bytes must be exactly the bytes RENDER gives it, so a repeated key, a byte order mark or any other spelling of a ledger is refused (rule bytes), never read as an unchanged ledger. S1: when head's bytes equal base's, the pull request makes no ledger change, and the rules below do not apply. S2: otherwise head.repository equals base.repository, head.generation is base.generation plus 1 (1 when the base has none), and head.history without its last entry equals base.history. S3: when head's last entry is admitted, the base has a ledger and its last history entry is the one setupChangeSet names; head.deferred is empty; head.entries equal base's, and head.files equal base's or add exactly one row and drop none, the row for clossys/AGENTS.md with mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53 (the digest of the Launcher guide's bytes, so a row naming any other bytes is refused) and head's last changeSet, where the base holds no row at that path in any letter case; every base keys row and every base packages row is in head unchanged; and the other packages rows are exactly base.deferred's rows, each with the same planItem, act, name, version, integrity and placement and head's last changeSet; and the other keys rows each name one of those packages, with head's last changeSet. So an admitted generation installs exactly the packages the approved setup set deferred and changes no other row, apart from that one files row. An approved generation is checked by S2 alone, and it proves nothing: a reader without the hub cannot authenticate the approval it names, so for that reader an approved head generation is an unauthenticated claim of approval, never an admission and never a pass. A pull request could relabel an admitted generation approved to escape S3; a reader that must decide without the hub refuses such a head on an apply pull request, or treats the pull request as one that needs the client's own review. The result of a succession check therefore says which of three it found: no new generation, an admitted generation that S3 proved, or a claimed approval.",
2341
+ "type": "object",
2342
+ "additionalProperties": false,
2343
+ "required": [
2344
+ "schemaVersion",
2345
+ "kind",
2346
+ "repository",
2347
+ "generation",
2348
+ "history",
2349
+ "files",
2350
+ "keys",
2351
+ "entries",
2352
+ "packages",
2353
+ "deferred"
2354
+ ],
2355
+ "properties": {
2356
+ "schemaVersion": {
2357
+ "const": 1
2358
+ },
2359
+ "kind": {
2360
+ "const": "clossys.installed-ledger"
2361
+ },
2362
+ "repository": {
2363
+ "type": "object",
2364
+ "additionalProperties": false,
2365
+ "required": [
2366
+ "id",
2367
+ "nodeId"
2368
+ ],
2369
+ "description": "This repository, as the change sets that wrote the ledger name it. Its own id only.",
2370
+ "properties": {
2371
+ "id": {
2372
+ "$ref": "#/definitions/repositoryId"
2373
+ },
2374
+ "nodeId": {
2375
+ "$ref": "#/definitions/nodeId"
2376
+ }
2377
+ }
2378
+ },
2379
+ "generation": {
2380
+ "type": "integer",
2381
+ "description": "The number of change sets that wrote this ledger (code rule L1). A change set computed from generation n writes generation n plus 1."
2382
+ },
2383
+ "history": {
2384
+ "type": "array",
2385
+ "minItems": 1,
2386
+ "description": "One entry per generation, oldest first (code rules L1 to L3). An entry is never rewritten: a later generation appends.",
2387
+ "items": {
2388
+ "$ref": "#/definitions/historyEntry"
2389
+ }
2390
+ },
2391
+ "files": {
2392
+ "type": "array",
2393
+ "description": "Every whole file the flow wrote and still owns, with the content digest it wrote (code rules L5 and L8).",
2394
+ "items": {
2395
+ "$ref": "#/definitions/fileRow"
2396
+ }
2397
+ },
2398
+ "keys": {
2399
+ "type": "array",
2400
+ "description": "Every package.json key the flow wrote, with the value it wrote (code rules L6 and L8).",
2401
+ "items": {
2402
+ "$ref": "#/definitions/keyRow"
2403
+ }
2404
+ },
2405
+ "entries": {
2406
+ "type": "array",
2407
+ "description": "Every entry the flow added to a list in a file the repository owns: a release-age exemption entry, or a root entry in the repository's Controller profile (code rules L8 and L9).",
2408
+ "items": {
2409
+ "$ref": "#/definitions/entryRow"
2410
+ }
2411
+ },
2412
+ "packages": {
2413
+ "type": "array",
2414
+ "description": "Every package act in effect: each install or pin-starter item a merged change set carried out or found already satisfied, with its exact identity (code rules L6 to L8).",
2415
+ "items": {
2416
+ "$ref": "#/definitions/packageRow"
2417
+ }
2418
+ },
2419
+ "deferred": {
2420
+ "type": "array",
2421
+ "description": "The package acts the latest setup set deferred to the apply set, with the plan's identity for each, so the apply set can be compared with them without the plan (code rules L4, L7 and L8).",
2422
+ "items": {
2423
+ "$ref": "#/definitions/deferredRow"
2424
+ }
2425
+ }
2426
+ },
2427
+ "definitions": {
2428
+ "sha256Digest": {
2429
+ "title": "sha256: followed by 64 lowercase hexadecimal digits",
2430
+ "type": "string",
2431
+ "pattern": "^sha256:[0-9a-f]{64}$"
2432
+ },
2433
+ "sha512Integrity": {
2434
+ "title": "one sha512 integrity value: sha512- followed by the canonical base64 of 64 bytes (88 characters ending in ==)",
2435
+ "description": "Canonical base64 only: the last character before == carries two bits of data and four zero bits, so it is one of A, Q, g or w. Any other character there would decode to the same bytes as one of those, so a second spelling of one value is refused.",
2436
+ "type": "string",
2437
+ "pattern": "^sha512-[A-Za-z0-9+/]{85}[AQgw]==$"
2438
+ },
2439
+ "exactVersion": {
2440
+ "title": "an exact release version such as 1.2.3, with no range, tag, prerelease or build suffix, and at most 16 digits in each part",
2441
+ "description": "Each part is 0 or has no leading zero, and has at most 16 digits, the longest numeric part semver accepts.",
2442
+ "type": "string",
2443
+ "pattern": "^(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})\\.(0|[1-9][0-9]{0,15})$"
2444
+ },
2445
+ "packageName": {
2446
+ "title": "a scoped package name in lowercase, such as @scope/name, of at most 214 characters",
2447
+ "description": "npm's limit is 214 characters, asserted by the lookahead because the checker has no maxLength. Neither the scope nor the name may start with '.', '_' or '-'.",
2448
+ "type": "string",
2449
+ "pattern": "^(?=.{1,214}$)@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$"
2450
+ },
2451
+ "repositoryId": {
2452
+ "title": "a repository inventory id: a bare repository name, or owner/name",
2453
+ "description": "The repository inventory's id rule (docs/contracts/repository-inventory.json) as one pattern: an optional GitHub owner (letters, digits and single inner hyphens, at most 39 characters) and a slash, then a repository name of letters, digits, '.', '_' and '-' that is not exactly '.' or '..'. No whitespace, no empty segment, at most one slash.",
2454
+ "type": "string",
2455
+ "pattern": "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/)?(?!\\.\\.?$)[A-Za-z0-9._-]+$"
2456
+ },
2457
+ "nodeId": {
2458
+ "title": "an opaque GitHub node id",
2459
+ "type": "string",
2460
+ "pattern": "^[A-Za-z0-9_=+/-]+$"
2461
+ },
2462
+ "commit": {
2463
+ "title": "a full commit id in lowercase hexadecimal",
2464
+ "type": "string",
2465
+ "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$"
2466
+ },
2467
+ "safePath": {
2468
+ "title": "a relative path with / between segments and no empty, . or .. segment",
2469
+ "type": "string",
2470
+ "pattern": "^(?:(?!\\.\\.?/)[^/\\\\\\u0000-\\u001f]+/)*(?!\\.\\.?$)[^/\\\\\\u0000-\\u001f]+$"
2471
+ },
2472
+ "nonBlankString": {
2473
+ "title": "a string with at least one non-whitespace character",
2474
+ "type": "string",
2475
+ "pattern": "\\S"
2476
+ },
2477
+ "placement": {
2478
+ "enum": [
2479
+ "dependencies",
2480
+ "devDependencies"
2481
+ ]
2482
+ },
2483
+ "ownedPattern": {
2484
+ "description": "A path pattern the apply flow may own: the same list as repository-change-set.json's definitions.ownedPattern.",
2485
+ "enum": [
2486
+ "clossys/**",
2487
+ ".agents/skills/clossys-*/**",
2488
+ ".claude/skills/clossys-*",
2489
+ ".cursor/skills/clossys-*",
2490
+ "AGENTS.md",
2491
+ "CLAUDE.md",
2492
+ ".starter/request.json",
2493
+ ".github/workflows/clossys-*",
2494
+ ".github/scripts/clossys-*",
2495
+ "package.json",
2496
+ "package-lock.json",
2497
+ "pnpm-lock.yaml",
2498
+ "yarn.lock",
2499
+ ".yarnrc.yml",
2500
+ "pnpm-workspace.yaml",
2501
+ "**/repository-profile.json",
2502
+ "**/repository-declaration.json"
2503
+ ]
2504
+ },
2505
+ "binding": {
2506
+ "description": "On what authority a change set was written (see BINDING in this contract's description). The apply-bundle contract reuses this definition for a planned repository's binding.",
2507
+ "oneOf": [
2508
+ {
2509
+ "$ref": "#/definitions/approvedBinding"
2510
+ },
2511
+ {
2512
+ "$ref": "#/definitions/admittedBinding"
2513
+ }
2514
+ ]
2515
+ },
2516
+ "approvedBinding": {
2517
+ "type": "object",
2518
+ "additionalProperties": false,
2519
+ "required": [
2520
+ "kind",
2521
+ "subjectDigest"
2522
+ ],
2523
+ "properties": {
2524
+ "kind": {
2525
+ "const": "approved"
2526
+ },
2527
+ "subjectDigest": {
2528
+ "$ref": "#/definitions/sha256Digest",
2529
+ "description": "The approving decision's subjectDigest: the digest of the bundle the founder approved, of which this change set is a member."
2530
+ }
2531
+ }
2532
+ },
2533
+ "admittedBinding": {
2534
+ "type": "object",
2535
+ "additionalProperties": false,
2536
+ "required": [
2537
+ "kind",
2538
+ "subjectDigest",
2539
+ "setupChangeSet"
2540
+ ],
2541
+ "properties": {
2542
+ "kind": {
2543
+ "const": "admitted"
2544
+ },
2545
+ "subjectDigest": {
2546
+ "$ref": "#/definitions/sha256Digest",
2547
+ "description": "The approving decision's subjectDigest: the approved bundle that holds the setup set this apply set follows."
2548
+ },
2549
+ "setupChangeSet": {
2550
+ "$ref": "#/definitions/sha256Digest",
2551
+ "description": "The changeSetDigest of that setup set."
2552
+ }
2553
+ }
2554
+ },
2555
+ "historyEntry": {
2556
+ "type": "object",
2557
+ "additionalProperties": false,
2558
+ "required": [
2559
+ "generation",
2560
+ "changeSet",
2561
+ "phase",
2562
+ "planDigest",
2563
+ "bundle",
2564
+ "baseCommit",
2565
+ "binding"
2566
+ ],
2567
+ "properties": {
2568
+ "generation": {
2569
+ "type": "integer"
2570
+ },
2571
+ "changeSet": {
2572
+ "$ref": "#/definitions/sha256Digest",
2573
+ "description": "The changeSetDigest of the change set that wrote this generation."
2574
+ },
2575
+ "phase": {
2576
+ "enum": [
2577
+ "setup",
2578
+ "apply"
2579
+ ]
2580
+ },
2581
+ "planDigest": {
2582
+ "$ref": "#/definitions/sha256Digest",
2583
+ "description": "The digest of the plan that change set applies."
2584
+ },
2585
+ "bundle": {
2586
+ "$ref": "#/definitions/sha256Digest",
2587
+ "description": "The bundle digest the change set was computed in."
2588
+ },
2589
+ "baseCommit": {
2590
+ "$ref": "#/definitions/commit",
2591
+ "description": "The default-branch commit the change set was computed against."
2592
+ },
2593
+ "binding": {
2594
+ "$ref": "#/definitions/binding"
2595
+ }
2596
+ }
2597
+ },
2598
+ "fileRow": {
2599
+ "type": "object",
2600
+ "additionalProperties": false,
2601
+ "required": [
2602
+ "path",
2603
+ "mode",
2604
+ "after",
2605
+ "changeSet"
2606
+ ],
2607
+ "properties": {
2608
+ "path": {
2609
+ "$ref": "#/definitions/safePath"
2610
+ },
2611
+ "mode": {
2612
+ "enum": [
2613
+ "100644",
2614
+ "120000"
2615
+ ],
2616
+ "description": "120000 for a discovery link, whose content is its target (code rule L5)."
2617
+ },
2618
+ "after": {
2619
+ "$ref": "#/definitions/sha256Digest",
2620
+ "description": "The content digest the flow wrote: sha256: and the hex SHA-256 of the file's bytes."
2621
+ },
2622
+ "changeSet": {
2623
+ "$ref": "#/definitions/sha256Digest"
2624
+ }
2625
+ }
2626
+ },
2627
+ "keyRow": {
2628
+ "type": "object",
2629
+ "additionalProperties": false,
2630
+ "required": [
2631
+ "file",
2632
+ "pointer",
2633
+ "value",
2634
+ "changeSet"
2635
+ ],
2636
+ "properties": {
2637
+ "file": {
2638
+ "const": "package.json"
2639
+ },
2640
+ "pointer": {
2641
+ "title": "a JSON pointer to one dependency entry, such as /devDependencies/@scope~1name",
2642
+ "type": "string",
2643
+ "pattern": "^/(?:dependencies|devDependencies)/@[a-z0-9][a-z0-9._-]*~1[a-z0-9][a-z0-9._-]*$"
2644
+ },
2645
+ "value": {
2646
+ "$ref": "#/definitions/exactVersion"
2647
+ },
2648
+ "changeSet": {
2649
+ "$ref": "#/definitions/sha256Digest"
2650
+ }
2651
+ }
2652
+ },
2653
+ "entryRow": {
2654
+ "description": "One entry the flow added to a list in a file the repository owns: a release-age exemption list, or the root vocabulary of the repository's Controller profile.",
2655
+ "oneOf": [
2656
+ {
2657
+ "type": "object",
2658
+ "additionalProperties": false,
2659
+ "required": [
2660
+ "file",
2661
+ "key",
2662
+ "value",
2663
+ "changeSet"
2664
+ ],
2665
+ "properties": {
2666
+ "file": {
2667
+ "const": "pnpm-workspace.yaml"
2668
+ },
2669
+ "key": {
2670
+ "const": "minimumReleaseAgeExclude"
2671
+ },
2672
+ "value": {
2673
+ "$ref": "#/definitions/scopeGlob"
2674
+ },
2675
+ "changeSet": {
2676
+ "$ref": "#/definitions/sha256Digest"
2677
+ }
2678
+ }
2679
+ },
2680
+ {
2681
+ "type": "object",
2682
+ "additionalProperties": false,
2683
+ "required": [
2684
+ "file",
2685
+ "key",
2686
+ "value",
2687
+ "changeSet"
2688
+ ],
2689
+ "properties": {
2690
+ "file": {
2691
+ "const": ".yarnrc.yml"
2692
+ },
2693
+ "key": {
2694
+ "const": "npmPreapprovedPackages"
2695
+ },
2696
+ "value": {
2697
+ "$ref": "#/definitions/scopeGlob"
2698
+ },
2699
+ "changeSet": {
2700
+ "$ref": "#/definitions/sha256Digest"
2701
+ }
2702
+ }
2703
+ },
2704
+ {
2705
+ "type": "object",
2706
+ "additionalProperties": false,
2707
+ "required": [
2708
+ "file",
2709
+ "key",
2710
+ "value",
2711
+ "changeSet"
2712
+ ],
2713
+ "properties": {
2714
+ "file": {
2715
+ "allOf": [
2716
+ {
2717
+ "$ref": "#/definitions/safePath"
2718
+ },
2719
+ {
2720
+ "$ref": "#/definitions/profileName"
2721
+ }
2722
+ ]
2723
+ },
2724
+ "key": {
2725
+ "const": "rootEntries"
2726
+ },
2727
+ "value": {
2728
+ "$ref": "#/definitions/rootEntryName"
2729
+ },
2730
+ "changeSet": {
2731
+ "$ref": "#/definitions/sha256Digest"
2732
+ }
2733
+ }
2734
+ }
2735
+ ]
2736
+ },
2737
+ "profileName": {
2738
+ "title": "a path whose last segment is repository-profile.json or repository-declaration.json, under none of the directories Controller's profile search skips (.git, node_modules, dist, build, .next, coverage, .turbo, .cache), and not under .github/workflows/, where the flow owns only clossys-* workflows",
2739
+ "type": "string",
2740
+ "pattern": "^(?!(?:[^/]*/)*(?:\\.git|node_modules|dist|build|\\.next|coverage|\\.turbo|\\.cache)/)(?!\\.github/workflows/)(?:[^/]+/)*repository-(?:profile|declaration)\\.json$"
2741
+ },
2742
+ "rootEntryName": {
2743
+ "title": "one direct-child name: 1 to 255 characters (code rules bound it to 255 UTF-16 code units), no / or \\, no control character, not . or .., and no leading or trailing whitespace",
2744
+ "type": "string",
2745
+ "pattern": "^(?!\\.\\.?$)(?=[^\\s])(?=[\\s\\S]*[^\\s]$)[^/\\\\\\u0000-\\u001f\\u007f]{1,255}$"
2746
+ },
2747
+ "scopeGlob": {
2748
+ "title": "a scope followed by /*, such as @scope/*",
2749
+ "type": "string",
2750
+ "pattern": "^@[a-z0-9][a-z0-9._-]*/\\*$"
2751
+ },
2752
+ "packageRow": {
2753
+ "type": "object",
2754
+ "additionalProperties": false,
2755
+ "required": [
2756
+ "planItem",
2757
+ "act",
2758
+ "name",
2759
+ "version",
2760
+ "integrity",
2761
+ "placement",
2762
+ "changeSet"
2763
+ ],
2764
+ "properties": {
2765
+ "planItem": {
2766
+ "$ref": "#/definitions/nonBlankString",
2767
+ "description": "repository.id, a colon and name (code rule L10)."
2768
+ },
2769
+ "act": {
2770
+ "enum": [
2771
+ "install",
2772
+ "pin-starter"
2773
+ ]
2774
+ },
2775
+ "name": {
2776
+ "$ref": "#/definitions/packageName"
2777
+ },
2778
+ "version": {
2779
+ "$ref": "#/definitions/exactVersion"
2780
+ },
2781
+ "integrity": {
2782
+ "$ref": "#/definitions/sha512Integrity"
2783
+ },
2784
+ "placement": {
2785
+ "$ref": "#/definitions/placement"
2786
+ },
2787
+ "changeSet": {
2788
+ "$ref": "#/definitions/sha256Digest",
2789
+ "description": "The change set that first recorded this exact act."
2790
+ }
2791
+ }
2792
+ },
2793
+ "deferredRow": {
2794
+ "type": "object",
2795
+ "additionalProperties": false,
2796
+ "required": [
2797
+ "planItem",
2798
+ "act",
2799
+ "name",
2800
+ "version",
2801
+ "integrity",
2802
+ "placement",
2803
+ "reason",
2804
+ "changeSet"
2805
+ ],
2806
+ "properties": {
2807
+ "planItem": {
2808
+ "$ref": "#/definitions/nonBlankString"
2809
+ },
2810
+ "act": {
2811
+ "const": "install",
2812
+ "description": "Only an install is deferred (repository-change-set.json code rule C10)."
2813
+ },
2814
+ "name": {
2815
+ "$ref": "#/definitions/packageName"
2816
+ },
2817
+ "version": {
2818
+ "$ref": "#/definitions/exactVersion"
2819
+ },
2820
+ "integrity": {
2821
+ "$ref": "#/definitions/sha512Integrity"
2822
+ },
2823
+ "placement": {
2824
+ "$ref": "#/definitions/placement"
2825
+ },
2826
+ "reason": {
2827
+ "enum": [
2828
+ "after-setup"
2829
+ ]
2830
+ },
2831
+ "changeSet": {
2832
+ "$ref": "#/definitions/sha256Digest",
2833
+ "description": "The setup set that deferred it (code rule L4)."
2834
+ }
2835
+ }
2836
+ }
2837
+ }
2838
+ },
2839
+ };
2840
+ //# sourceMappingURL=plan-contracts.generated.js.map