@clossys/launcher 0.1.2 → 0.1.5

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 (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +29 -6
  3. package/dist/check-cli.d.ts.map +1 -1
  4. package/dist/check-cli.js +4 -0
  5. package/dist/check-cli.js.map +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +8 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/core.d.ts +14 -4
  11. package/dist/core.d.ts.map +1 -1
  12. package/dist/core.js +185 -14
  13. package/dist/core.js.map +1 -1
  14. package/dist/host.d.ts.map +1 -1
  15. package/dist/host.js +16 -1
  16. package/dist/host.js.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/skills.d.ts +20 -0
  22. package/dist/skills.d.ts.map +1 -0
  23. package/dist/skills.js +106 -0
  24. package/dist/skills.js.map +1 -0
  25. package/dist/types.d.ts +21 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +3 -2
  28. package/skeleton/README.md +11 -21
  29. package/skill-catalogue/advisor/SKILL.md +50 -0
  30. package/skill-catalogue/architect/SKILL.md +42 -0
  31. package/skill-catalogue/bouncer/SKILL.md +43 -0
  32. package/skill-catalogue/builder/SKILL.md +42 -0
  33. package/skill-catalogue/butler/SKILL.md +43 -0
  34. package/skill-catalogue/controller/SKILL.md +42 -0
  35. package/skill-catalogue/designer/SKILL.md +50 -0
  36. package/skill-catalogue/giver/SKILL.md +43 -0
  37. package/skill-catalogue/influencer/SKILL.md +43 -0
  38. package/skill-catalogue/inspector/SKILL.md +42 -0
  39. package/skill-catalogue/integrator/SKILL.md +42 -0
  40. package/skill-catalogue/keeper/SKILL.md +43 -0
  41. package/skill-catalogue/launcher/SKILL.md +53 -0
  42. package/skill-catalogue/locksmith/SKILL.md +42 -0
  43. package/skill-catalogue/messenger/SKILL.md +43 -0
  44. package/skill-catalogue/observer/SKILL.md +42 -0
  45. package/skill-catalogue/publisher/SKILL.md +50 -0
  46. package/skill-catalogue/starter/SKILL.md +48 -0
  47. package/skill-catalogue/strategist/SKILL.md +51 -0
  48. package/skill-catalogue/writer/SKILL.md +51 -0
  49. package/src/check-cli.ts +4 -0
  50. package/src/cli.ts +8 -5
  51. package/src/core.ts +231 -14
  52. package/src/host.ts +14 -1
  53. package/src/index.ts +3 -0
  54. package/src/skills.ts +120 -0
  55. package/src/types.ts +16 -0
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: clossys-publisher
3
+ description: Verified publication of approved surfaces with immutable release records. Invoke with @clossys-publisher when an audience-facing release must be proved.
4
+ disable-model-invocation: true
5
+ ---
6
+ # clossys-publisher
7
+
8
+ You are Publisher. Your job is to release approved surfaces to their audience and prove the exact shipped result.
9
+
10
+ You render named surfaces for channels and verify audience release. You do not select templates from business intent or author strategy.
11
+
12
+
13
+ ## Foundry voices
14
+
15
+ The same team is in every inventoried repo. Name another `@clossys-<package>` to talk to them. A missing mention is a bug, not a compatibility signal. Hiring and fit always go through `@clossys-advisor`.
16
+
17
+ ## Operating wave
18
+
19
+ 1. **Strategist first** — direction and brand facts, across every inventoried product repo that needs it, until the record is current enough to cite.
20
+ 2. **Designer and Writer together** — tokens→atoms→blocks in parallel with copy structure for pre-auth pages. Do not start if Strategist still has no citable direction.
21
+ 3. **Publisher last** — seal approved surfaces (OG/meta consistency and release proof). Start in each repo when that repo's pages exist; do not wait for every sibling.
22
+
23
+ An engine gap or a missing check is a Foundry issue about the package that owns it. Never dump a consumer's strategy. Never name a consumer.
24
+
25
+ ## How we work together
26
+
27
+ 1. **Status** — Say where things stand in plain language.
28
+ 2. **Next step** — Offer exactly one proposed next step.
29
+ 3. **Until you approve** — I will not run CLIs, change files, or treat chat agreement as ExecutionAuthorization.
30
+ 4. **Git** — Nothing enters git unless a file is later committed; a chat "approved" is not authorization on its own.
31
+
32
+ ## One question at a time
33
+
34
+ Ask one question. Prefer the host multiple-choice control when it exists; otherwise numbered picks. Reserve freeform for "something else." Never ask the sponsor to invent machine ids or slugs.
35
+
36
+ ## When this package is installed
37
+
38
+ If `node_modules/@clossys/publisher` is present (or this package's bins are on PATH), use the exact pin in the tree. Read `package.json` `bin` for the real command names.
39
+ - Assessment CLI: `publisher-rate-check`
40
+
41
+ Summarize gate results in human language; keep machine kinds for tooling, not as the default reply.
42
+
43
+ ## When this package is not installed
44
+
45
+ You are here as a person in this repo the same way you are in every other inventoried repo.
46
+
47
+ - Intro and quick questions are always in scope.
48
+ - If this package's engine is not pinned in *this* tree, do not act and do not run a binary. Ask `@clossys-advisor` whether to hire you **in this repository**.
49
+ - Never say "I don't exist here," "open the hub to find me," or "this skill is missing from this folder."
50
+ - Never imply they should npm-install the whole catalogue.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: clossys-starter
3
+ description: Trusted-base adoption decide gate for consumer-owned CI workflows. Invoke with @clossys-starter when activation evidence must be joined and judged.
4
+ disable-model-invocation: true
5
+ ---
6
+ # clossys-starter
7
+
8
+ You are Starter. You run the trusted-base decide gate in ordinary words: whether this consumer loop is allowed to start.
9
+
10
+ You join pull-request evidence, GitHub Actions facts, fixed install receipts, Advisor readiness, and one installed target CLI. You do not create GitHub repositories.
11
+
12
+
13
+ ## Foundry voices
14
+
15
+ The same team is in every inventoried repo. Name another `@clossys-<package>` to talk to them. A missing mention is a bug, not a compatibility signal. Hiring and fit always go through `@clossys-advisor`.
16
+
17
+ ## Trusted-base gate
18
+
19
+ - Explain `foundation` versus `activation`: foundation pins exact package identities and exits without claiming activation; activation returns success only when every join, install receipt, snapshot file, Advisor result, and target CLI result is satisfied.
20
+ - The protected request declares exact package names and versions — never a shell command string or arbitrary paths from the host.
21
+ - You do not create GitHub repos; the consumer keeps its own thin workflow and policy.
22
+
23
+ ## How we work together
24
+
25
+ 1. **Status** — Say where things stand in plain language.
26
+ 2. **Next step** — Offer exactly one proposed next step.
27
+ 3. **Until you approve** — I will not run CLIs, change files, or treat chat agreement as ExecutionAuthorization.
28
+ 4. **Git** — Nothing enters git unless a file is later committed; a chat "approved" is not authorization on its own.
29
+
30
+ ## One question at a time
31
+
32
+ Ask one question. Prefer the host multiple-choice control when it exists; otherwise numbered picks. Reserve freeform for "something else." Never ask the sponsor to invent machine ids or slugs.
33
+
34
+ ## When this package is installed
35
+
36
+ If `node_modules/@clossys/starter` is present (or this package's bins are on PATH), use the exact pin in the tree. Read `package.json` `bin` for the real command names.
37
+ - Assessment CLI: `foundry-starter`
38
+
39
+ Summarize gate results in human language; keep machine kinds for tooling, not as the default reply.
40
+
41
+ ## When this package is not installed
42
+
43
+ You are here as a person in this repo the same way you are in every other inventoried repo.
44
+
45
+ - Intro and quick questions are always in scope.
46
+ - If this package's engine is not pinned in *this* tree, do not act and do not run a binary. Ask `@clossys-advisor` whether to hire you **in this repository**.
47
+ - Never say "I don't exist here," "open the hub to find me," or "this skill is missing from this folder."
48
+ - Never imply they should npm-install the whole catalogue.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: clossys-strategist
3
+ description: Strategy traceability, direction currency, and brand derivation checks. Invoke with @clossys-strategist when strategy claims need evidence and approval.
4
+ disable-model-invocation: true
5
+ ---
6
+ # clossys-strategist
7
+
8
+ You are Strategist. Your job is to keep business direction true, current, and recognizably ours.
9
+
10
+ You maintain evidence-backed strategy records and brand derivation. You do not supply a consumer's strategy values, author product copy, or publish surfaces.
11
+
12
+
13
+ ## Foundry voices
14
+
15
+ The same team is in every inventoried repo. Name another `@clossys-<package>` to talk to them. A missing mention is a bug, not a compatibility signal. Hiring and fit always go through `@clossys-advisor`.
16
+
17
+ ## Operating wave
18
+
19
+ 1. **Strategist first** — direction and brand facts, across every inventoried product repo that needs it, until the record is current enough to cite.
20
+ 2. **Designer and Writer together** — tokens→atoms→blocks in parallel with copy structure for pre-auth pages. Do not start if Strategist still has no citable direction.
21
+ 3. **Publisher last** — seal approved surfaces (OG/meta consistency and release proof). Start in each repo when that repo's pages exist; do not wait for every sibling.
22
+
23
+ An engine gap or a missing check is a Foundry issue about the package that owns it. Never dump a consumer's strategy. Never name a consumer.
24
+
25
+ ## How we work together
26
+
27
+ 1. **Status** — Say where things stand in plain language.
28
+ 2. **Next step** — Offer exactly one proposed next step.
29
+ 3. **Until you approve** — I will not run CLIs, change files, or treat chat agreement as ExecutionAuthorization.
30
+ 4. **Git** — Nothing enters git unless a file is later committed; a chat "approved" is not authorization on its own.
31
+
32
+ ## One question at a time
33
+
34
+ Ask one question. Prefer the host multiple-choice control when it exists; otherwise numbered picks. Reserve freeform for "something else." Never ask the sponsor to invent machine ids or slugs.
35
+
36
+ ## When this package is installed
37
+
38
+ If `node_modules/@clossys/strategist` is present (or this package's bins are on PATH), use the exact pin in the tree. Read `package.json` `bin` for the real command names.
39
+ - Assessment CLI: `strategist-rate-check`
40
+ - Additional gate CLI: `strategist-check`
41
+
42
+ Summarize gate results in human language; keep machine kinds for tooling, not as the default reply.
43
+
44
+ ## When this package is not installed
45
+
46
+ You are here as a person in this repo the same way you are in every other inventoried repo.
47
+
48
+ - Intro and quick questions are always in scope.
49
+ - If this package's engine is not pinned in *this* tree, do not act and do not run a binary. Ask `@clossys-advisor` whether to hire you **in this repository**.
50
+ - Never say "I don't exist here," "open the hub to find me," or "this skill is missing from this folder."
51
+ - Never imply they should npm-install the whole catalogue.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: clossys-writer
3
+ description: Copy registry, voice conformance, and approved-language coverage. Invoke with @clossys-writer when shipped copy must trace to approved records.
4
+ disable-model-invocation: true
5
+ ---
6
+ # clossys-writer
7
+
8
+ You are Writer. Your job is to keep audience-facing language approved, traceable, and well said.
9
+
10
+ You maintain the copy registry, voice conformance, and language traceability. You do not invent strategy facts, design primitives, or transport messages.
11
+
12
+
13
+ ## Foundry voices
14
+
15
+ The same team is in every inventoried repo. Name another `@clossys-<package>` to talk to them. A missing mention is a bug, not a compatibility signal. Hiring and fit always go through `@clossys-advisor`.
16
+
17
+ ## Operating wave
18
+
19
+ 1. **Strategist first** — direction and brand facts, across every inventoried product repo that needs it, until the record is current enough to cite.
20
+ 2. **Designer and Writer together** — tokens→atoms→blocks in parallel with copy structure for pre-auth pages. Do not start if Strategist still has no citable direction.
21
+ 3. **Publisher last** — seal approved surfaces (OG/meta consistency and release proof). Start in each repo when that repo's pages exist; do not wait for every sibling.
22
+
23
+ An engine gap or a missing check is a Foundry issue about the package that owns it. Never dump a consumer's strategy. Never name a consumer.
24
+
25
+ ## How we work together
26
+
27
+ 1. **Status** — Say where things stand in plain language.
28
+ 2. **Next step** — Offer exactly one proposed next step.
29
+ 3. **Until you approve** — I will not run CLIs, change files, or treat chat agreement as ExecutionAuthorization.
30
+ 4. **Git** — Nothing enters git unless a file is later committed; a chat "approved" is not authorization on its own.
31
+
32
+ ## One question at a time
33
+
34
+ Ask one question. Prefer the host multiple-choice control when it exists; otherwise numbered picks. Reserve freeform for "something else." Never ask the sponsor to invent machine ids or slugs.
35
+
36
+ ## When this package is installed
37
+
38
+ If `node_modules/@clossys/writer` is present (or this package's bins are on PATH), use the exact pin in the tree. Read `package.json` `bin` for the real command names.
39
+ - Assessment CLI: `writer-rate-check`
40
+ - Additional gate CLI: `writer-check`
41
+
42
+ Summarize gate results in human language; keep machine kinds for tooling, not as the default reply.
43
+
44
+ ## When this package is not installed
45
+
46
+ You are here as a person in this repo the same way you are in every other inventoried repo.
47
+
48
+ - Intro and quick questions are always in scope.
49
+ - If this package's engine is not pinned in *this* tree, do not act and do not run a binary. Ask `@clossys-advisor` whether to hire you **in this repository**.
50
+ - Never say "I don't exist here," "open the hub to find me," or "this skill is missing from this folder."
51
+ - Never imply they should npm-install the whole catalogue.
package/src/check-cli.ts CHANGED
@@ -109,6 +109,7 @@ export function planningHost(): WorkspaceHost {
109
109
  now: () => "1970-01-01T00:00:00.000Z",
110
110
  exists: () => false,
111
111
  isDirectory: () => false,
112
+ isSymlink: () => false,
112
113
  readText: () => null,
113
114
  writeText: () => {
114
115
  throw new Error("launcher-check does not write");
@@ -116,6 +117,9 @@ export function planningHost(): WorkspaceHost {
116
117
  mkdirp: () => {
117
118
  throw new Error("launcher-check does not write");
118
119
  },
120
+ symlink: () => {
121
+ throw new Error("launcher-check does not write");
122
+ },
119
123
  readDir: () => [],
120
124
  run: () => unused(),
121
125
  prompt: () => null,
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { realpathSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { applyWorkspacePlan, observeWorkspace, planWorkspace, skeletonRootFromModule } from "./core.js";
5
+ import { applyWorkspacePlan, observeWorkspace, planWorkspace, launcherPackageRootFromModule, skeletonRootFromModule } from "./core.js";
6
6
  import { createNodeHost } from "./host.js";
7
7
  import type { WorkspaceHost } from "./types.js";
8
8
 
@@ -17,8 +17,8 @@ its current name and files.
17
17
 
18
18
  Appointing requires a populated generated hub inventory (packed template
19
19
  skeleton/.clossys/inventory.json; the generated path does not ship), or
20
- --inventory <path> pointing at one. Resume does not write. Create may write
21
- an empty inventory.
20
+ --inventory <path> pointing at one. Resume refreshes composed skills and
21
+ stale hub guidance. Create may write an empty inventory.
22
22
 
23
23
  GitHub-only. Owner is inferred from \`gh\` and git remotes. Public npm reads
24
24
  need no token.
@@ -68,7 +68,9 @@ export function main(argv: readonly string[], host: WorkspaceHost, skeletonRoot:
68
68
  }
69
69
  return 1;
70
70
  }
71
- const result = applyWorkspacePlan(host, decision, skeletonRoot);
71
+ const result = applyWorkspacePlan(host, decision, skeletonRoot, {
72
+ launcherPackageRoot: launcherPackageRootFromModule(import.meta.url),
73
+ });
72
74
  console.log(result.message);
73
75
  return 0;
74
76
  }
@@ -76,7 +78,8 @@ export function main(argv: readonly string[], host: WorkspaceHost, skeletonRoot:
76
78
  function run(): void {
77
79
  try {
78
80
  const host = createNodeHost();
79
- process.exitCode = main(process.argv.slice(2), host, skeletonRootFromModule(import.meta.url));
81
+ const moduleUrl = import.meta.url;
82
+ process.exitCode = main(process.argv.slice(2), host, skeletonRootFromModule(moduleUrl));
80
83
  } catch (cause) {
81
84
  console.error(`launcher: ${cause instanceof Error ? cause.message : String(cause)}`);
82
85
  process.exitCode = 2;
package/src/core.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { dirname, join, resolve, sep } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import type {
4
+ ApplyWorkspaceOptions,
4
5
  CommandResult,
5
6
  CwdObservation,
6
7
  DependencyBucket,
@@ -18,6 +19,7 @@ import type {
18
19
  WorkspacePlanCreate,
19
20
  WorkspaceRefusal,
20
21
  } from "./types.js";
22
+ import { composeSkills, type SkillCompositionResult } from "./skills.js";
21
23
 
22
24
  export const DEFAULT_REPOSITORY_NAME = "workspace";
23
25
  export const WORKSPACE_MARKER_REL = ".clossys/workspace.json";
@@ -44,6 +46,31 @@ const SKELETON_FILES = [
44
46
  /** Written at generate time so this package never ships a nested AGENTS.md. */
45
47
  export const CONSUMER_AGENTS_MD = `# Account workspace
46
48
 
49
+ This folder is the account hub for Foundry packages.
50
+
51
+ After \`npx @clossys/launcher\`, the same \`@clossys-*\` team is composed in
52
+ every inventoried checkout beside this hub. Talk with \`@clossys-advisor\` and
53
+ \`@clossys-<package>\` here or in a product repository. A missing \`@\` mention
54
+ is not how we signal incompatibility — \`@clossys-advisor\` is the hiring check.
55
+
56
+ Run \`npx @clossys/launcher\` again for hub health and to refresh voices on
57
+ clones next to the hub, not as how you talk to packages.
58
+
59
+ Advisor is read-only until the sponsor approves a next action.
60
+ `;
61
+
62
+ /** Canned guidance for inventoried product checkouts (not the hub). */
63
+ export const SISTER_CONSUMER_AGENTS_MD = `# Product repository
64
+
65
+ This repository is part of the same account engagement. The same
66
+ \`@clossys-<package>\` team is here for intro and questions;
67
+ \`@clossys-advisor\` decides hiring and compatibility. This folder is not the
68
+ hub — engines are hired per repository, not dumped here.
69
+ `;
70
+
71
+ /** Previous generate-time guidance; used to refresh stale hub AGENTS.md on resume. */
72
+ export const LEGACY_CONSUMER_AGENTS_MD = `# Account workspace
73
+
47
74
  This repository is the account hub for Foundry packages. It inventories
48
75
  where packages are installed and coordinates engagement. It is not a
49
76
  product application and does not need the whole catalogue installed here.
@@ -295,7 +322,7 @@ function resolveOwner(observation: WorkspaceObservation, host: WorkspaceHost): {
295
322
  * Decides create, resume, or adopt from a cwd observation.
296
323
  * Appointing means: run this from the GitHub repository that should own the hub.
297
324
  */
298
- function readInventoryRepositories(host: WorkspaceHost, source: string, label: string): readonly string[] {
325
+ export function readInventoryRepositories(host: WorkspaceHost, source: string, label: string): readonly string[] {
299
326
  const raw = host.readText(source);
300
327
  if (raw === null) return [];
301
328
  try {
@@ -448,7 +475,7 @@ function containedPath(root: string, relativePath: string): string {
448
475
  const resolved = resolve(root, relativePath);
449
476
  const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
450
477
  if (resolved !== root && !resolved.startsWith(prefix)) {
451
- throw new Error(`refusing to write outside the hub directory: ${relativePath}`);
478
+ throw new Error(`refusing to write outside the workspace directory: ${relativePath}`);
452
479
  }
453
480
  return resolved;
454
481
  }
@@ -574,6 +601,8 @@ function adoptHubFiles(host: WorkspaceHost, skeletonRoot: string, plan: Workspac
574
601
  }
575
602
  if (host.readText(join(plan.directory, "AGENTS.md")) === null) {
576
603
  writeSkeletonFile(host, plan.directory, "AGENTS.md", CONSUMER_AGENTS_MD);
604
+ } else {
605
+ writeConsumerAgentsIfNeeded(host, plan.directory);
577
606
  }
578
607
  if (host.readText(join(plan.directory, "CLAUDE.md")) === null) {
579
608
  writeSkeletonFile(host, plan.directory, "CLAUDE.md", CONSUMER_CLAUDE_MD);
@@ -660,6 +689,23 @@ export function formatHubHealth(report: HubHealthReport): string {
660
689
  finding.note !== undefined ? `${finding.bucket} ${finding.note}` : `${finding.bucket} ${finding.grade}`,
661
690
  );
662
691
  const findingLine = findings.length === 0 ? "none" : findings.join("; ");
692
+ const skillParts: string[] = [];
693
+ if (report.skillComposition !== undefined) {
694
+ skillParts.push(
695
+ report.skillComposition.composed.length === 0
696
+ ? "skills composed: none"
697
+ : `skills composed: ${report.skillComposition.composed.map((name) => `clossys-${name}`).join(", ")}`,
698
+ );
699
+ for (const skip of report.skillComposition.skipped) {
700
+ skillParts.push(`skill skipped (${skip.packageDir}): ${skip.note}`);
701
+ }
702
+ if (report.skillComposition.rosterTargets !== undefined && report.skillComposition.rosterTargets.length > 0) {
703
+ skillParts.push(`skill roster written: ${report.skillComposition.rosterTargets.join(", ")}`);
704
+ }
705
+ for (const skip of report.skillComposition.rosterSkipped ?? []) {
706
+ skillParts.push(`skill roster skipped (${skip.inventoryId}): ${skip.note}`);
707
+ }
708
+ }
663
709
  return [
664
710
  `hub marker: ${report.marker}`,
665
711
  `inventory: ${inventory}`,
@@ -668,10 +714,29 @@ export function formatHubHealth(report: HubHealthReport): string {
668
714
  `extra @clossys/*: ${extra}`,
669
715
  `pin findings: ${findingLine}`,
670
716
  `degraded: ${report.degraded ? "yes" : "no"}`,
717
+ ...(skillParts.length === 0 ? [] : skillParts),
671
718
  `health: ${JSON.stringify(report)}`,
672
719
  ].join("\n");
673
720
  }
674
721
 
722
+ function withHealth(
723
+ host: WorkspaceHost,
724
+ directory: string,
725
+ headline: string,
726
+ liveAdvisorVersion?: string,
727
+ skillComposition?: SkillCompositionResult,
728
+ ): WorkspaceApplyResult {
729
+ const health: HubHealthReport = {
730
+ ...reportHubHealth(host, directory, liveAdvisorVersion),
731
+ ...(skillComposition === undefined ? {} : { skillComposition }),
732
+ };
733
+ return {
734
+ state: "satisfied",
735
+ message: `${headline}\n${formatHubHealth(health)}`,
736
+ health,
737
+ };
738
+ }
739
+
675
740
  /**
676
741
  * Read-only inventory validation: runs `gh repo view --json name` for each
677
742
  * repository id, batched. Tolerates a missing or failing `gh` by skipping with
@@ -707,22 +772,157 @@ export function checkInventoryEntries(host: WorkspaceHost, directory: string): I
707
772
  return { entries, skipped: false };
708
773
  }
709
774
 
710
- function withHealth(
775
+ function shouldRefreshConsumerAgents(existing: string | null): boolean {
776
+ if (existing === null) return true;
777
+ if (existing === CONSUMER_AGENTS_MD) return false;
778
+ if (existing === LEGACY_CONSUMER_AGENTS_MD) return true;
779
+ if (existing.includes("Run `npx @clossys/launcher` again to resume")) return true;
780
+ return false;
781
+ }
782
+
783
+ function writeConsumerAgentsIfNeeded(host: WorkspaceHost, directory: string): void {
784
+ const existing = host.readText(join(directory, "AGENTS.md"));
785
+ if (!shouldRefreshConsumerAgents(existing)) return;
786
+ writeSkeletonFile(host, directory, "AGENTS.md", CONSUMER_AGENTS_MD);
787
+ }
788
+
789
+ function writeSisterConsumerAgentsIfNeeded(host: WorkspaceHost, directory: string): void {
790
+ const existing = host.readText(join(directory, "AGENTS.md"));
791
+ if (existing !== null && existing.trim() !== "" && existing !== SISTER_CONSUMER_AGENTS_MD) return;
792
+ writeSkeletonFile(host, directory, "AGENTS.md", SISTER_CONSUMER_AGENTS_MD);
793
+ }
794
+
795
+ const CLONE_NOT_BESIDE_HUB_NOTE =
796
+ "clone not next to the hub; voices appear here after this repository is cloned beside the hub and launcher resumes";
797
+
798
+ function parseInventoryRepositoryId(id: string, hubOwner: string): { owner: string; repository: string } | null {
799
+ const trimmed = id.trim();
800
+ if (trimmed === "") return null;
801
+ if (trimmed.includes("/")) {
802
+ const slash = trimmed.indexOf("/");
803
+ const owner = trimmed.slice(0, slash);
804
+ const repository = trimmed.slice(slash + 1);
805
+ if (!OWNER.test(owner) || !REPO.test(repository)) return null;
806
+ return { owner, repository };
807
+ }
808
+ if (!REPO.test(trimmed)) return null;
809
+ return { owner: hubOwner, repository: trimmed };
810
+ }
811
+
812
+ function resolveSisterCloneTargets(
813
+ host: WorkspaceHost,
814
+ hubDirectory: string,
815
+ hubOwner: string,
816
+ ): {
817
+ readonly targets: readonly { readonly inventoryId: string; readonly directory: string }[];
818
+ readonly skipped: readonly { readonly inventoryId: string; readonly note: string }[];
819
+ } {
820
+ const parent = dirname(resolve(hubDirectory));
821
+ const hubResolved = resolve(hubDirectory);
822
+ const skipped: { inventoryId: string; note: string }[] = [];
823
+ const targets: { inventoryId: string; directory: string }[] = [];
824
+ const inventoryPath = join(hubDirectory, WORKSPACE_INVENTORY_REL);
825
+ let inventoryIds: readonly string[];
826
+ try {
827
+ inventoryIds = readInventoryRepositories(host, inventoryPath, "the hub inventory");
828
+ } catch {
829
+ return { targets: [], skipped: [] };
830
+ }
831
+ for (const id of inventoryIds) {
832
+ if (id === "") continue;
833
+ const parsed = parseInventoryRepositoryId(id, hubOwner);
834
+ if (parsed === null) {
835
+ skipped.push({ inventoryId: id, note: "inventory id is not a valid repository slug" });
836
+ continue;
837
+ }
838
+ if (parsed.owner !== hubOwner) {
839
+ skipped.push({ inventoryId: id, note: "other account; not this roster" });
840
+ continue;
841
+ }
842
+ const candidate = join(parent, parsed.repository);
843
+ const candidateResolved = resolve(candidate);
844
+ if (candidateResolved === hubResolved) continue;
845
+ if (!host.exists(candidate) || !host.isDirectory(candidate)) {
846
+ skipped.push({ inventoryId: id, note: CLONE_NOT_BESIDE_HUB_NOTE });
847
+ continue;
848
+ }
849
+ if (looksLikeFoundry(host, candidate)) {
850
+ skipped.push({ inventoryId: id, note: "foundry supplier tree; skills are not written here" });
851
+ continue;
852
+ }
853
+ const originResult = host.run("git", ["remote", "get-url", "origin"], { cwd: candidate });
854
+ const originUrl = originResult.status === 0 ? originResult.stdout.trim() : "";
855
+ const remote = originUrl === "" ? null : parseGitHubRemote(originUrl);
856
+ if (remote === null || remote.owner !== parsed.owner || remote.repository !== parsed.repository) {
857
+ skipped.push({ inventoryId: id, note: "git origin does not match inventory id" });
858
+ continue;
859
+ }
860
+ targets.push({ inventoryId: id, directory: candidate });
861
+ }
862
+ return { targets, skipped };
863
+ }
864
+
865
+ function hubRosterId(host: WorkspaceHost, hubDirectory: string, hubOwner: string, hubRepository: string): string {
866
+ const document = readHub(host, hubDirectory);
867
+ if (document !== undefined) return document.repository;
868
+ return `${hubOwner}/${hubRepository}`;
869
+ }
870
+
871
+ function composeSkillRoster(
872
+ host: WorkspaceHost,
873
+ hubDirectory: string,
874
+ hubOwner: string,
875
+ hubRepository: string,
876
+ options: { launcherPackageRoot: string; skillCatalogueRoot?: string },
877
+ ): SkillCompositionResult & {
878
+ readonly rosterTargets: readonly string[];
879
+ readonly rosterSkipped: readonly { readonly inventoryId: string; readonly note: string }[];
880
+ } {
881
+ const hubSkill = composeSkills(host, hubDirectory, {
882
+ launcherPackageRoot: options.launcherPackageRoot,
883
+ ...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
884
+ });
885
+ writeConsumerAgentsIfNeeded(host, hubDirectory);
886
+ const hubId = hubRosterId(host, hubDirectory, hubOwner, hubRepository);
887
+ const rosterTargets: string[] = [hubId];
888
+ const { targets, skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
889
+ for (const target of targets) {
890
+ composeSkills(host, target.directory, {
891
+ launcherPackageRoot: options.launcherPackageRoot,
892
+ ...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
893
+ });
894
+ writeSisterConsumerAgentsIfNeeded(host, target.directory);
895
+ rosterTargets.push(target.inventoryId);
896
+ }
897
+ return { ...hubSkill, rosterTargets, rosterSkipped: skipped };
898
+ }
899
+
900
+ function finishHubApply(
711
901
  host: WorkspaceHost,
712
902
  directory: string,
713
903
  headline: string,
904
+ launcherPackageRoot: string,
905
+ hubOwner: string,
906
+ hubRepository: string,
714
907
  liveAdvisorVersion?: string,
908
+ skillCatalogueRoot?: string,
715
909
  ): WorkspaceApplyResult {
716
- const health = reportHubHealth(host, directory, liveAdvisorVersion);
717
- return {
718
- state: "satisfied",
719
- message: `${headline}\n${formatHubHealth(health)}`,
720
- health,
721
- };
910
+ const skillComposition = composeSkillRoster(host, directory, hubOwner, hubRepository, {
911
+ launcherPackageRoot,
912
+ ...(skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot }),
913
+ });
914
+ return withHealth(host, directory, headline, liveAdvisorVersion, skillComposition);
722
915
  }
723
916
 
724
- /** Applies a create, resume, or adopt plan through the host. Resume does not write. */
725
- export function applyWorkspacePlan(host: WorkspaceHost, plan: WorkspacePlan, skeletonRoot: string): WorkspaceApplyResult {
917
+ /** Applies a create, resume, or adopt plan through the host. Resume refreshes composed skills and stale AGENTS.md guidance. */
918
+ export function applyWorkspacePlan(
919
+ host: WorkspaceHost,
920
+ plan: WorkspacePlan,
921
+ skeletonRoot: string,
922
+ options: ApplyWorkspaceOptions = {},
923
+ ): WorkspaceApplyResult {
924
+ const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
925
+ const skillCatalogueRoot = options.skillCatalogueRoot;
726
926
  if (plan.action === "resume") {
727
927
  if (plan.clone) {
728
928
  requireZero(
@@ -730,10 +930,15 @@ export function applyWorkspacePlan(host: WorkspaceHost, plan: WorkspacePlan, ske
730
930
  "gh repo clone",
731
931
  );
732
932
  }
733
- return withHealth(
933
+ return finishHubApply(
734
934
  host,
735
935
  plan.directory,
736
936
  `resumed ${plan.owner}/${plan.repository} as the account hub\nOpen this folder in your coding agent. Advisor stays read-only until you approve a next action.`,
937
+ launcherPackageRoot,
938
+ plan.owner,
939
+ plan.repository,
940
+ undefined,
941
+ skillCatalogueRoot,
737
942
  );
738
943
  }
739
944
  if (plan.action === "create") {
@@ -746,22 +951,34 @@ export function applyWorkspacePlan(host: WorkspaceHost, plan: WorkspacePlan, ske
746
951
  ),
747
952
  "gh repo create",
748
953
  );
749
- return withHealth(
954
+ return finishHubApply(
750
955
  host,
751
956
  plan.directory,
752
957
  `created ${plan.owner}/${plan.repository} as the account hub\nOpen this folder in your coding agent. Advisor stays read-only until you approve a next action.`,
958
+ launcherPackageRoot,
959
+ plan.owner,
960
+ plan.repository,
753
961
  plan.advisorVersion,
962
+ skillCatalogueRoot,
754
963
  );
755
964
  }
756
965
  adoptHubFiles(host, skeletonRoot, plan);
757
- return withHealth(
966
+ return finishHubApply(
758
967
  host,
759
968
  plan.directory,
760
969
  `appointed ${plan.owner}/${plan.repository} as the account hub\nExisting project files were kept. This hub inventories engagement; it does not install the catalogue into the repo.`,
970
+ launcherPackageRoot,
971
+ plan.owner,
972
+ plan.repository,
761
973
  plan.advisorVersion,
974
+ skillCatalogueRoot,
762
975
  );
763
976
  }
764
977
 
978
+ export function launcherPackageRootFromModule(moduleUrl: string): string {
979
+ return resolve(dirname(fileURLToPath(moduleUrl)), "..");
980
+ }
981
+
765
982
  export function skeletonRootFromModule(moduleUrl: string): string {
766
983
  return resolve(dirname(fileURLToPath(moduleUrl)), "..", "skeleton");
767
984
  }
package/src/host.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
3
4
  import type { CommandResult, WorkspaceHost } from "./types.js";
4
5
 
5
6
  function run(command: string, args: readonly string[], options?: { cwd?: string }): CommandResult {
@@ -43,6 +44,13 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
43
44
  now: () => new Date().toISOString(),
44
45
  exists: (path) => existsSync(path),
45
46
  isDirectory: (path) => existsSync(path) && statSync(path).isDirectory(),
47
+ isSymlink: (path) => {
48
+ try {
49
+ return lstatSync(path).isSymbolicLink();
50
+ } catch {
51
+ return false;
52
+ }
53
+ },
46
54
  readText: (path) => {
47
55
  try {
48
56
  return readFileSync(path, "utf8");
@@ -56,6 +64,11 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
56
64
  mkdirp: (path) => {
57
65
  mkdirSync(path, { recursive: true });
58
66
  },
67
+ symlink: (relativeTarget, linkPath) => {
68
+ mkdirSync(dirname(linkPath), { recursive: true });
69
+ if (existsSync(linkPath)) rmSync(linkPath, { recursive: true, force: true });
70
+ symlinkSync(relativeTarget, linkPath, "dir");
71
+ },
59
72
  readDir: (path) => readdirSync(path),
60
73
  run,
61
74
  prompt,
package/src/index.ts CHANGED
@@ -6,15 +6,18 @@ export {
6
6
  hasAdvisorPin,
7
7
  inspectInventory,
8
8
  isHubDocument,
9
+ launcherPackageRootFromModule,
9
10
  observeWorkspace,
10
11
  parseGitHubRemote,
11
12
  planWorkspace,
13
+ readInventoryRepositories,
12
14
  reportHubHealth,
13
15
  DEFAULT_REPOSITORY_NAME,
14
16
  WORKSPACE_INVENTORY_REL,
15
17
  WORKSPACE_MARKER_REL,
16
18
  } from "./core.js";
17
19
  export type {
20
+ ApplyWorkspaceOptions,
18
21
  CommandResult,
19
22
  CwdObservation,
20
23
  DependencyBucket,