@clossys/launcher 0.1.5 → 0.3.1
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/README.md +213 -20
- package/contracts/conversation-contract.md +41 -0
- package/dist/apply-plan-cli.d.ts +7 -0
- package/dist/apply-plan-cli.d.ts.map +1 -0
- package/dist/apply-plan-cli.js +96 -0
- package/dist/apply-plan-cli.js.map +1 -0
- package/dist/apply-plan.d.ts +79 -0
- package/dist/apply-plan.d.ts.map +1 -0
- package/dist/apply-plan.js +129 -0
- package/dist/apply-plan.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +3 -0
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -11
- package/dist/cli.js.map +1 -1
- package/dist/contract.d.ts +28 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +78 -0
- package/dist/contract.js.map +1 -0
- package/dist/core.d.ts +38 -6
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +289 -42
- package/dist/core.js.map +1 -1
- package/dist/doctor-cli.d.ts +4 -0
- package/dist/doctor-cli.d.ts.map +1 -0
- package/dist/doctor-cli.js +32 -0
- package/dist/doctor-cli.js.map +1 -0
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +68 -0
- package/dist/doctor.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +3 -0
- package/dist/host.js.map +1 -1
- package/dist/hosts.d.ts +14 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +61 -0
- package/dist/hosts.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +20 -0
- package/dist/inventory-adoption.d.ts.map +1 -0
- package/dist/inventory-adoption.js +67 -0
- package/dist/inventory-adoption.js.map +1 -0
- package/dist/manifest.d.ts +20 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +106 -0
- package/dist/manifest.js.map +1 -0
- package/dist/model-profile.d.ts +46 -0
- package/dist/model-profile.d.ts.map +1 -0
- package/dist/model-profile.js +98 -0
- package/dist/model-profile.js.map +1 -0
- package/dist/product-repository.d.ts +26 -0
- package/dist/product-repository.d.ts.map +1 -0
- package/dist/product-repository.js +49 -0
- package/dist/product-repository.js.map +1 -0
- package/dist/skills.d.ts +20 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +98 -7
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +57 -1
- package/dist/types.d.ts.map +1 -1
- package/model-profiles/claude-code.json +10 -0
- package/model-profiles/codex.json +10 -0
- package/model-profiles/cursor.json +10 -0
- package/package.json +9 -5
- package/skeleton/README.md +5 -0
- package/skeleton/package.json +1 -1
- package/skill/SKILL.md +41 -0
- package/skill-catalogue/advisor/SKILL.md +46 -8
- package/skill-catalogue/architect/SKILL.md +0 -11
- package/skill-catalogue/bouncer/SKILL.md +0 -11
- package/skill-catalogue/builder/SKILL.md +0 -11
- package/skill-catalogue/butler/SKILL.md +0 -11
- package/skill-catalogue/controller/SKILL.md +6 -9
- package/skill-catalogue/customer/SKILL.md +83 -0
- package/skill-catalogue/designer/SKILL.md +18 -9
- package/skill-catalogue/giver/SKILL.md +0 -11
- package/skill-catalogue/influencer/SKILL.md +0 -11
- package/skill-catalogue/inspector/SKILL.md +1 -12
- package/skill-catalogue/integrator/SKILL.md +0 -11
- package/skill-catalogue/keeper/SKILL.md +0 -11
- package/skill-catalogue/launcher/SKILL.md +0 -12
- package/skill-catalogue/locksmith/SKILL.md +0 -11
- package/skill-catalogue/messenger/SKILL.md +0 -11
- package/skill-catalogue/observer/SKILL.md +0 -11
- package/skill-catalogue/publisher/SKILL.md +18 -10
- package/skill-catalogue/starter/SKILL.md +0 -11
- package/skill-catalogue/strategist/SKILL.md +35 -12
- package/skill-catalogue/writer/SKILL.md +7 -9
- package/src/apply-plan-cli.ts +94 -0
- package/src/apply-plan.ts +172 -0
- package/src/check-cli.ts +3 -0
- package/src/cli.ts +45 -10
- package/src/contract.ts +81 -0
- package/src/core.ts +337 -37
- package/src/doctor-cli.ts +33 -0
- package/src/doctor.ts +145 -0
- package/src/host.ts +3 -0
- package/src/hosts.ts +79 -0
- package/src/index.ts +33 -0
- package/src/inventory-adoption.ts +85 -0
- package/src/manifest.ts +103 -0
- package/src/model-profile.ts +148 -0
- package/src/product-repository.ts +73 -0
- package/src/skills.ts +113 -8
- package/src/types.ts +58 -1
- package/CHANGELOG.md +0 -64
- /package/skeleton/{.clossys → clossys/.state}/inventory.json +0 -0
- /package/skeleton/{.clossys → clossys/.state}/workspace.json +0 -0
package/dist/core.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { composeSkills } from "./skills.js";
|
|
3
|
+
import { composeSkills, SKILLS_MANIFEST_REL } from "./skills.js";
|
|
4
|
+
import { parseSkillManifest, summarizeSkillsManifest } from "./manifest.js";
|
|
5
|
+
import { detectLinkedHosts, serializeHostRecord, HOSTS_REL } from "./hosts.js";
|
|
6
|
+
import { reportInventoryDrift } from "./inventory-adoption.js";
|
|
4
7
|
export const DEFAULT_REPOSITORY_NAME = "workspace";
|
|
5
|
-
|
|
6
|
-
export const
|
|
8
|
+
/** The one visible, per-repository Clossys folder (#1171). Every role's output lives under it. */
|
|
9
|
+
export const CLOSSYS_DIR_REL = "clossys";
|
|
10
|
+
/** Machine files only: hub marker, inventory, skills manifest. Visible (not dot-hidden) so it is easy to find, but not a place a person edits by hand. */
|
|
11
|
+
export const STATE_DIR_REL = join(CLOSSYS_DIR_REL, ".state");
|
|
12
|
+
export const WORKSPACE_MARKER_REL = join(STATE_DIR_REL, "workspace.json");
|
|
13
|
+
export const WORKSPACE_INVENTORY_REL = join(STATE_DIR_REL, "inventory.json");
|
|
14
|
+
export const CLOSSYS_README_REL = join(CLOSSYS_DIR_REL, "README.md");
|
|
15
|
+
/** Pre-#1171 machine-state directory. Resume migrates it automatically; see `locateHub`. */
|
|
16
|
+
export const LEGACY_STATE_DIR_REL = ".clossys";
|
|
17
|
+
export const LEGACY_WORKSPACE_MARKER_REL = join(LEGACY_STATE_DIR_REL, "workspace.json");
|
|
18
|
+
export const LEGACY_WORKSPACE_INVENTORY_REL = join(LEGACY_STATE_DIR_REL, "inventory.json");
|
|
7
19
|
export const ADVISOR_PACKAGE = "@clossys/advisor";
|
|
20
|
+
export const LAUNCHER_PACKAGE = "@clossys/launcher";
|
|
8
21
|
const DEPENDENCY_BUCKETS = [
|
|
9
22
|
"dependencies",
|
|
10
23
|
"devDependencies",
|
|
@@ -33,6 +46,10 @@ is not how we signal incompatibility — \`@clossys-advisor\` is the hiring chec
|
|
|
33
46
|
Run \`npx @clossys/launcher\` again for hub health and to refresh voices on
|
|
34
47
|
clones next to the hub, not as how you talk to packages.
|
|
35
48
|
|
|
49
|
+
The person in this folder is a founder, not an engineer. Speak like a
|
|
50
|
+
person. Do not dump machine identifiers, JSON, hashes, or grant fields
|
|
51
|
+
unless they ask.
|
|
52
|
+
|
|
36
53
|
Advisor is read-only until the sponsor approves a next action.
|
|
37
54
|
`;
|
|
38
55
|
/** Canned guidance for inventoried product checkouts (not the hub). */
|
|
@@ -50,6 +67,21 @@ This repository is the account hub for Foundry packages. It inventories
|
|
|
50
67
|
where packages are installed and coordinates engagement. It is not a
|
|
51
68
|
product application and does not need the whole catalogue installed here.
|
|
52
69
|
|
|
70
|
+
The person in this folder is a founder, not an engineer. Speak like a
|
|
71
|
+
person. Do not dump machine identifiers, JSON, hashes, or grant fields
|
|
72
|
+
unless they ask.
|
|
73
|
+
|
|
74
|
+
When there is a next step, say only:
|
|
75
|
+
|
|
76
|
+
1. Where we are (one sentence).
|
|
77
|
+
2. What you should do next (one sentence).
|
|
78
|
+
3. What we will not do until you say yes.
|
|
79
|
+
4. Whether anything will be saved to git (usually no).
|
|
80
|
+
|
|
81
|
+
Wait for a plain yes before changing files. "Approved" in chat is
|
|
82
|
+
permission for that one step only. It is not a lasting grant and it
|
|
83
|
+
does not become a commit unless someone later saves a file.
|
|
84
|
+
|
|
53
85
|
Open this folder in your coding agent. Advisor is read-only until you
|
|
54
86
|
approve a next action.
|
|
55
87
|
|
|
@@ -146,8 +178,8 @@ export function isHubDocument(value) {
|
|
|
146
178
|
return false;
|
|
147
179
|
return true;
|
|
148
180
|
}
|
|
149
|
-
function
|
|
150
|
-
const raw = host.readText(
|
|
181
|
+
function readHubAt(host, path) {
|
|
182
|
+
const raw = host.readText(path);
|
|
151
183
|
if (raw === null)
|
|
152
184
|
return undefined;
|
|
153
185
|
try {
|
|
@@ -158,7 +190,29 @@ function readHub(host, directory) {
|
|
|
158
190
|
return undefined;
|
|
159
191
|
}
|
|
160
192
|
}
|
|
161
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Locates the hub marker across the `.clossys/` -> `clossys/.state/`
|
|
195
|
+
* migration (#1171). `clean`: only the current path has a marker. `legacy`:
|
|
196
|
+
* only the old path does; resume migrates it (see `migrateLegacyHubState`).
|
|
197
|
+
* `indeterminate`: both paths carry a parseable marker; launcher never
|
|
198
|
+
* merges them silently, so `planWorkspace` refuses instead. `none`: neither
|
|
199
|
+
* path has one.
|
|
200
|
+
*/
|
|
201
|
+
function locateHub(host, directory) {
|
|
202
|
+
const current = readHubAt(host, join(directory, WORKSPACE_MARKER_REL));
|
|
203
|
+
const legacy = readHubAt(host, join(directory, LEGACY_WORKSPACE_MARKER_REL));
|
|
204
|
+
if (current !== undefined && legacy !== undefined)
|
|
205
|
+
return { migration: "indeterminate" };
|
|
206
|
+
if (current !== undefined)
|
|
207
|
+
return { document: current, migration: "clean" };
|
|
208
|
+
if (legacy !== undefined)
|
|
209
|
+
return { document: legacy, migration: "legacy" };
|
|
210
|
+
return { migration: "none" };
|
|
211
|
+
}
|
|
212
|
+
function readHub(host, directory) {
|
|
213
|
+
return locateHub(host, directory).document;
|
|
214
|
+
}
|
|
215
|
+
/** Classifies a generated hub inventory (packed template skeleton/clossys/.state/inventory.json; the generated path does not ship) without inventing repositories. */
|
|
162
216
|
export function inspectInventory(raw) {
|
|
163
217
|
if (raw === null)
|
|
164
218
|
return { status: "missing", count: 0 };
|
|
@@ -212,6 +266,16 @@ export function hasAdvisorPin(manifest) {
|
|
|
212
266
|
return DEPENDENCY_BUCKETS.some((bucket) => clossysNames(manifest[bucket], new Set()) !== undefined);
|
|
213
267
|
}
|
|
214
268
|
/** Collects GitHub owner, cwd shape, default-hub presence, and the public Advisor pin. */
|
|
269
|
+
/**
|
|
270
|
+
* Reads the public `@clossys/launcher` registry version, used only to grade
|
|
271
|
+
* catalogue-sourced skill staleness in the health report (#1183). A missing
|
|
272
|
+
* or unparseable read leaves staleness ungraded rather than refusing.
|
|
273
|
+
*/
|
|
274
|
+
export function readLiveLauncherVersion(host) {
|
|
275
|
+
const viewed = host.run("npm", ["view", LAUNCHER_PACKAGE, "version"]);
|
|
276
|
+
const version = viewed.stdout.trim();
|
|
277
|
+
return viewed.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : undefined;
|
|
278
|
+
}
|
|
215
279
|
export function observeWorkspace(host) {
|
|
216
280
|
const cwd = host.cwd;
|
|
217
281
|
const ghAvailable = commandAvailable(host, "gh");
|
|
@@ -251,24 +315,26 @@ export function observeWorkspace(host) {
|
|
|
251
315
|
if (viewed.status === 0)
|
|
252
316
|
remoteDefaultHub = { owner: ownerGuess, repository: DEFAULT_REPOSITORY_NAME };
|
|
253
317
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
318
|
+
const viewedAdvisor = host.run("npm", ["view", ADVISOR_PACKAGE, "version"]);
|
|
319
|
+
const version = viewedAdvisor.stdout.trim();
|
|
320
|
+
if (viewedAdvisor.status === 0 && /^\d+\.\d+\.\d+$/.test(version))
|
|
321
|
+
advisorVersion = version;
|
|
322
|
+
const hubLocation = locateHub(host, cwd);
|
|
323
|
+
// While only the legacy `.clossys/` marker exists, its sibling inventory is
|
|
324
|
+
// the one resume will migrate; read from there so planning sees it too.
|
|
325
|
+
const inventoryRaw = hubLocation.migration === "legacy"
|
|
326
|
+
? host.readText(join(cwd, LEGACY_WORKSPACE_INVENTORY_REL))
|
|
327
|
+
: host.readText(join(cwd, WORKSPACE_INVENTORY_REL));
|
|
263
328
|
const cwdObservation = {
|
|
264
329
|
absolutePath: cwd,
|
|
265
330
|
empty: isEffectivelyEmpty(entries),
|
|
266
331
|
git,
|
|
267
332
|
...(githubOwner === undefined ? {} : { githubOwner }),
|
|
268
333
|
...(githubRepository === undefined ? {} : { githubRepository }),
|
|
269
|
-
...(
|
|
334
|
+
...(hubLocation.document === undefined ? {} : { hub: hubLocation.document }),
|
|
335
|
+
...(hubLocation.migration === "none" ? {} : { hubMigration: hubLocation.migration }),
|
|
270
336
|
looksLikeFoundry: looksLikeFoundry(host, cwd),
|
|
271
|
-
inventory: inspectInventory(
|
|
337
|
+
inventory: inspectInventory(inventoryRaw),
|
|
272
338
|
};
|
|
273
339
|
return {
|
|
274
340
|
cwd: cwdObservation,
|
|
@@ -332,7 +398,7 @@ function resolveAdoptInventory(host, cwd, inventoryPath) {
|
|
|
332
398
|
const trimmed = inventoryPath?.trim();
|
|
333
399
|
const onDiskPopulated = cwd.inventory?.status === "populated";
|
|
334
400
|
if (!onDiskPopulated && !trimmed) {
|
|
335
|
-
return refuse("violated", "appointing requires a populated generated hub inventory (packed template skeleton/.
|
|
401
|
+
return refuse("violated", "appointing requires a populated generated hub inventory (packed template skeleton/clossys/.state/inventory.json; the generated path does not ship), or --inventory <path> to a populated inventory document");
|
|
336
402
|
}
|
|
337
403
|
if (!trimmed)
|
|
338
404
|
return {};
|
|
@@ -361,6 +427,9 @@ function resolveAdoptInventory(host, cwd, inventoryPath) {
|
|
|
361
427
|
}
|
|
362
428
|
export function planWorkspace(observation, host, options = {}) {
|
|
363
429
|
const { cwd } = observation;
|
|
430
|
+
if (cwd.hubMigration === "indeterminate") {
|
|
431
|
+
return refuse("indeterminate", `both ${WORKSPACE_MARKER_REL} and the legacy ${LEGACY_WORKSPACE_MARKER_REL} are present; launcher never merges them silently -- remove one before resuming`);
|
|
432
|
+
}
|
|
364
433
|
const envOwner = observation.envOwner ?? (host.env.CLOSSYS_OWNER?.trim() || undefined);
|
|
365
434
|
if (envOwner !== undefined &&
|
|
366
435
|
!cwd.looksLikeFoundry &&
|
|
@@ -380,6 +449,8 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
380
449
|
repository: repoNameFromSlug(cwd.hub.repository, DEFAULT_REPOSITORY_NAME),
|
|
381
450
|
directory: cwd.absolutePath,
|
|
382
451
|
clone: false,
|
|
452
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
453
|
+
...(cwd.hubMigration === "legacy" ? { migrateFrom: "legacy" } : {}),
|
|
383
454
|
};
|
|
384
455
|
}
|
|
385
456
|
if (cwd.git && cwd.githubOwner && cwd.githubRepository) {
|
|
@@ -415,6 +486,7 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
415
486
|
repository: observation.remoteDefaultHub.repository,
|
|
416
487
|
directory: cwd.absolutePath,
|
|
417
488
|
clone: true,
|
|
489
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
418
490
|
};
|
|
419
491
|
}
|
|
420
492
|
if (!observation.ghAvailable) {
|
|
@@ -467,15 +539,20 @@ function clossysNames(bucket, extra) {
|
|
|
467
539
|
}
|
|
468
540
|
return advisor;
|
|
469
541
|
}
|
|
470
|
-
/**
|
|
471
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Pins live Advisor in `devDependencies` only. Relocates a pin left in any
|
|
544
|
+
* other bucket and overwrites a frozen version. Does not touch other
|
|
545
|
+
* `@clossys/*` names. A dedicated `{owner}/workspace` hub is named
|
|
546
|
+
* `@owner/workspace`.
|
|
547
|
+
*/
|
|
548
|
+
function mergeAdvisorPin(host, directory, skeletonRoot, advisorVersion, owner, repository) {
|
|
472
549
|
const path = join(directory, "package.json");
|
|
473
550
|
const raw = host.readText(path);
|
|
474
551
|
if (raw === null) {
|
|
475
552
|
const skeleton = host.readText(join(skeletonRoot, "package.json"));
|
|
476
553
|
if (skeleton === null)
|
|
477
554
|
throw new Error("missing skeleton package.json");
|
|
478
|
-
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner
|
|
555
|
+
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner, repository, advisorVersion }));
|
|
479
556
|
return;
|
|
480
557
|
}
|
|
481
558
|
let manifest;
|
|
@@ -488,13 +565,25 @@ function mergeAdvisorPin(host, directory, skeletonRoot, advisorVersion, reposito
|
|
|
488
565
|
catch {
|
|
489
566
|
throw new Error("existing package.json is unreadable JSON");
|
|
490
567
|
}
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
568
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
569
|
+
if (bucket === "devDependencies")
|
|
570
|
+
continue;
|
|
571
|
+
const current = manifest[bucket];
|
|
572
|
+
if (!isRecord(current) || !(ADVISOR_PACKAGE in current))
|
|
573
|
+
continue;
|
|
574
|
+
const next = { ...current };
|
|
575
|
+
delete next[ADVISOR_PACKAGE];
|
|
576
|
+
if (Object.keys(next).length === 0)
|
|
577
|
+
delete manifest[bucket];
|
|
578
|
+
else
|
|
579
|
+
manifest[bucket] = next;
|
|
580
|
+
}
|
|
495
581
|
const devDependencies = isRecord(manifest.devDependencies) ? { ...manifest.devDependencies } : {};
|
|
496
582
|
devDependencies[ADVISOR_PACKAGE] = advisorVersion;
|
|
497
583
|
manifest.devDependencies = devDependencies;
|
|
584
|
+
if (repository === DEFAULT_REPOSITORY_NAME) {
|
|
585
|
+
manifest.name = `@${owner}/${repository}`;
|
|
586
|
+
}
|
|
498
587
|
host.writeText(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
499
588
|
}
|
|
500
589
|
function copySkeleton(host, skeletonRoot, plan) {
|
|
@@ -566,7 +655,7 @@ function adoptHubFiles(host, skeletonRoot, plan) {
|
|
|
566
655
|
if (ignore !== null)
|
|
567
656
|
writeSkeletonFile(host, plan.directory, ".gitignore", ignore);
|
|
568
657
|
}
|
|
569
|
-
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.repository);
|
|
658
|
+
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.owner, plan.repository);
|
|
570
659
|
}
|
|
571
660
|
function requireZero(result, label) {
|
|
572
661
|
if (result.status !== 0) {
|
|
@@ -579,7 +668,7 @@ function requireZero(result, label) {
|
|
|
579
668
|
* registry version, marking a pin older than live as a stale-pin finding and a
|
|
580
669
|
* degraded report.
|
|
581
670
|
*/
|
|
582
|
-
export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
671
|
+
export function reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, retiredThisRun = [], migration) {
|
|
583
672
|
const extra = new Set();
|
|
584
673
|
const pins = {};
|
|
585
674
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
@@ -609,6 +698,7 @@ export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
|
609
698
|
? [{ bucket: bucket, pinned, grade: "stale", note: `pinned ${pinned} is older than live ${liveAdvisorVersion}` }]
|
|
610
699
|
: [];
|
|
611
700
|
});
|
|
701
|
+
const skillsManifest = summarizeSkillsManifest(parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL))), liveLauncherVersion, retiredThisRun);
|
|
612
702
|
return {
|
|
613
703
|
marker: readHub(host, directory) === undefined ? "missing" : "present",
|
|
614
704
|
inventory: inspectInventory(host.readText(join(directory, WORKSPACE_INVENTORY_REL))),
|
|
@@ -622,7 +712,14 @@ export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
|
622
712
|
dualPin: Object.values(pins).filter((value) => value !== undefined).length > 1,
|
|
623
713
|
extraClossys: [...extra].sort(),
|
|
624
714
|
pinFindings,
|
|
625
|
-
degraded: pinFindings.some((finding) => finding.grade === "stale")
|
|
715
|
+
degraded: pinFindings.some((finding) => finding.grade === "stale") ||
|
|
716
|
+
Object.values(pins).filter((value) => value !== undefined).length > 1 ||
|
|
717
|
+
pins.devDependencies === undefined ||
|
|
718
|
+
pins.dependencies !== undefined ||
|
|
719
|
+
pins.optionalDependencies !== undefined ||
|
|
720
|
+
pins.peerDependencies !== undefined,
|
|
721
|
+
...(migration === undefined ? {} : { migration }),
|
|
722
|
+
skillsManifest,
|
|
626
723
|
};
|
|
627
724
|
}
|
|
628
725
|
export function formatHubHealth(report) {
|
|
@@ -652,7 +749,24 @@ export function formatHubHealth(report) {
|
|
|
652
749
|
for (const skip of report.skillComposition.rosterSkipped ?? []) {
|
|
653
750
|
skillParts.push(`skill roster skipped (${skip.inventoryId}): ${skip.note}`);
|
|
654
751
|
}
|
|
752
|
+
if (report.skillComposition.retired !== undefined && report.skillComposition.retired.length > 0) {
|
|
753
|
+
skillParts.push(`skills retired: ${report.skillComposition.retired.map((name) => `clossys-${name}`).join(", ")}`);
|
|
754
|
+
}
|
|
655
755
|
}
|
|
756
|
+
const skillsManifestLine = report.skillsManifest === undefined
|
|
757
|
+
? undefined
|
|
758
|
+
: report.skillsManifest.status === "missing"
|
|
759
|
+
? "skills manifest: missing"
|
|
760
|
+
: `skills: ${report.skillsManifest.stale} out of date, ${report.skillsManifest.retired} retired (${report.skillsManifest.total} composed)`;
|
|
761
|
+
const migrationLine = report.migration === undefined ? undefined : `migration: moved hub state from ${report.migration.from} to ${report.migration.to}`;
|
|
762
|
+
const linkedHostsLine = report.linkedHosts === undefined
|
|
763
|
+
? undefined
|
|
764
|
+
: `linked hosts: ${report.linkedHosts.length === 0 ? "none detected" : report.linkedHosts.join(", ")}`;
|
|
765
|
+
const inventoryDriftLine = report.inventoryDrift === undefined
|
|
766
|
+
? undefined
|
|
767
|
+
: report.inventoryDrift.status === "indeterminate"
|
|
768
|
+
? `inventory drift: indeterminate${report.inventoryDrift.note === undefined ? "" : ` -- ${report.inventoryDrift.note}`}`
|
|
769
|
+
: `inventory drift: external-only ${report.inventoryDrift.externalOnly.length}, launcher-only ${report.inventoryDrift.launcherOnly.length}, agreeing ${report.inventoryDrift.agreeing.length}`;
|
|
656
770
|
return [
|
|
657
771
|
`hub marker: ${report.marker}`,
|
|
658
772
|
`inventory: ${inventory}`,
|
|
@@ -661,14 +775,23 @@ export function formatHubHealth(report) {
|
|
|
661
775
|
`extra @clossys/*: ${extra}`,
|
|
662
776
|
`pin findings: ${findingLine}`,
|
|
663
777
|
`degraded: ${report.degraded ? "yes" : "no"}`,
|
|
778
|
+
...(migrationLine === undefined ? [] : [migrationLine]),
|
|
779
|
+
...(linkedHostsLine === undefined ? [] : [linkedHostsLine]),
|
|
780
|
+
...(inventoryDriftLine === undefined ? [] : [inventoryDriftLine]),
|
|
781
|
+
...(skillsManifestLine === undefined ? [] : [skillsManifestLine]),
|
|
664
782
|
...(skillParts.length === 0 ? [] : skillParts),
|
|
665
783
|
`health: ${JSON.stringify(report)}`,
|
|
666
784
|
].join("\n");
|
|
667
785
|
}
|
|
668
|
-
function withHealth(host, directory, headline, liveAdvisorVersion, skillComposition) {
|
|
786
|
+
function withHealth(host, directory, headline, liveAdvisorVersion, skillComposition, liveLauncherVersion, migration, inventoryDrift) {
|
|
787
|
+
const base = reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, skillComposition?.retired ?? [], migration);
|
|
788
|
+
const rosterSkipped = skillComposition?.rosterSkipped ?? [];
|
|
669
789
|
const health = {
|
|
670
|
-
...
|
|
790
|
+
...base,
|
|
671
791
|
...(skillComposition === undefined ? {} : { skillComposition }),
|
|
792
|
+
...(skillComposition?.linkedHosts === undefined ? {} : { linkedHosts: skillComposition.linkedHosts }),
|
|
793
|
+
...(inventoryDrift === undefined || inventoryDrift.status === "no-external-source" ? {} : { inventoryDrift }),
|
|
794
|
+
degraded: base.degraded || rosterSkipped.length > 0,
|
|
672
795
|
};
|
|
673
796
|
return {
|
|
674
797
|
state: "satisfied",
|
|
@@ -798,55 +921,179 @@ function resolveSisterCloneTargets(host, hubDirectory, hubOwner) {
|
|
|
798
921
|
}
|
|
799
922
|
return { targets, skipped };
|
|
800
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Explicit, approved action (#1179, the #1045 pattern): clones every
|
|
926
|
+
* inventoried repository that resolveSisterCloneTargets's own skip pass
|
|
927
|
+
* identified as "just needs a clone" (CLONE_NOT_BESIDE_HUB_NOTE), and only
|
|
928
|
+
* those -- every other skip reason (wrong account, foundry supplier tree,
|
|
929
|
+
* origin mismatch, invalid slug) is left exactly as skipped, never
|
|
930
|
+
* attempted. Never called from resume's default path; only from the
|
|
931
|
+
* --clone-missing flag. Reverses the launcher README's own no-clone
|
|
932
|
+
* default for exactly this one approved action.
|
|
933
|
+
*/
|
|
934
|
+
export function cloneMissingInventoryRepositories(host, hubDirectory, hubOwner) {
|
|
935
|
+
const { skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
936
|
+
const parent = dirname(resolve(hubDirectory));
|
|
937
|
+
const outcomes = [];
|
|
938
|
+
for (const skip of skipped) {
|
|
939
|
+
if (skip.note !== CLONE_NOT_BESIDE_HUB_NOTE) {
|
|
940
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "skipped-other-reason", note: skip.note });
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const parsed = parseInventoryRepositoryId(skip.inventoryId, hubOwner);
|
|
944
|
+
if (parsed === null) {
|
|
945
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "failed", note: "inventory id is not a valid repository slug" });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const siblingPath = join(parent, parsed.repository);
|
|
949
|
+
const result = host.run("gh", ["repo", "clone", `${hubOwner}/${parsed.repository}`, siblingPath]);
|
|
950
|
+
if (result.status === 0) {
|
|
951
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "cloned", note: `cloned to ${siblingPath}` });
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
outcomes.push({
|
|
955
|
+
inventoryId: skip.inventoryId,
|
|
956
|
+
result: "failed",
|
|
957
|
+
note: `gh repo clone exited ${result.status ?? "null"}: ${result.stderr.trim() || "no stderr"}`,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return outcomes;
|
|
962
|
+
}
|
|
801
963
|
function hubRosterId(host, hubDirectory, hubOwner, hubRepository) {
|
|
802
964
|
const document = readHub(host, hubDirectory);
|
|
803
965
|
if (document !== undefined)
|
|
804
966
|
return document.repository;
|
|
805
967
|
return `${hubOwner}/${hubRepository}`;
|
|
806
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* Regenerates the generated `README.md` at the root of `clossys/`: an index of which `clossys/<role>/`
|
|
971
|
+
* folders are active here, and what `.state/` holds. Written on every apply
|
|
972
|
+
* so it never drifts from what is actually on disk (#1171).
|
|
973
|
+
*/
|
|
974
|
+
function writeClossysReadme(host, directory) {
|
|
975
|
+
const root = join(directory, CLOSSYS_DIR_REL);
|
|
976
|
+
const roles = host
|
|
977
|
+
.isDirectory(root)
|
|
978
|
+
? host
|
|
979
|
+
.readDir(root)
|
|
980
|
+
.filter((name) => name !== ".state" && name !== "README.md" && host.isDirectory(join(root, name)))
|
|
981
|
+
.sort((a, b) => a.localeCompare(b))
|
|
982
|
+
: [];
|
|
983
|
+
const hasBrief = host.exists(join(root, "brief.json"));
|
|
984
|
+
const lines = [
|
|
985
|
+
"# clossys/",
|
|
986
|
+
"",
|
|
987
|
+
"Generated by `@clossys/launcher`. This file is rewritten on every launcher",
|
|
988
|
+
`run to reflect what is active here; do not edit it by hand. Last generated: ${host.now()}.`,
|
|
989
|
+
"",
|
|
990
|
+
"## Engagement brief",
|
|
991
|
+
"",
|
|
992
|
+
...(hasBrief
|
|
993
|
+
? ["`clossys/brief.json` — why each role is staffed here, its goals, handoffs, and sequence (owner: @clossys/advisor)."]
|
|
994
|
+
: ["No engagement brief yet. `@clossys-advisor` writes `clossys/brief.json` once a plan is approved."]),
|
|
995
|
+
"",
|
|
996
|
+
"## Active roles",
|
|
997
|
+
"",
|
|
998
|
+
...(roles.length === 0
|
|
999
|
+
? ["No role folder is active here yet."]
|
|
1000
|
+
: roles.map((role) => `- \`clossys/${role}/\` — @clossys-${role}`)),
|
|
1001
|
+
"",
|
|
1002
|
+
"## Machine state",
|
|
1003
|
+
"",
|
|
1004
|
+
"`clossys/.state/` holds machine files only: the hub marker, the inventory,",
|
|
1005
|
+
"and the skills manifest. It is visible so it is easy to find, but it is not",
|
|
1006
|
+
"a place a person edits by hand.",
|
|
1007
|
+
"",
|
|
1008
|
+
];
|
|
1009
|
+
writeSkeletonFile(host, directory, CLOSSYS_README_REL, lines.join("\n"));
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Reads which coding-agent hosts already had skill discovery linked in
|
|
1013
|
+
* `directory` BEFORE this call, then records that snapshot to
|
|
1014
|
+
* `clossys/.state/hosts.json` (#1180). Deliberately called ahead of
|
|
1015
|
+
* `composeSkills`, which unconditionally stamps discovery links for every
|
|
1016
|
+
* host once it runs -- reading afterward would report "all hosts" on every
|
|
1017
|
+
* apply and make the record meaningless.
|
|
1018
|
+
*/
|
|
1019
|
+
function recordLinkedHosts(host, directory) {
|
|
1020
|
+
const linkedHosts = detectLinkedHosts(host, directory);
|
|
1021
|
+
writeSkeletonFile(host, directory, HOSTS_REL, serializeHostRecord({ schemaVersion: 1, linkedHosts, recordedAt: host.now() }));
|
|
1022
|
+
return linkedHosts;
|
|
1023
|
+
}
|
|
807
1024
|
function composeSkillRoster(host, hubDirectory, hubOwner, hubRepository, options) {
|
|
808
|
-
const
|
|
1025
|
+
const composeOptions = {
|
|
809
1026
|
launcherPackageRoot: options.launcherPackageRoot,
|
|
810
1027
|
...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
|
|
811
|
-
|
|
1028
|
+
...(options.contractPath === undefined ? {} : { contractPath: options.contractPath }),
|
|
1029
|
+
};
|
|
1030
|
+
const linkedHosts = recordLinkedHosts(host, hubDirectory);
|
|
1031
|
+
const hubSkill = composeSkills(host, hubDirectory, composeOptions);
|
|
812
1032
|
writeConsumerAgentsIfNeeded(host, hubDirectory);
|
|
1033
|
+
writeClossysReadme(host, hubDirectory);
|
|
813
1034
|
const hubId = hubRosterId(host, hubDirectory, hubOwner, hubRepository);
|
|
814
1035
|
const rosterTargets = [hubId];
|
|
815
1036
|
const { targets, skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
816
1037
|
for (const target of targets) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
|
|
820
|
-
});
|
|
1038
|
+
recordLinkedHosts(host, target.directory);
|
|
1039
|
+
composeSkills(host, target.directory, composeOptions);
|
|
821
1040
|
writeSisterConsumerAgentsIfNeeded(host, target.directory);
|
|
822
1041
|
rosterTargets.push(target.inventoryId);
|
|
823
1042
|
}
|
|
824
|
-
return { ...hubSkill, rosterTargets, rosterSkipped: skipped };
|
|
1043
|
+
return { ...hubSkill, rosterTargets, rosterSkipped: skipped, linkedHosts };
|
|
825
1044
|
}
|
|
826
|
-
function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner, hubRepository, liveAdvisorVersion, skillCatalogueRoot) {
|
|
1045
|
+
function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner, hubRepository, liveAdvisorVersion, skillCatalogueRoot, contractPath, liveLauncherVersion, migration) {
|
|
827
1046
|
const skillComposition = composeSkillRoster(host, directory, hubOwner, hubRepository, {
|
|
828
1047
|
launcherPackageRoot,
|
|
829
1048
|
...(skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot }),
|
|
1049
|
+
...(contractPath === undefined ? {} : { contractPath }),
|
|
830
1050
|
});
|
|
831
|
-
|
|
1051
|
+
// #1216: when the hub marker declares an external inventory, report drift against
|
|
1052
|
+
// it on every apply (create's fresh marker never declares one, so this is a no-op there).
|
|
1053
|
+
const hubDocument = readHub(host, directory);
|
|
1054
|
+
const inventoryDrift = reportInventoryDrift(host, directory, hubDocument?.externalInventory, WORKSPACE_INVENTORY_REL);
|
|
1055
|
+
return withHealth(host, directory, headline, liveAdvisorVersion, skillComposition, liveLauncherVersion, migration, inventoryDrift);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Migrates a legacy `.clossys/` hub marker (and its sibling inventory, when
|
|
1059
|
+
* present) to `clossys/.state/`, then removes the old directory. Called only
|
|
1060
|
+
* when `locateHub` found the marker at the legacy path and nowhere else
|
|
1061
|
+
* (`plan.migrateFrom === "legacy"`); a hub with markers at both paths is
|
|
1062
|
+
* refused by `planWorkspace` before apply ever runs, so this never merges
|
|
1063
|
+
* two hub states.
|
|
1064
|
+
*/
|
|
1065
|
+
function migrateLegacyHubState(host, directory) {
|
|
1066
|
+
const markerRaw = host.readText(join(directory, LEGACY_WORKSPACE_MARKER_REL));
|
|
1067
|
+
if (markerRaw === null)
|
|
1068
|
+
return undefined;
|
|
1069
|
+
writeSkeletonFile(host, directory, WORKSPACE_MARKER_REL, markerRaw.endsWith("\n") ? markerRaw : `${markerRaw}\n`);
|
|
1070
|
+
const inventoryRaw = host.readText(join(directory, LEGACY_WORKSPACE_INVENTORY_REL));
|
|
1071
|
+
if (inventoryRaw !== null) {
|
|
1072
|
+
writeSkeletonFile(host, directory, WORKSPACE_INVENTORY_REL, inventoryRaw.endsWith("\n") ? inventoryRaw : `${inventoryRaw}\n`);
|
|
1073
|
+
}
|
|
1074
|
+
host.remove(join(directory, LEGACY_STATE_DIR_REL));
|
|
1075
|
+
return { status: "migrated", from: LEGACY_STATE_DIR_REL, to: STATE_DIR_REL };
|
|
832
1076
|
}
|
|
833
1077
|
/** Applies a create, resume, or adopt plan through the host. Resume refreshes composed skills and stale AGENTS.md guidance. */
|
|
834
1078
|
export function applyWorkspacePlan(host, plan, skeletonRoot, options = {}) {
|
|
835
1079
|
const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
|
|
836
1080
|
const skillCatalogueRoot = options.skillCatalogueRoot;
|
|
1081
|
+
const contractPath = options.contractPath;
|
|
1082
|
+
const liveLauncherVersion = options.liveLauncherVersion;
|
|
837
1083
|
if (plan.action === "resume") {
|
|
838
1084
|
if (plan.clone) {
|
|
839
1085
|
requireZero(host.run("gh", ["repo", "clone", `${plan.owner}/${plan.repository}`, plan.directory]), "gh repo clone");
|
|
840
1086
|
}
|
|
841
|
-
|
|
1087
|
+
const migration = plan.migrateFrom === "legacy" ? migrateLegacyHubState(host, plan.directory) : undefined;
|
|
1088
|
+
return finishHubApply(host, plan.directory, `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.`, launcherPackageRoot, plan.owner, plan.repository, plan.advisorVersion, skillCatalogueRoot, contractPath, liveLauncherVersion, migration);
|
|
842
1089
|
}
|
|
843
1090
|
if (plan.action === "create") {
|
|
844
1091
|
copySkeleton(host, skeletonRoot, plan);
|
|
845
1092
|
requireZero(host.run("gh", ["repo", "create", `${plan.owner}/${plan.repository}`, "--private", "--source", plan.directory, "--remote", "origin", "--push"], { cwd: plan.directory }), "gh repo create");
|
|
846
|
-
return finishHubApply(host, plan.directory, `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.`, launcherPackageRoot, plan.owner, plan.repository, plan.advisorVersion, skillCatalogueRoot);
|
|
1093
|
+
return finishHubApply(host, plan.directory, `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.`, launcherPackageRoot, plan.owner, plan.repository, plan.advisorVersion, skillCatalogueRoot, contractPath, liveLauncherVersion);
|
|
847
1094
|
}
|
|
848
1095
|
adoptHubFiles(host, skeletonRoot, plan);
|
|
849
|
-
return finishHubApply(host, plan.directory, `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.`, launcherPackageRoot, plan.owner, plan.repository, plan.advisorVersion, skillCatalogueRoot);
|
|
1096
|
+
return finishHubApply(host, plan.directory, `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.`, launcherPackageRoot, plan.owner, plan.repository, plan.advisorVersion, skillCatalogueRoot, contractPath, liveLauncherVersion);
|
|
850
1097
|
}
|
|
851
1098
|
export function launcherPackageRootFromModule(moduleUrl) {
|
|
852
1099
|
return resolve(dirname(fileURLToPath(moduleUrl)), "..");
|