@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.
- package/CHANGELOG.md +20 -0
- package/README.md +29 -6
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +4 -0
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +14 -4
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +185 -14
- package/dist/core.js.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +16 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/skills.d.ts +20 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +106 -0
- package/dist/skills.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/skeleton/README.md +11 -21
- package/skill-catalogue/advisor/SKILL.md +50 -0
- package/skill-catalogue/architect/SKILL.md +42 -0
- package/skill-catalogue/bouncer/SKILL.md +43 -0
- package/skill-catalogue/builder/SKILL.md +42 -0
- package/skill-catalogue/butler/SKILL.md +43 -0
- package/skill-catalogue/controller/SKILL.md +42 -0
- package/skill-catalogue/designer/SKILL.md +50 -0
- package/skill-catalogue/giver/SKILL.md +43 -0
- package/skill-catalogue/influencer/SKILL.md +43 -0
- package/skill-catalogue/inspector/SKILL.md +42 -0
- package/skill-catalogue/integrator/SKILL.md +42 -0
- package/skill-catalogue/keeper/SKILL.md +43 -0
- package/skill-catalogue/launcher/SKILL.md +53 -0
- package/skill-catalogue/locksmith/SKILL.md +42 -0
- package/skill-catalogue/messenger/SKILL.md +43 -0
- package/skill-catalogue/observer/SKILL.md +42 -0
- package/skill-catalogue/publisher/SKILL.md +50 -0
- package/skill-catalogue/starter/SKILL.md +48 -0
- package/skill-catalogue/strategist/SKILL.md +51 -0
- package/skill-catalogue/writer/SKILL.md +51 -0
- package/src/check-cli.ts +4 -0
- package/src/cli.ts +8 -5
- package/src/core.ts +231 -14
- package/src/host.ts +14 -1
- package/src/index.ts +3 -0
- package/src/skills.ts +120 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
725
|
-
export function applyWorkspacePlan(
|
|
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
|
|
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
|
|
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
|
|
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,
|