@clossys/launcher 0.3.1 → 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 (285) hide show
  1. package/README.md +1324 -60
  2. package/contracts/conversation-contract.md +2 -2
  3. package/contracts/product-ci-workflow.yml +74 -0
  4. package/contracts/repository-inventory.json +53 -0
  5. package/dist/admission-fixture.d.ts +168 -0
  6. package/dist/admission-fixture.d.ts.map +1 -0
  7. package/dist/admission-fixture.js +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 -4
  210. package/skeleton/README.md +14 -9
  211. package/skeleton/package.json +2 -1
  212. package/skill/SKILL.md +23 -7
  213. package/skill-catalogue/advisor/SKILL.md +59 -6
  214. package/skill-catalogue/architect/SKILL.md +2 -2
  215. package/skill-catalogue/bouncer/SKILL.md +2 -2
  216. package/skill-catalogue/builder/SKILL.md +2 -2
  217. package/skill-catalogue/butler/SKILL.md +2 -2
  218. package/skill-catalogue/controller/SKILL.md +2 -2
  219. package/skill-catalogue/customer/SKILL.md +2 -2
  220. package/skill-catalogue/designer/SKILL.md +4 -2
  221. package/skill-catalogue/giver/SKILL.md +2 -2
  222. package/skill-catalogue/influencer/SKILL.md +2 -2
  223. package/skill-catalogue/inspector/SKILL.md +2 -2
  224. package/skill-catalogue/integrator/SKILL.md +2 -2
  225. package/skill-catalogue/keeper/SKILL.md +2 -2
  226. package/skill-catalogue/launcher/SKILL.md +23 -7
  227. package/skill-catalogue/locksmith/SKILL.md +2 -2
  228. package/skill-catalogue/messenger/SKILL.md +2 -2
  229. package/skill-catalogue/observer/SKILL.md +2 -2
  230. package/skill-catalogue/publisher/SKILL.md +2 -2
  231. package/skill-catalogue/starter/SKILL.md +3 -2
  232. package/skill-catalogue/strategist/SKILL.md +12 -4
  233. package/skill-catalogue/writer/SKILL.md +2 -2
  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
package/src/host.ts CHANGED
@@ -58,9 +58,19 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
58
58
  return null;
59
59
  }
60
60
  },
61
+ readBytes: (path) => {
62
+ try {
63
+ return readFileSync(path);
64
+ } catch {
65
+ return null;
66
+ }
67
+ },
61
68
  writeText: (path, contents) => {
62
69
  writeFileSync(path, contents, "utf8");
63
70
  },
71
+ writeBytes: (path, contents) => {
72
+ writeFileSync(path, contents);
73
+ },
64
74
  mkdirp: (path) => {
65
75
  mkdirSync(path, { recursive: true });
66
76
  },
@@ -0,0 +1,51 @@
1
+ // The one rule for "is this the same GitHub repository" and "is this the
2
+ // same GitHub account" in Launcher (#1179). Every such decision -- the
3
+ // inventory's duplicate rule, a --repositories choice against the stored
4
+ // inventory, the appoint --inventory merge, the hub excluding itself from
5
+ // its own roster, matching a sibling checkout's origin, whether an id
6
+ // belongs to the hub's account, CLOSSYS_OWNER against the origin, and the
7
+ // drift report -- goes through the functions below and nothing else.
8
+ //
9
+ // GitHub compares owner and repository names without regard to letter case,
10
+ // and a bare inventory id names a repository of the hub's own owner, as
11
+ // Launcher's sibling resolution reads it. A folder path is never used as a
12
+ // repository's identity: on a case-insensitive file system two spellings of
13
+ // a path name the same folder, so a path comparison can disagree with the
14
+ // repository it holds.
15
+
16
+ /** An owner or repository name as GitHub compares it. */
17
+ function nameKey(name: string): string {
18
+ return name.toLowerCase();
19
+ }
20
+
21
+ /**
22
+ * The identity of a repository id: a bare id is qualified with the hub's
23
+ * owner when that owner is known, and the result is compared as GitHub
24
+ * compares names. Without an owner, a bare id stays bare.
25
+ */
26
+ export function inventoryKey(id: string, hubOwner?: string): string {
27
+ return nameKey(hubOwner !== undefined && !id.includes("/") ? `${hubOwner}/${id}` : id);
28
+ }
29
+
30
+ /** Whether two repository ids name the same repository. */
31
+ export function sameRepository(left: string, right: string, hubOwner?: string): boolean {
32
+ return inventoryKey(left, hubOwner) === inventoryKey(right, hubOwner);
33
+ }
34
+
35
+ /** Whether two owner names name the same GitHub account. */
36
+ export function sameOwner(left: string, right: string): boolean {
37
+ return nameKey(left) === nameKey(right);
38
+ }
39
+
40
+ /** Whether a repository id names a repository of `hubOwner`'s account (a bare id always does). */
41
+ export function belongsToOwner(id: string, hubOwner: string): boolean {
42
+ const slash = id.indexOf("/");
43
+ return slash === -1 || sameOwner(id.slice(0, slash), hubOwner);
44
+ }
45
+
46
+ /** The first spelling of each distinct owner, in order: case variants of one account are one owner. */
47
+ export function distinctOwners(owners: Iterable<string>): string[] {
48
+ const kept: string[] = [];
49
+ for (const owner of owners) if (!kept.some((existing) => sameOwner(existing, owner))) kept.push(owner);
50
+ return kept;
51
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  readInventoryRepositories,
15
15
  readLiveLauncherVersion,
16
16
  reportHubHealth,
17
+ validateInventoryDocument,
17
18
  CLOSSYS_DIR_REL,
18
19
  CLOSSYS_README_REL,
19
20
  DEFAULT_REPOSITORY_NAME,
@@ -24,17 +25,70 @@ export {
24
25
  WORKSPACE_INVENTORY_REL,
25
26
  WORKSPACE_MARKER_REL,
26
27
  } from "./core.js";
27
- export type { CloneMissingOutcome } from "./core.js";
28
+ export type { CloneMissingOutcome, InventoryValidation, PlanWorkspaceOptions } from "./core.js";
29
+ export type { InventoryReadOptions } from "./inventory-contract.js";
28
30
  export { runDoctorChecks, renderDoctorReport } from "./doctor.js";
29
- export { applyEngagementBrief, isPlanApproved, validateAdvisorPlan, validateEngagementBrief } from "./apply-plan.js";
30
- export type { AdvisorPlan, ApplyBriefResult, BlockerKind, EngagementBrief, EngagementBriefRole, PlanBlocker, PlanDecision, ValidationResult } from "./apply-plan.js";
31
+ export { applyEngagementBrief, approvedSubject, isPlanApproved, validateAdvisorPlan, validateEngagementBrief } from "./apply-plan.js";
32
+ export type {
33
+ AdvisorPlan, ApplyBriefResult, BlockerKind, EngagementBrief, EngagementBriefRole, EngagementContext, EngagementContextField, EngagementContextFieldId, GoalDirection, PlanBlocker,
34
+ PlanDecision, PlanKit, PlanPackageAct, PlanStaffing, ValidationResult,
35
+ } from "./apply-plan.js";
36
+ export { PLAN_DIGEST_EXCLUDED_FIELDS, canonicalDigest, canonicalJson, planDigest } from "./plan-digest.js";
37
+ export { CHANGE_SET_DIGEST_EXCLUDED_FIELDS, DERIVED_FILE_DIGEST_FIELDS, bundleDigest, changeSetDigest, changeSetDigestSubject } from "./change-set-digest.js";
38
+ export type { BundleDigestEntry } from "./change-set-digest.js";
39
+ export { AGENTS_GUIDE_PATH, AGENTS_GUIDE_TEXT, CLOSSYS_SKILL_PATTERNS, verifyAgentsGuide } from "./agents-guide.js";
40
+ export { validateApplyBundle, validateRepositoryChangeSet } from "./change-set-contract.js";
41
+ export type {
42
+ ApplyBundle, ApplyBundleRepository, ApplyCheck, ApplyCheckId, ApprovalBinding, ChangeSetDeferral, ChangeSetItem, ChangeSetPhase, ChangeSetRefusal, CheckVerdict, ContentDigest,
43
+ DependencyPlacement, DerivedFileChange, DiscoveryRoot, ExemptionSurfaceKind, FileChange, KeyChange, LedgerInvariant, LockfileName, PackageInvariant, PackageManagerKind,
44
+ PinnedPackage, RefusalReason, ReleaseAgeSurfaceKind, RepositoryChangeSet, RepositoryProfileObservation, RepositoryVisibility, RootEntryDeclaration, WholeFileChange,
45
+ WriteRecordSource,
46
+ } from "./change-set-contract.js";
47
+ export { ledgerSuccession, readInstalledLedger, renderInstalledLedger, serializeInstalledLedger, validateInstalledLedger } from "./ledger-contract.js";
48
+ export { reconcileWholeFile, trustInstalledLedger } from "./ledger-trust.js";
49
+ export type { LedgerTrust, LedgerTrustRule, PlanPackageActs, TrustInstalledLedgerOptions, WholeFileOutcome, WholeFileState } from "./ledger-trust.js";
50
+ export { BUNDLE_STORE_REL, CHANGE_SET_STORE_REL, readStoredApplyBundle, readStoredChangeSet, storeApplyBundle, storeChangeSet } from "./apply-store.js";
51
+ export { isRootEntryName, wouldViolateRootEntries } from "./root-entries.js";
52
+ export type { RootEntriesVerdict } from "./root-entries.js";
53
+ export { editReleaseAgeExemption, verifyReleaseAgeExemption } from "./release-age-edit.js";
54
+ export type { ReleaseAgeEdit, ReleaseAgeEditInput, ReleaseAgeEditRefusalReason, ReleaseAgeVerdict, ReleaseAgeVerifyInput } from "./release-age-edit.js";
55
+ export type { InstalledLedger, LedgerPackageIdentity, LedgerSuccession, LedgerViolation } from "./ledger-contract.js";
56
+ export { PUBLIC_PROBLEM_PLACEHOLDER, planApplyBundle, projectEngagementBrief, serializeEngagementBrief } from "./plan-bundle.js";
57
+ export type { PlanApplyBundleInputs, PlanApplyBundleResult, RepositoryObservation, SkippedRepositoryObservation } from "./plan-bundle.js";
58
+ export { observeRepository } from "./observe-repository.js";
59
+ export type { ObserveRepositoryInput, RepositoryObservationPorts } from "./observe-repository.js";
31
60
  export type { DoctorCheckHost, DoctorReport, DoctorStepId, DoctorStepResult } from "./doctor.js";
32
61
  export { checkCloudSessionBootstrap } from "./product-repository.js";
33
62
  export type { CloudBootstrapCheck, CloudBootstrapReport } from "./product-repository.js";
63
+ // Setup templates: pure renderers for the files a setup change writes.
64
+ export {
65
+ renderSetupTemplate,
66
+ renderStarterRequest,
67
+ renderAdoptionDecisionWorkflow,
68
+ renderProductCiWorkflow,
69
+ STARTER_PIN_RANGE,
70
+ } from "./setup-templates.js";
71
+ export type {
72
+ SetupPackageManager,
73
+ StarterPinInput,
74
+ StarterRequestInput,
75
+ TemplateFile,
76
+ TemplateRefusal,
77
+ TemplateResult,
78
+ } from "./setup-templates.js";
79
+ export {
80
+ renderSnapshotCollector,
81
+ renderAdoptionEvidenceWorkflow,
82
+ renderPathScopeScript,
83
+ renderPathScopeWorkflow,
84
+ OWNED_PATH_PATTERNS,
85
+ } from "./setup-template-scripts.js";
34
86
  export { reportInventoryDrift } from "./inventory-adoption.js";
35
87
  export type { ExternalInventoryDeclaration, InventoryDriftReport } from "./inventory-adoption.js";
36
88
  export { detectLinkedHosts, parseHostRecord, serializeHostRecord, HOSTS_REL } from "./hosts.js";
37
89
  export type { DiscoveredHost, HostRecord } from "./hosts.js";
90
+ export { readChangeSetMarker, renderPullRequest } from "./pull-request-body.js";
91
+ export type { PullRequestRefusal, PullRequestRefusalReason, PullRequestText, RenderPullRequestInput } from "./pull-request-body.js";
38
92
  export { parsePreferences, readHostModelProfile, resolveModelForTier } from "./model-profile.js";
39
93
  export type {
40
94
  BudgetPreference,
@@ -47,10 +101,14 @@ export type {
47
101
  } from "./model-profile.js";
48
102
  export type {
49
103
  ApplyWorkspaceOptions,
104
+ ChosenInventory,
50
105
  CommandResult,
51
106
  CwdObservation,
52
107
  DependencyBucket,
108
+ EngineInstallFinding,
109
+ EnginePinChange,
53
110
  HubDocument,
111
+ HubEnginePin,
54
112
  HubHealthReport,
55
113
  HubMigrationState,
56
114
  InventoryObservation,
@@ -69,3 +127,14 @@ export type {
69
127
  WorkspaceRefusal,
70
128
  WorkspaceState,
71
129
  } from "./types.js";
130
+ export {
131
+ PROVENANCE_CHECK_BIN,
132
+ PROVENANCE_CHECK_MAX_BUFFER,
133
+ PROVENANCE_CHECK_TIMEOUT_MS,
134
+ checkSetProvenance,
135
+ registrySnapshotDigest,
136
+ } from "./provenance-gate.js";
137
+ export type { ProvenanceGateInput, ProvenanceGatePorts } from "./provenance-gate.js";
138
+
139
+ export { decideSetBinding, planPackagesFor, readHubAuthority } from "./admission.js";
140
+ export type { AdmissionDecision, AdmissionRefusal, HubAuthority, ReadinessRunner } from "./admission.js";
@@ -4,6 +4,9 @@
4
4
  // it. Launcher writes only what that inventory lacks (a later apply step,
5
5
  // not this module) and reports drift instead of silently merging.
6
6
 
7
+ import { readContractDocument } from "./generated/contract-schema.generated.js";
8
+ import { sameRepository } from "./identity.js";
9
+ import { validateInventoryDocument } from "./inventory-contract.js";
7
10
  import type { WorkspaceHost } from "./types.js";
8
11
 
9
12
  export interface ExternalInventoryDeclaration {
@@ -11,33 +14,69 @@ export interface ExternalInventoryDeclaration {
11
14
  readonly shape: "foundry" | "custom";
12
15
  }
13
16
 
17
+ /**
18
+ * A count plus each disagreeing or agreeing id's position, never the ids
19
+ * themselves: `externalInventory` at `declaration.path` and Launcher's own
20
+ * stored inventory are both document content a hub declares or writes, and
21
+ * this whole report reaches `formatHubHealth`'s JSON-dumped `health:` line
22
+ * on every resume of a hub that declares `externalInventory`, so an id kept
23
+ * here would still reach that message (#1179). A position names the array
24
+ * it indexes into: `externalInventory[<i>]` for `declaration.path`'s
25
+ * `repositories[<i>].id`, `repositories[<j>]` for the hub's own stored
26
+ * inventory's `repositories[<j>].id`.
27
+ */
28
+ export interface InventoryDriftPositions {
29
+ readonly count: number;
30
+ readonly positions: readonly string[];
31
+ }
32
+
14
33
  export interface InventoryDriftReport {
15
34
  readonly status: "no-external-source" | "reconciled" | "indeterminate";
16
- readonly externalOnly: readonly string[];
17
- readonly launcherOnly: readonly string[];
18
- readonly agreeing: readonly string[];
35
+ readonly externalOnly: InventoryDriftPositions;
36
+ readonly launcherOnly: InventoryDriftPositions;
37
+ readonly agreeing: InventoryDriftPositions;
19
38
  readonly note?: string;
20
39
  }
21
40
 
41
+ const NO_DRIFT: InventoryDriftPositions = { count: 0, positions: [] };
42
+
22
43
  function isRecord(value: unknown): value is Record<string, unknown> {
23
44
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
45
  }
25
46
 
26
- /** Reads a foundry-shaped inventory document's repository ids. Malformed or missing is null, never []. */
27
- function readForeignIds(host: WorkspaceHost, path: string): readonly string[] | null {
28
- const raw = host.readText(path);
47
+ /** One `externalInventory` entry with a usable id, paired with its own index in that document's `repositories` array. */
48
+ interface ForeignId {
49
+ readonly id: string;
50
+ readonly index: number;
51
+ }
52
+
53
+ /**
54
+ * Reads a foundry-shaped inventory document's repository ids. Malformed or
55
+ * missing is null, never []. The file is read as bytes by the shared strict
56
+ * reader, so bytes that are not valid UTF-8, a repeated key, or a byte order
57
+ * mark make it unreadable rather than silently repaired (#1179). Its shape
58
+ * is read leniently on purpose: an external source is not Launcher's own
59
+ * document -- a non-object entry, or one with a missing, blank or non-string
60
+ * `id`, is skipped rather than refusing the whole document. Skipped entries
61
+ * still occupy a slot in `repositories`, so each kept id is paired with its
62
+ * own original array index, never a count of ids kept so far: the file's
63
+ * fourth entry (index 3) must be reported as `externalInventory[3]` even
64
+ * when it is the second entry actually kept (#1179).
65
+ */
66
+ function readForeignIds(host: WorkspaceHost, path: string): readonly ForeignId[] | null {
67
+ const raw = host.readBytes(path);
29
68
  if (raw === null) return null;
30
69
  let parsed: unknown;
31
70
  try {
32
- parsed = JSON.parse(raw);
71
+ parsed = readContractDocument(raw);
33
72
  } catch {
34
73
  return null;
35
74
  }
36
75
  if (!isRecord(parsed) || parsed.schemaVersion !== 1 || !Array.isArray(parsed.repositories)) return null;
37
- const ids: string[] = [];
38
- for (const entry of parsed.repositories) {
39
- if (isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "") ids.push(entry.id);
40
- }
76
+ const ids: ForeignId[] = [];
77
+ parsed.repositories.forEach((entry, index) => {
78
+ if (isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "") ids.push({ id: entry.id, index });
79
+ });
41
80
  return ids;
42
81
  }
43
82
 
@@ -45,41 +84,80 @@ function readForeignIds(host: WorkspaceHost, path: string): readonly string[] |
45
84
  * Compares the declared external inventory against the launcher-written
46
85
  * inventory at `directory/launcherInventoryRelPath`. Read-only -- callers
47
86
  * decide whether and how to write the reconciled set, as an explicit,
48
- * approved apply step (same #1045 pattern as clone-on-approval).
87
+ * approved apply step (same #1045 pattern as clone-on-approval). Ids are
88
+ * compared with `sameRepository()` (identity.ts): case-insensitively, and,
89
+ * when `hubOwner` is given, with a bare id read as that owner's repository.
90
+ * The hub's own inventory is read by `validateInventoryDocument()`; when it
91
+ * is present but invalid the report is indeterminate.
49
92
  */
50
93
  export function reportInventoryDrift(
51
94
  host: WorkspaceHost,
52
95
  directory: string,
53
96
  declaration: ExternalInventoryDeclaration | undefined,
54
97
  launcherInventoryRelPath: string,
98
+ hubOwner?: string,
55
99
  ): InventoryDriftReport {
56
100
  if (declaration === undefined) {
57
- return { status: "no-external-source", externalOnly: [], launcherOnly: [], agreeing: [] };
101
+ return { status: "no-external-source", externalOnly: NO_DRIFT, launcherOnly: NO_DRIFT, agreeing: NO_DRIFT };
58
102
  }
59
103
  if (declaration.shape === "custom") {
60
104
  return {
61
105
  status: "indeterminate",
62
- externalOnly: [],
63
- launcherOnly: [],
64
- agreeing: [],
65
- note: `externalInventory at ${declaration.path} declares shape "custom"; launcher has no mapping for a non-foundry inventory shape yet and will not guess one. Reconcile by hand or file the mapping gap.`,
106
+ externalOnly: NO_DRIFT,
107
+ launcherOnly: NO_DRIFT,
108
+ agreeing: NO_DRIFT,
109
+ note: `the declared \`externalInventory\` declares a shape launcher has no mapping for yet; launcher will not guess one. Reconcile by hand or file the mapping gap.`,
66
110
  };
67
111
  }
68
- const externalIds = readForeignIds(host, declaration.path);
69
- if (externalIds === null) {
112
+ const externalEntries = readForeignIds(host, declaration.path);
113
+ if (externalEntries === null) {
70
114
  return {
71
115
  status: "indeterminate",
72
- externalOnly: [],
73
- launcherOnly: [],
74
- agreeing: [],
75
- note: `externalInventory at ${declaration.path} could not be read as a populated schemaVersion:1 inventory document.`,
116
+ externalOnly: NO_DRIFT,
117
+ launcherOnly: NO_DRIFT,
118
+ agreeing: NO_DRIFT,
119
+ note: `the declared \`externalInventory\` could not be read as a populated schemaVersion:1 inventory document.`,
76
120
  };
77
121
  }
78
- const launcherIds = readForeignIds(host, `${directory}/${launcherInventoryRelPath}`) ?? [];
79
- const externalSet = new Set(externalIds);
80
- const launcherSet = new Set(launcherIds);
81
- const externalOnly = externalIds.filter((id) => !launcherSet.has(id));
82
- const launcherOnly = launcherIds.filter((id) => !externalSet.has(id));
83
- const agreeing = externalIds.filter((id) => launcherSet.has(id));
122
+ // Launcher's own inventory is read by its own strict reader. A missing one
123
+ // lists nothing; one that is present but cannot be read makes the
124
+ // comparison indeterminate, never a comparison against an empty list.
125
+ const launcherPath = `${directory}/${launcherInventoryRelPath}`;
126
+ const launcherBytes = host.readBytes(launcherPath);
127
+ let launcherIds: readonly string[] = [];
128
+ if (launcherBytes !== null) {
129
+ const launcher = validateInventoryDocument(launcherBytes, hubOwner === undefined ? {} : { hubOwner });
130
+ if (!launcher.valid) {
131
+ return {
132
+ status: "indeterminate",
133
+ externalOnly: NO_DRIFT,
134
+ launcherOnly: NO_DRIFT,
135
+ agreeing: NO_DRIFT,
136
+ note: `the hub's own inventory ${launcher.reason}, so it cannot be compared with the declared \`externalInventory\`.`,
137
+ };
138
+ }
139
+ launcherIds = launcher.ids;
140
+ }
141
+ // One repository identity, as everywhere in Launcher (identity.ts): a bare id is the hub owner's, and case is ignored.
142
+ const agrees = (id: string, others: readonly string[]) => others.some((other) => sameRepository(id, other, hubOwner));
143
+ const externalIds = externalEntries.map((entry) => entry.id);
144
+ // Every position below is the id's own index in the array a caller can already read
145
+ // (externalEntries.index is `externalInventory`'s document's own `repositories` index --
146
+ // readForeignIds() pairs it with the id precisely because that array is filtered, so a
147
+ // count of ids kept so far would not match the file (#1179); launcherIds is never
148
+ // filtered, so its own array index already is that position) -- never the id, per
149
+ // InventoryDriftPositions' own doc comment.
150
+ const externalOnly = externalEntries.reduce<{ count: number; positions: string[] }>(
151
+ (acc, entry) => (agrees(entry.id, launcherIds) ? acc : { count: acc.count + 1, positions: [...acc.positions, `externalInventory[${entry.index}]`] }),
152
+ { count: 0, positions: [] },
153
+ );
154
+ const launcherOnly = launcherIds.reduce<{ count: number; positions: string[] }>(
155
+ (acc, id, index) => (agrees(id, externalIds) ? acc : { count: acc.count + 1, positions: [...acc.positions, `repositories[${index}]`] }),
156
+ { count: 0, positions: [] },
157
+ );
158
+ const agreeing = externalEntries.reduce<{ count: number; positions: string[] }>(
159
+ (acc, entry) => (agrees(entry.id, launcherIds) ? { count: acc.count + 1, positions: [...acc.positions, `externalInventory[${entry.index}]`] } : acc),
160
+ { count: 0, positions: [] },
161
+ );
84
162
  return { status: "reconciled", externalOnly, launcherOnly, agreeing };
85
163
  }
@@ -0,0 +1,172 @@
1
+ // Writing the hub inventory from the repositories a founder chose on
2
+ // Advisor's repository-choice card (issue #1179), so a founder never
3
+ // hand-writes JSON. `launcher --repositories` is the command; this module
4
+ // decides what that command may write, and never writes anything itself.
5
+ //
6
+ // The chosen ids and the document built from them are validated against
7
+ // docs/contracts/repository-inventory.json (in the public repository;
8
+ // that exact path does not ship in this package, but this package's
9
+ // build packs and ships its own copy of the contract) through the shared
10
+ // contract checker (./inventory-contract.ts) -- the same check every later read of
11
+ // the stored inventory applies, so what this writes, Launcher reads back.
12
+ //
13
+ // An inventory already on disk that names a different set of repositories
14
+ // is never merged into or overwritten silently: the choice is refused, with
15
+ // the difference in counts and positions, unless the caller passes an
16
+ // explicit replace approval (`--replace-inventory`).
17
+ //
18
+ // Every message this module builds -- the refusal and the success line
19
+ // alike -- names a repository only by its position, never by its id: a
20
+ // stored inventory file, and the `--repositories` argument, are both
21
+ // document-shaped input an agent may relay verbatim, and a repository id is
22
+ // exactly the kind of short string a hostile entry could shape as
23
+ // prompt-injection text (see ./inventory-contract.ts's own header). The
24
+ // caller looks the reported position up in its own copy of the inventory
25
+ // file or its own `--repositories` argument to learn which repository it
26
+ // names.
27
+
28
+ import { inventoryKey } from "./identity.js";
29
+ import { readInventoryDocument, renderInventoryDocument, validateInventoryDocument, validateInventoryValue, type InventoryEntry } from "./inventory-contract.js";
30
+ import type { ChosenInventory } from "./types.js";
31
+
32
+ export type ChosenInventoryResolution =
33
+ | { readonly kind: "resolved"; readonly chosen: ChosenInventory }
34
+ | { readonly kind: "refuse"; readonly message: string };
35
+
36
+ /** The command a refusal names for an explicit replace approval. */
37
+ export const REPLACE_INVENTORY_FLAG = "--replace-inventory";
38
+
39
+ function repositories(count: number): string {
40
+ return `${count} ${count === 1 ? "repository" : "repositories"}`;
41
+ }
42
+
43
+ /** Added ids are named by their position in the `--repositories` argument, never by the id itself. */
44
+ function listAddedPositions(positions: readonly number[]): string {
45
+ return positions.length === 0 ? "" : ` (${positions.map((index) => `--repositories[${index}]`).join(", ")})`;
46
+ }
47
+
48
+ /**
49
+ * Removed ids are named by their position in an inventory's `repositories`
50
+ * array, never by the id itself. `label` distinguishes which file that
51
+ * position indexes into: `"stored inventory"` for the refusal, where the
52
+ * file on disk is still the one the positions were computed against, and
53
+ * `"replaced inventory"` for the success line after `--replace-inventory`
54
+ * writes -- by the time that line prints, the file on disk is already the
55
+ * new one, so reusing "stored inventory" there would point a reader at the
56
+ * wrong document (#1179).
57
+ */
58
+ function listRemovedPositions(positions: readonly number[], label: "stored inventory" | "replaced inventory"): string {
59
+ return positions.length === 0 ? "" : ` (${positions.map((index) => `repositories[${index}] in the ${label}`).join(", ")})`;
60
+ }
61
+
62
+ /**
63
+ * Decides what `launcher --repositories` writes into a hub whose stored
64
+ * inventory is `onDisk` -- the file's exact bytes, or `null` when there is
65
+ * none.
66
+ *
67
+ * Every comparison uses `inventoryKey(id, hubOwner)`: a bare id names a
68
+ * repository of the hub's own owner, and letter case is ignored.
69
+ *
70
+ * - The chosen ids must form a valid inventory on their own: at least one
71
+ * id, each one satisfying the contract's repository id rule, and no two
72
+ * naming the same repository. A refusal names positions only.
73
+ * - The stored inventory is read by the same strict reader, with the same
74
+ * owner: one that fails its contract, including one listing a repository
75
+ * twice (for example `app` and `<hubOwner>/app`), is replaced only with
76
+ * `replace`, and then with the chosen ids alone. It is never merged.
77
+ * - No inventory, or an empty one: the chosen repositories are written.
78
+ * - An inventory naming exactly the same repositories: nothing is written.
79
+ * - An inventory naming a different set: refused, reporting how many
80
+ * repositories each side has and the positions that would be added and
81
+ * removed (never the ids themselves), unless `replace` is true. A
82
+ * replacement keeps each kept repository's existing entry as it was (its
83
+ * `packages` included).
84
+ *
85
+ * The document to write is checked again by the same reader every later
86
+ * read uses, before it is returned.
87
+ */
88
+ export function resolveChosenInventory(
89
+ onDisk: Uint8Array | string | null,
90
+ chosenIds: readonly string[],
91
+ hubOwner: string,
92
+ replace: boolean,
93
+ ): ChosenInventoryResolution {
94
+ if (chosenIds.length === 0) {
95
+ return { kind: "refuse", message: "--repositories must name at least one repository" };
96
+ }
97
+ const chosenCheck = validateInventoryValue({ schemaVersion: 1, repositories: chosenIds.map((id) => ({ id })) }, { hubOwner });
98
+ if (!chosenCheck.valid) return { kind: "refuse", message: `--repositories is not a valid inventory: ${chosenCheck.reason}` };
99
+
100
+ let entries: InventoryEntry[];
101
+ let added: readonly string[] = chosenIds;
102
+ let removed: readonly string[] = [];
103
+ let addedPositions: readonly number[] = chosenIds.map((_, index) => index);
104
+ let removedPositions: readonly number[] = [];
105
+ let replaced: Extract<ChosenInventory, { kind: "write" }>["replaced"] = "nothing";
106
+ let previousCount = 0;
107
+ if (onDisk === null) {
108
+ entries = chosenIds.map((id) => ({ id }));
109
+ } else {
110
+ const stored = readInventoryDocument(onDisk, { hubOwner });
111
+ if (!stored.valid) {
112
+ if (!replace) {
113
+ return {
114
+ kind: "refuse",
115
+ message: `the hub inventory ${stored.reason}; to replace it with the ${repositories(chosenIds.length)} chosen, run again with ${REPLACE_INVENTORY_FLAG}`,
116
+ };
117
+ }
118
+ entries = chosenIds.map((id) => ({ id }));
119
+ replaced = "invalid";
120
+ } else {
121
+ previousCount = stored.ids.length;
122
+ // Keys are unique on both sides: the reader and the chosen-id check above both refuse two ids with one key.
123
+ const storedByKey = new Map(stored.entries.map((entry) => [inventoryKey(entry.id, hubOwner), entry] as const));
124
+ const chosenKeys = new Set(chosenIds.map((id) => inventoryKey(id, hubOwner)));
125
+ added = chosenIds.filter((id) => !storedByKey.has(inventoryKey(id, hubOwner)));
126
+ removed = stored.ids.filter((id) => !chosenKeys.has(inventoryKey(id, hubOwner)));
127
+ // Positions, not ids: `added` indexes into the --repositories argument the caller passed; `removed` indexes into the stored inventory's own `repositories` array.
128
+ addedPositions = added.map((id) => chosenIds.indexOf(id));
129
+ removedPositions = removed.map((id) => stored.ids.indexOf(id));
130
+ if (stored.ids.length > 0 && added.length === 0 && removed.length === 0) {
131
+ return { kind: "resolved", chosen: { kind: "unchanged", count: stored.ids.length } };
132
+ }
133
+ if (stored.ids.length > 0) {
134
+ if (!replace) {
135
+ return {
136
+ kind: "refuse",
137
+ message:
138
+ `the hub inventory already lists ${repositories(stored.ids.length)} and the choice has ${chosenIds.length}: ` +
139
+ `${added.length} to add${listAddedPositions(addedPositions)}, ${removed.length} to remove${listRemovedPositions(removedPositions, "stored inventory")}. ` +
140
+ `Launcher never merges or overwrites an inventory silently; to replace it with the choice, run again with ${REPLACE_INVENTORY_FLAG}`,
141
+ };
142
+ }
143
+ replaced = "differing";
144
+ }
145
+ entries = chosenIds.map((id) => storedByKey.get(inventoryKey(id, hubOwner)) ?? { id });
146
+ }
147
+ }
148
+
149
+ const document = renderInventoryDocument(entries);
150
+ const written = validateInventoryDocument(document, { hubOwner });
151
+ if (!written.valid) return { kind: "refuse", message: `the inventory to write ${written.reason}` };
152
+ return {
153
+ kind: "resolved",
154
+ chosen: { kind: "write", document, count: written.ids.length, previousCount, added, removed, addedPositions, removedPositions, replaced },
155
+ };
156
+ }
157
+
158
+ /** One line for the apply message saying what happened to the inventory. */
159
+ export function describeChosenInventory(chosen: ChosenInventory): string {
160
+ if (chosen.kind === "unchanged") {
161
+ return `inventory: unchanged -- it already lists the ${repositories(chosen.count)} you chose`;
162
+ }
163
+ const count = repositories(chosen.count);
164
+ if (chosen.replaced === "invalid") return `inventory: replaced one that failed its contract with the ${count} you chose`;
165
+ if (chosen.replaced === "differing") {
166
+ return (
167
+ `inventory: replaced ${chosen.previousCount} with the ${count} you chose -- ` +
168
+ `${chosen.added.length} added${listAddedPositions(chosen.addedPositions)}, ${chosen.removed.length} removed${listRemovedPositions(chosen.removedPositions, "replaced inventory")}`
169
+ );
170
+ }
171
+ return `inventory: wrote the ${count} you chose`;
172
+ }