@clossys/launcher 0.1.5 → 0.3.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 (101) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +209 -20
  3. package/contracts/conversation-contract.md +40 -0
  4. package/dist/apply-plan-cli.d.ts +7 -0
  5. package/dist/apply-plan-cli.d.ts.map +1 -0
  6. package/dist/apply-plan-cli.js +96 -0
  7. package/dist/apply-plan-cli.js.map +1 -0
  8. package/dist/apply-plan.d.ts +79 -0
  9. package/dist/apply-plan.d.ts.map +1 -0
  10. package/dist/apply-plan.js +129 -0
  11. package/dist/apply-plan.js.map +1 -0
  12. package/dist/check-cli.d.ts.map +1 -1
  13. package/dist/check-cli.js +3 -0
  14. package/dist/check-cli.js.map +1 -1
  15. package/dist/cli.d.ts +2 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +37 -11
  18. package/dist/cli.js.map +1 -1
  19. package/dist/contract.d.ts +28 -0
  20. package/dist/contract.d.ts.map +1 -0
  21. package/dist/contract.js +78 -0
  22. package/dist/contract.js.map +1 -0
  23. package/dist/core.d.ts +38 -6
  24. package/dist/core.d.ts.map +1 -1
  25. package/dist/core.js +289 -42
  26. package/dist/core.js.map +1 -1
  27. package/dist/doctor-cli.d.ts +4 -0
  28. package/dist/doctor-cli.d.ts.map +1 -0
  29. package/dist/doctor-cli.js +32 -0
  30. package/dist/doctor-cli.js.map +1 -0
  31. package/dist/doctor.d.ts +28 -0
  32. package/dist/doctor.d.ts.map +1 -0
  33. package/dist/doctor.js +68 -0
  34. package/dist/doctor.js.map +1 -0
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +3 -0
  37. package/dist/host.js.map +1 -1
  38. package/dist/hosts.d.ts +14 -0
  39. package/dist/hosts.d.ts.map +1 -0
  40. package/dist/hosts.js +61 -0
  41. package/dist/hosts.js.map +1 -0
  42. package/dist/index.d.ts +15 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +7 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/inventory-adoption.d.ts +20 -0
  47. package/dist/inventory-adoption.d.ts.map +1 -0
  48. package/dist/inventory-adoption.js +67 -0
  49. package/dist/inventory-adoption.js.map +1 -0
  50. package/dist/manifest.d.ts +20 -0
  51. package/dist/manifest.d.ts.map +1 -0
  52. package/dist/manifest.js +106 -0
  53. package/dist/manifest.js.map +1 -0
  54. package/dist/model-profile.d.ts +46 -0
  55. package/dist/model-profile.d.ts.map +1 -0
  56. package/dist/model-profile.js +98 -0
  57. package/dist/model-profile.js.map +1 -0
  58. package/dist/product-repository.d.ts +26 -0
  59. package/dist/product-repository.d.ts.map +1 -0
  60. package/dist/product-repository.js +49 -0
  61. package/dist/product-repository.js.map +1 -0
  62. package/dist/skills.d.ts +20 -1
  63. package/dist/skills.d.ts.map +1 -1
  64. package/dist/skills.js +98 -7
  65. package/dist/skills.js.map +1 -1
  66. package/dist/types.d.ts +57 -1
  67. package/dist/types.d.ts.map +1 -1
  68. package/model-profiles/claude-code.json +10 -0
  69. package/model-profiles/codex.json +10 -0
  70. package/model-profiles/cursor.json +10 -0
  71. package/package.json +9 -4
  72. package/skeleton/README.md +5 -0
  73. package/skeleton/package.json +1 -1
  74. package/skill/SKILL.md +53 -0
  75. package/skill-catalogue/advisor/SKILL.md +3 -1
  76. package/skill-catalogue/controller/SKILL.md +8 -0
  77. package/skill-catalogue/customer/SKILL.md +92 -0
  78. package/skill-catalogue/designer/SKILL.md +18 -2
  79. package/skill-catalogue/inspector/SKILL.md +1 -1
  80. package/skill-catalogue/publisher/SKILL.md +17 -3
  81. package/skill-catalogue/strategist/SKILL.md +37 -3
  82. package/skill-catalogue/writer/SKILL.md +7 -2
  83. package/src/apply-plan-cli.ts +94 -0
  84. package/src/apply-plan.ts +172 -0
  85. package/src/check-cli.ts +3 -0
  86. package/src/cli.ts +45 -10
  87. package/src/contract.ts +81 -0
  88. package/src/core.ts +337 -37
  89. package/src/doctor-cli.ts +33 -0
  90. package/src/doctor.ts +145 -0
  91. package/src/host.ts +3 -0
  92. package/src/hosts.ts +79 -0
  93. package/src/index.ts +33 -0
  94. package/src/inventory-adoption.ts +85 -0
  95. package/src/manifest.ts +103 -0
  96. package/src/model-profile.ts +148 -0
  97. package/src/product-repository.ts +73 -0
  98. package/src/skills.ts +113 -8
  99. package/src/types.ts +58 -1
  100. /package/skeleton/{.clossys → clossys/.state}/inventory.json +0 -0
  101. /package/skeleton/{.clossys → clossys/.state}/workspace.json +0 -0
@@ -0,0 +1,73 @@
1
+ import { join } from "node:path";
2
+ import type { WorkspaceHost } from "./types.js";
3
+
4
+ /**
5
+ * One check from this repository's own docs/contracts/product-repository-layout.json `cloudSessionBootstrap` list (that contract file does not ship in the published package).
6
+ * Read-only: verifies a product repository has what a cloud agent session
7
+ * (browser plus GitHub only, no local setup) needs to install and run the
8
+ * team, without foundry's own heavy governance gates.
9
+ */
10
+ export interface CloudBootstrapCheck {
11
+ readonly id: "package-manifest" | "agents-pointer" | "hub-marker";
12
+ readonly label: string;
13
+ readonly satisfied: boolean;
14
+ readonly note?: string;
15
+ }
16
+
17
+ export interface CloudBootstrapReport {
18
+ readonly schemaVersion: 1;
19
+ readonly checks: readonly CloudBootstrapCheck[];
20
+ readonly ready: boolean;
21
+ }
22
+
23
+ const PACKAGE_JSON_REL = "package.json";
24
+ const PACKAGE_LOCK_REL = "package-lock.json";
25
+ const AGENTS_MD_REL = "AGENTS.md";
26
+ const HUB_MARKER_REL = join("clossys", ".state", "workspace.json");
27
+
28
+ /**
29
+ * Runs the three product-repository-layout.json cloudSessionBootstrap checks
30
+ * against `directory`. Never mutates anything, never runs `npm ci` itself
31
+ * -- it checks that the manifest and lockfile are BOTH present, which is
32
+ * the precondition a real `npm ci` needs, not a substitute for running it.
33
+ */
34
+ export function checkCloudSessionBootstrap(host: WorkspaceHost, directory: string): CloudBootstrapReport {
35
+ const hasManifest = host.exists(join(directory, PACKAGE_JSON_REL));
36
+ const hasLock = host.exists(join(directory, PACKAGE_LOCK_REL));
37
+ const manifestCheck: CloudBootstrapCheck = {
38
+ id: "package-manifest",
39
+ label: "package.json and package-lock.json are both present",
40
+ satisfied: hasManifest && hasLock,
41
+ ...(hasManifest && hasLock
42
+ ? {}
43
+ : {
44
+ note: !hasManifest && !hasLock
45
+ ? "Neither package.json nor package-lock.json exists yet."
46
+ : !hasManifest
47
+ ? "package-lock.json exists but package.json does not."
48
+ : "package.json exists but package-lock.json does not -- npm ci needs both.",
49
+ }),
50
+ };
51
+
52
+ const agentsRaw = host.readText(join(directory, AGENTS_MD_REL));
53
+ const agentsPointsAtClossys = agentsRaw !== null && agentsRaw.includes("clossys/");
54
+ const agentsCheck: CloudBootstrapCheck = {
55
+ id: "agents-pointer",
56
+ label: "AGENTS.md exists at the repository root and points at clossys/",
57
+ satisfied: agentsPointsAtClossys,
58
+ ...(agentsPointsAtClossys
59
+ ? {}
60
+ : { note: agentsRaw === null ? "AGENTS.md does not exist at the repository root." : "AGENTS.md exists but does not mention clossys/." }),
61
+ };
62
+
63
+ const hasHubMarker = host.exists(join(directory, HUB_MARKER_REL));
64
+ const markerCheck: CloudBootstrapCheck = {
65
+ id: "hub-marker",
66
+ label: "clossys/.state/workspace.json (the hub marker) is present",
67
+ satisfied: hasHubMarker,
68
+ ...(hasHubMarker ? {} : { note: "clossys/.state/workspace.json does not exist yet." }),
69
+ };
70
+
71
+ const checks = [manifestCheck, agentsCheck, markerCheck];
72
+ return { schemaVersion: 1, checks, ready: checks.every((check) => check.satisfied) };
73
+ }
package/src/skills.ts CHANGED
@@ -1,18 +1,27 @@
1
1
  import { dirname, join, resolve, sep } from "node:path";
2
- import type { WorkspaceHost } from "./types.js";
2
+ import { extractContractBlock, injectContract } from "./contract.js";
3
+ import { parseSkillManifest, readInstalledVersion, serializeSkillManifest, sha256Hex } from "./manifest.js";
4
+ import type { SkillManifestEntry, WorkspaceHost } from "./types.js";
3
5
 
4
6
  export interface SkillCompositionResult {
5
7
  readonly composed: readonly string[];
6
8
  readonly skipped: readonly { readonly packageDir: string; readonly note: string }[];
9
+ readonly rosterTargets?: readonly string[];
10
+ readonly rosterSkipped?: readonly { readonly inventoryId: string; readonly note: string }[];
11
+ /** Skills pruned this run: present in the directory's previous manifest, absent from this run's composed set. */
12
+ readonly retired: readonly string[];
7
13
  }
8
14
 
9
15
  export interface ComposeSkillsOptions {
10
16
  readonly skillCatalogueRoot?: string;
11
17
  readonly launcherPackageRoot: string;
18
+ /** Overrides where the packed conversation contract is read from (tests). */
19
+ readonly contractPath?: string;
12
20
  }
13
21
 
14
- const DISCOVERY_PREFIXES = [".cursor/skills", ".claude/skills"] as const;
22
+ export const DISCOVERY_PREFIXES = [".cursor/skills", ".claude/skills"] as const;
15
23
  const AGENTS_SKILLS_REL = join(".agents", "skills");
24
+ export const SKILLS_MANIFEST_REL = join("clossys", ".state", "skills.json");
16
25
 
17
26
  function containedPath(root: string, relativePath: string): string {
18
27
  const resolved = resolve(root, relativePath);
@@ -33,14 +42,63 @@ function skillSourceCandidates(packageDir: string, options: ComposeSkillsOptions
33
42
  return paths;
34
43
  }
35
44
 
36
- function readSkillBody(host: WorkspaceHost, packageDir: string, options: ComposeSkillsOptions): string | null {
45
+ function installedSkillPath(composeTargetDirectory: string, packageDir: string): string {
46
+ return join(composeTargetDirectory, "node_modules", "@clossys", packageDir, "skill", "SKILL.md");
47
+ }
48
+
49
+ function readSkillBody(
50
+ host: WorkspaceHost,
51
+ packageDir: string,
52
+ options: ComposeSkillsOptions,
53
+ composeTargetDirectory: string,
54
+ ): { body: string; source: "installed" | "catalogue" } | null {
55
+ const installed = host.readText(installedSkillPath(composeTargetDirectory, packageDir));
56
+ if (installed !== null) return { body: installed, source: "installed" };
37
57
  for (const path of skillSourceCandidates(packageDir, options)) {
38
58
  const body = host.readText(path);
39
- if (body !== null) return body;
59
+ if (body !== null) return { body, source: "catalogue" };
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function contractSourceCandidates(options: ComposeSkillsOptions): string[] {
65
+ const paths: string[] = [];
66
+ if (options.contractPath) paths.push(options.contractPath);
67
+ paths.push(join(options.launcherPackageRoot, "contracts", "conversation-contract.md"));
68
+ // Monorepo fallback so tests and dev runs work without a prior `npm run build` pack step.
69
+ paths.push(join(options.launcherPackageRoot, "..", "..", "docs", "contracts", "conversation-contract.md"));
70
+ return paths;
71
+ }
72
+
73
+ /** Reads and extracts the shared conversation contract block. Returns null when no source resolves. */
74
+ function readContractBlock(host: WorkspaceHost, options: ComposeSkillsOptions): string | null {
75
+ for (const path of contractSourceCandidates(options)) {
76
+ const raw = host.readText(path);
77
+ if (raw === null) continue;
78
+ try {
79
+ return extractContractBlock(raw);
80
+ } catch {
81
+ continue;
82
+ }
40
83
  }
41
84
  return null;
42
85
  }
43
86
 
87
+ function readLauncherVersion(host: WorkspaceHost, options: ComposeSkillsOptions): string | undefined {
88
+ const raw = host.readText(join(options.launcherPackageRoot, "package.json"));
89
+ if (raw === null) return undefined;
90
+ try {
91
+ const parsed: unknown = JSON.parse(raw);
92
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
93
+ const version = (parsed as { version?: unknown }).version;
94
+ if (typeof version === "string" && version.trim() !== "") return version;
95
+ }
96
+ } catch {
97
+ /* unreadable manifest; no version */
98
+ }
99
+ return undefined;
100
+ }
101
+
44
102
  /** Package directory names that have a resolvable skill source in the catalogue or monorepo. */
45
103
  export function listSkillPackageCandidates(host: WorkspaceHost, options: ComposeSkillsOptions): string[] {
46
104
  const names = new Set<string>();
@@ -95,9 +153,27 @@ function writeDiscoveryLink(host: WorkspaceHost, directory: string, packageDir:
95
153
  }
96
154
  }
97
155
 
156
+ /** Removes a composed skill's `.agents/skills/` directory and its host discovery links. Missing paths are no-ops. */
157
+ function removeComposedSkill(host: WorkspaceHost, directory: string, packageDir: string): void {
158
+ host.remove(containedPath(directory, join(AGENTS_SKILLS_REL, `clossys-${packageDir}`)));
159
+ for (const prefix of DISCOVERY_PREFIXES) {
160
+ const prefixPath = containedPath(directory, prefix);
161
+ if (host.isSymlink(prefixPath)) continue; // shared view onto .agents/skills; already handled above
162
+ host.remove(containedPath(directory, join(prefix, `clossys-${packageDir}`)));
163
+ }
164
+ }
165
+
98
166
  /**
99
167
  * Writes `.agents/skills/clossys-<pkg>/SKILL.md` and host discovery links for every
100
- * catalogue package. Missing sources are skipped with notes; the hub apply continues.
168
+ * catalogue package, with the shared conversation contract (#1182) injected in place
169
+ * of each skill's own `## How we work together` / `## One question at a time`
170
+ * sections. Missing sources are skipped with notes; the hub apply continues.
171
+ *
172
+ * Also writes `clossys/.state/skills.json` (#1183): source, version, and content
173
+ * digest per composed skill. A skill named in this directory's *previous* manifest
174
+ * but not composed this run is retired — its composed output is pruned and it is
175
+ * dropped from the new manifest. Never touches a skill this directory's manifest
176
+ * did not itself write.
101
177
  */
102
178
  export function composeSkills(
103
179
  host: WorkspaceHost,
@@ -106,15 +182,44 @@ export function composeSkills(
106
182
  ): SkillCompositionResult {
107
183
  const composed: string[] = [];
108
184
  const skipped: { packageDir: string; note: string }[] = [];
185
+ const manifestEntries: SkillManifestEntry[] = [];
186
+ const contractBlock = readContractBlock(host, options);
187
+ const launcherVersion = readLauncherVersion(host, options);
188
+
109
189
  for (const packageDir of listSkillPackageCandidates(host, options)) {
110
- const body = readSkillBody(host, packageDir, options);
111
- if (body === null) {
190
+ const found = readSkillBody(host, packageDir, options, directory);
191
+ if (found === null) {
112
192
  skipped.push({ packageDir, note: "skill source missing at apply time" });
113
193
  continue;
114
194
  }
195
+ const body = contractBlock === null ? found.body : injectContract(found.body, contractBlock);
115
196
  writeAgentsSkill(host, directory, packageDir, body);
116
197
  writeDiscoveryLink(host, directory, packageDir, body);
117
198
  composed.push(packageDir);
199
+ const version = found.source === "installed" ? readInstalledVersion(host, directory, packageDir) : launcherVersion;
200
+ manifestEntries.push({
201
+ name: packageDir,
202
+ source: found.source,
203
+ sha256: sha256Hex(body.endsWith("\n") ? body : `${body}\n`),
204
+ ...(version === undefined ? {} : { version }),
205
+ });
118
206
  }
119
- return { composed, skipped };
207
+
208
+ const previousManifest = parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL)));
209
+ const composedSet = new Set(composed);
210
+ const retired: string[] = [];
211
+ for (const entry of previousManifest?.skills ?? []) {
212
+ if (composedSet.has(entry.name)) continue;
213
+ removeComposedSkill(host, directory, entry.name);
214
+ retired.push(entry.name);
215
+ }
216
+
217
+ const manifestPath = containedPath(directory, SKILLS_MANIFEST_REL);
218
+ host.mkdirp(dirname(manifestPath));
219
+ host.writeText(
220
+ manifestPath,
221
+ serializeSkillManifest({ schemaVersion: 1, generatedAt: host.now(), skills: manifestEntries }),
222
+ );
223
+
224
+ return { composed, skipped, retired };
120
225
  }
package/src/types.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { DiscoveredHost } from "./hosts.js";
2
+ import type { ExternalInventoryDeclaration, InventoryDriftReport } from "./inventory-adoption.js";
3
+
1
4
  /** Ternary retained by the installed CLI. */
2
5
  export type WorkspaceState = "satisfied" | "violated" | "indeterminate";
3
6
 
@@ -23,17 +26,52 @@ export interface WorkspaceHost {
23
26
  mkdirp(path: string): void;
24
27
  /** Creates a relative symlink at linkPath pointing at relativeTarget (directory link). */
25
28
  symlink(relativeTarget: string, linkPath: string): void;
29
+ /** Recursively removes path. A missing path is a no-op, never a throw. */
30
+ remove(path: string): void;
26
31
  readDir(path: string): string[];
27
32
  run(command: string, args: readonly string[], options?: { cwd?: string }): CommandResult;
28
33
  prompt(message: string, choices: readonly string[]): string | null;
29
34
  }
30
35
 
31
- /** On-disk hub marker generated on the consumer hub. Packed template: skeleton/.clossys/workspace.json. */
36
+ /** On-disk hub marker generated on the consumer hub. Packed template: skeleton/clossys/.state/workspace.json. */
32
37
  export interface HubDocument {
33
38
  readonly schemaVersion: 1;
34
39
  readonly kind: "account-hub";
35
40
  readonly owner: string;
36
41
  readonly repository: string;
42
+ /** Declares an existing external repository inventory as the source of truth (#1216). Hand-edited by the client; launcher never writes this field. */
43
+ readonly externalInventory?: ExternalInventoryDeclaration;
44
+ }
45
+
46
+ /**
47
+ * Where the hub marker was found relative to the `.clossys/` -> `clossys/.state/`
48
+ * migration (#1171): `clean` — only the current path. `legacy` — only the old
49
+ * `.clossys/` path; resume migrates it. `indeterminate` — both paths carry a
50
+ * parseable marker; launcher never merges them silently and refuses instead.
51
+ */
52
+ export type HubMigrationState = "clean" | "legacy" | "indeterminate";
53
+
54
+ /** One skill recorded in `clossys/.state/skills.json` (#1183). */
55
+ export interface SkillManifestEntry {
56
+ readonly name: string;
57
+ readonly source: "installed" | "catalogue";
58
+ readonly version?: string;
59
+ readonly sha256: string;
60
+ }
61
+
62
+ /** The `clossys/.state/skills.json` document itself. */
63
+ export interface SkillManifestDocument {
64
+ readonly schemaVersion: 1;
65
+ readonly generatedAt: string;
66
+ readonly skills: readonly SkillManifestEntry[];
67
+ }
68
+
69
+ /** Read-only freshness summary derived from the skills manifest for the health report. */
70
+ export interface SkillsManifestSummary {
71
+ readonly status: "present" | "missing";
72
+ readonly total: number;
73
+ readonly stale: number;
74
+ readonly retired: number;
37
75
  }
38
76
 
39
77
  export interface InventoryObservation {
@@ -84,18 +122,31 @@ export interface HubHealthReport {
84
122
  readonly extraClossys: readonly string[];
85
123
  readonly pinFindings: readonly PinFinding[];
86
124
  readonly degraded: boolean;
125
+ /** Coding-agent hosts this apply found already linked for skill discovery here, recorded before compose ran (#1180). Always present after apply. */
126
+ readonly linkedHosts?: readonly DiscoveredHost[];
127
+ /** Present only when the hub marker declares an external inventory (#1216) and there is something to say about it (i.e. not "no-external-source"). */
128
+ readonly inventoryDrift?: InventoryDriftReport;
87
129
  readonly skillComposition?: {
88
130
  readonly composed: readonly string[];
89
131
  readonly skipped: readonly { readonly packageDir: string; readonly note: string }[];
90
132
  readonly rosterTargets?: readonly string[];
91
133
  readonly rosterSkipped?: readonly { readonly inventoryId: string; readonly note: string }[];
134
+ readonly retired?: readonly string[];
92
135
  };
136
+ /** Present only on the run that performed the `.clossys/` -> `clossys/.state/` migration. */
137
+ readonly migration?: { readonly status: "migrated"; readonly from: string; readonly to: string };
138
+ /** Freshness summary derived from `clossys/.state/skills.json`, when present. */
139
+ readonly skillsManifest?: SkillsManifestSummary;
93
140
  }
94
141
 
95
142
  /** Optional paths for skill composition during apply. */
96
143
  export interface ApplyWorkspaceOptions {
97
144
  readonly skillCatalogueRoot?: string;
98
145
  readonly launcherPackageRoot?: string;
146
+ /** Overrides where the packed conversation contract is read from (tests). */
147
+ readonly contractPath?: string;
148
+ /** Live registry `@clossys/launcher` version, used only to grade catalogue-sourced skill staleness. */
149
+ readonly liveLauncherVersion?: string;
99
150
  }
100
151
 
101
152
  export interface CwdObservation {
@@ -107,6 +158,8 @@ export interface CwdObservation {
107
158
  readonly hub?: HubDocument;
108
159
  readonly looksLikeFoundry: boolean;
109
160
  readonly inventory?: InventoryObservation;
161
+ /** Present only when a hub marker was found; absent means neither path has one. */
162
+ readonly hubMigration?: HubMigrationState;
110
163
  }
111
164
 
112
165
  export interface WorkspaceObservation {
@@ -133,6 +186,10 @@ export interface WorkspacePlanResume {
133
186
  readonly repository: string;
134
187
  readonly directory: string;
135
188
  readonly clone: boolean;
189
+ /** Live registry Advisor version, when observeWorkspace could read one. Used only to grade health. */
190
+ readonly advisorVersion?: string;
191
+ /** Set when the hub marker was found only at the legacy `.clossys/` path; apply migrates it. */
192
+ readonly migrateFrom?: "legacy";
136
193
  }
137
194
 
138
195
  export interface WorkspacePlanAdopt {