@clossys/launcher 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/README.md +231 -19
- package/contracts/conversation-contract.md +40 -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 +7 -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 +43 -14
- 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 +50 -8
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +458 -40
- 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 +19 -1
- 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 +39 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +197 -0
- package/dist/skills.js.map +1 -0
- package/dist/types.d.ts +78 -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 +11 -5
- package/skeleton/README.md +15 -20
- package/skeleton/package.json +1 -1
- package/skill/SKILL.md +53 -0
- package/skill-catalogue/advisor/SKILL.md +52 -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 +50 -0
- package/skill-catalogue/customer/SKILL.md +92 -0
- package/skill-catalogue/designer/SKILL.md +66 -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 +64 -0
- package/skill-catalogue/starter/SKILL.md +48 -0
- package/skill-catalogue/strategist/SKILL.md +85 -0
- package/skill-catalogue/writer/SKILL.md +56 -0
- package/src/apply-plan-cli.ts +94 -0
- package/src/apply-plan.ts +172 -0
- package/src/check-cli.ts +7 -0
- package/src/cli.ts +51 -13
- package/src/contract.ts +81 -0
- package/src/core.ts +555 -38
- package/src/doctor-cli.ts +33 -0
- package/src/doctor.ts +145 -0
- package/src/host.ts +17 -1
- package/src/hosts.ts +79 -0
- package/src/index.ts +36 -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 +225 -0
- package/src/types.ts +74 -1
- /package/skeleton/{.clossys → clossys/.state}/inventory.json +0 -0
- /package/skeleton/{.clossys → clossys/.state}/workspace.json +0 -0
package/dist/core.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
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";
|
|
3
7
|
export const DEFAULT_REPOSITORY_NAME = "workspace";
|
|
4
|
-
|
|
5
|
-
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");
|
|
6
19
|
export const ADVISOR_PACKAGE = "@clossys/advisor";
|
|
20
|
+
export const LAUNCHER_PACKAGE = "@clossys/launcher";
|
|
7
21
|
const DEPENDENCY_BUCKETS = [
|
|
8
22
|
"dependencies",
|
|
9
23
|
"devDependencies",
|
|
@@ -22,10 +36,52 @@ const SKELETON_FILES = [
|
|
|
22
36
|
/** Written at generate time so this package never ships a nested AGENTS.md. */
|
|
23
37
|
export const CONSUMER_AGENTS_MD = `# Account workspace
|
|
24
38
|
|
|
39
|
+
This folder is the account hub for Foundry packages.
|
|
40
|
+
|
|
41
|
+
After \`npx @clossys/launcher\`, the same \`@clossys-*\` team is composed in
|
|
42
|
+
every inventoried checkout beside this hub. Talk with \`@clossys-advisor\` and
|
|
43
|
+
\`@clossys-<package>\` here or in a product repository. A missing \`@\` mention
|
|
44
|
+
is not how we signal incompatibility — \`@clossys-advisor\` is the hiring check.
|
|
45
|
+
|
|
46
|
+
Run \`npx @clossys/launcher\` again for hub health and to refresh voices on
|
|
47
|
+
clones next to the hub, not as how you talk to packages.
|
|
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
|
+
|
|
53
|
+
Advisor is read-only until the sponsor approves a next action.
|
|
54
|
+
`;
|
|
55
|
+
/** Canned guidance for inventoried product checkouts (not the hub). */
|
|
56
|
+
export const SISTER_CONSUMER_AGENTS_MD = `# Product repository
|
|
57
|
+
|
|
58
|
+
This repository is part of the same account engagement. The same
|
|
59
|
+
\`@clossys-<package>\` team is here for intro and questions;
|
|
60
|
+
\`@clossys-advisor\` decides hiring and compatibility. This folder is not the
|
|
61
|
+
hub — engines are hired per repository, not dumped here.
|
|
62
|
+
`;
|
|
63
|
+
/** Previous generate-time guidance; used to refresh stale hub AGENTS.md on resume. */
|
|
64
|
+
export const LEGACY_CONSUMER_AGENTS_MD = `# Account workspace
|
|
65
|
+
|
|
25
66
|
This repository is the account hub for Foundry packages. It inventories
|
|
26
67
|
where packages are installed and coordinates engagement. It is not a
|
|
27
68
|
product application and does not need the whole catalogue installed here.
|
|
28
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
|
+
|
|
29
85
|
Open this folder in your coding agent. Advisor is read-only until you
|
|
30
86
|
approve a next action.
|
|
31
87
|
|
|
@@ -122,8 +178,8 @@ export function isHubDocument(value) {
|
|
|
122
178
|
return false;
|
|
123
179
|
return true;
|
|
124
180
|
}
|
|
125
|
-
function
|
|
126
|
-
const raw = host.readText(
|
|
181
|
+
function readHubAt(host, path) {
|
|
182
|
+
const raw = host.readText(path);
|
|
127
183
|
if (raw === null)
|
|
128
184
|
return undefined;
|
|
129
185
|
try {
|
|
@@ -134,7 +190,29 @@ function readHub(host, directory) {
|
|
|
134
190
|
return undefined;
|
|
135
191
|
}
|
|
136
192
|
}
|
|
137
|
-
/**
|
|
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. */
|
|
138
216
|
export function inspectInventory(raw) {
|
|
139
217
|
if (raw === null)
|
|
140
218
|
return { status: "missing", count: 0 };
|
|
@@ -188,6 +266,16 @@ export function hasAdvisorPin(manifest) {
|
|
|
188
266
|
return DEPENDENCY_BUCKETS.some((bucket) => clossysNames(manifest[bucket], new Set()) !== undefined);
|
|
189
267
|
}
|
|
190
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
|
+
}
|
|
191
279
|
export function observeWorkspace(host) {
|
|
192
280
|
const cwd = host.cwd;
|
|
193
281
|
const ghAvailable = commandAvailable(host, "gh");
|
|
@@ -227,24 +315,26 @@ export function observeWorkspace(host) {
|
|
|
227
315
|
if (viewed.status === 0)
|
|
228
316
|
remoteDefaultHub = { owner: ownerGuess, repository: DEFAULT_REPOSITORY_NAME };
|
|
229
317
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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));
|
|
239
328
|
const cwdObservation = {
|
|
240
329
|
absolutePath: cwd,
|
|
241
330
|
empty: isEffectivelyEmpty(entries),
|
|
242
331
|
git,
|
|
243
332
|
...(githubOwner === undefined ? {} : { githubOwner }),
|
|
244
333
|
...(githubRepository === undefined ? {} : { githubRepository }),
|
|
245
|
-
...(
|
|
334
|
+
...(hubLocation.document === undefined ? {} : { hub: hubLocation.document }),
|
|
335
|
+
...(hubLocation.migration === "none" ? {} : { hubMigration: hubLocation.migration }),
|
|
246
336
|
looksLikeFoundry: looksLikeFoundry(host, cwd),
|
|
247
|
-
inventory: inspectInventory(
|
|
337
|
+
inventory: inspectInventory(inventoryRaw),
|
|
248
338
|
};
|
|
249
339
|
return {
|
|
250
340
|
cwd: cwdObservation,
|
|
@@ -278,7 +368,7 @@ function resolveOwner(observation, host) {
|
|
|
278
368
|
* Decides create, resume, or adopt from a cwd observation.
|
|
279
369
|
* Appointing means: run this from the GitHub repository that should own the hub.
|
|
280
370
|
*/
|
|
281
|
-
function readInventoryRepositories(host, source, label) {
|
|
371
|
+
export function readInventoryRepositories(host, source, label) {
|
|
282
372
|
const raw = host.readText(source);
|
|
283
373
|
if (raw === null)
|
|
284
374
|
return [];
|
|
@@ -308,7 +398,7 @@ function resolveAdoptInventory(host, cwd, inventoryPath) {
|
|
|
308
398
|
const trimmed = inventoryPath?.trim();
|
|
309
399
|
const onDiskPopulated = cwd.inventory?.status === "populated";
|
|
310
400
|
if (!onDiskPopulated && !trimmed) {
|
|
311
|
-
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");
|
|
312
402
|
}
|
|
313
403
|
if (!trimmed)
|
|
314
404
|
return {};
|
|
@@ -337,6 +427,9 @@ function resolveAdoptInventory(host, cwd, inventoryPath) {
|
|
|
337
427
|
}
|
|
338
428
|
export function planWorkspace(observation, host, options = {}) {
|
|
339
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
|
+
}
|
|
340
433
|
const envOwner = observation.envOwner ?? (host.env.CLOSSYS_OWNER?.trim() || undefined);
|
|
341
434
|
if (envOwner !== undefined &&
|
|
342
435
|
!cwd.looksLikeFoundry &&
|
|
@@ -356,6 +449,8 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
356
449
|
repository: repoNameFromSlug(cwd.hub.repository, DEFAULT_REPOSITORY_NAME),
|
|
357
450
|
directory: cwd.absolutePath,
|
|
358
451
|
clone: false,
|
|
452
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
453
|
+
...(cwd.hubMigration === "legacy" ? { migrateFrom: "legacy" } : {}),
|
|
359
454
|
};
|
|
360
455
|
}
|
|
361
456
|
if (cwd.git && cwd.githubOwner && cwd.githubRepository) {
|
|
@@ -391,6 +486,7 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
391
486
|
repository: observation.remoteDefaultHub.repository,
|
|
392
487
|
directory: cwd.absolutePath,
|
|
393
488
|
clone: true,
|
|
489
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
394
490
|
};
|
|
395
491
|
}
|
|
396
492
|
if (!observation.ghAvailable) {
|
|
@@ -411,7 +507,7 @@ function containedPath(root, relativePath) {
|
|
|
411
507
|
const resolved = resolve(root, relativePath);
|
|
412
508
|
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
413
509
|
if (resolved !== root && !resolved.startsWith(prefix)) {
|
|
414
|
-
throw new Error(`refusing to write outside the
|
|
510
|
+
throw new Error(`refusing to write outside the workspace directory: ${relativePath}`);
|
|
415
511
|
}
|
|
416
512
|
return resolved;
|
|
417
513
|
}
|
|
@@ -443,15 +539,20 @@ function clossysNames(bucket, extra) {
|
|
|
443
539
|
}
|
|
444
540
|
return advisor;
|
|
445
541
|
}
|
|
446
|
-
/**
|
|
447
|
-
|
|
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) {
|
|
448
549
|
const path = join(directory, "package.json");
|
|
449
550
|
const raw = host.readText(path);
|
|
450
551
|
if (raw === null) {
|
|
451
552
|
const skeleton = host.readText(join(skeletonRoot, "package.json"));
|
|
452
553
|
if (skeleton === null)
|
|
453
554
|
throw new Error("missing skeleton package.json");
|
|
454
|
-
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner
|
|
555
|
+
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner, repository, advisorVersion }));
|
|
455
556
|
return;
|
|
456
557
|
}
|
|
457
558
|
let manifest;
|
|
@@ -464,13 +565,25 @@ function mergeAdvisorPin(host, directory, skeletonRoot, advisorVersion, reposito
|
|
|
464
565
|
catch {
|
|
465
566
|
throw new Error("existing package.json is unreadable JSON");
|
|
466
567
|
}
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
581
|
const devDependencies = isRecord(manifest.devDependencies) ? { ...manifest.devDependencies } : {};
|
|
472
582
|
devDependencies[ADVISOR_PACKAGE] = advisorVersion;
|
|
473
583
|
manifest.devDependencies = devDependencies;
|
|
584
|
+
if (repository === DEFAULT_REPOSITORY_NAME) {
|
|
585
|
+
manifest.name = `@${owner}/${repository}`;
|
|
586
|
+
}
|
|
474
587
|
host.writeText(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
475
588
|
}
|
|
476
589
|
function copySkeleton(host, skeletonRoot, plan) {
|
|
@@ -526,6 +639,9 @@ function adoptHubFiles(host, skeletonRoot, plan) {
|
|
|
526
639
|
if (host.readText(join(plan.directory, "AGENTS.md")) === null) {
|
|
527
640
|
writeSkeletonFile(host, plan.directory, "AGENTS.md", CONSUMER_AGENTS_MD);
|
|
528
641
|
}
|
|
642
|
+
else {
|
|
643
|
+
writeConsumerAgentsIfNeeded(host, plan.directory);
|
|
644
|
+
}
|
|
529
645
|
if (host.readText(join(plan.directory, "CLAUDE.md")) === null) {
|
|
530
646
|
writeSkeletonFile(host, plan.directory, "CLAUDE.md", CONSUMER_CLAUDE_MD);
|
|
531
647
|
}
|
|
@@ -539,7 +655,7 @@ function adoptHubFiles(host, skeletonRoot, plan) {
|
|
|
539
655
|
if (ignore !== null)
|
|
540
656
|
writeSkeletonFile(host, plan.directory, ".gitignore", ignore);
|
|
541
657
|
}
|
|
542
|
-
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.repository);
|
|
658
|
+
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.owner, plan.repository);
|
|
543
659
|
}
|
|
544
660
|
function requireZero(result, label) {
|
|
545
661
|
if (result.status !== 0) {
|
|
@@ -552,7 +668,7 @@ function requireZero(result, label) {
|
|
|
552
668
|
* registry version, marking a pin older than live as a stale-pin finding and a
|
|
553
669
|
* degraded report.
|
|
554
670
|
*/
|
|
555
|
-
export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
671
|
+
export function reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, retiredThisRun = [], migration) {
|
|
556
672
|
const extra = new Set();
|
|
557
673
|
const pins = {};
|
|
558
674
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
@@ -582,6 +698,7 @@ export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
|
582
698
|
? [{ bucket: bucket, pinned, grade: "stale", note: `pinned ${pinned} is older than live ${liveAdvisorVersion}` }]
|
|
583
699
|
: [];
|
|
584
700
|
});
|
|
701
|
+
const skillsManifest = summarizeSkillsManifest(parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL))), liveLauncherVersion, retiredThisRun);
|
|
585
702
|
return {
|
|
586
703
|
marker: readHub(host, directory) === undefined ? "missing" : "present",
|
|
587
704
|
inventory: inspectInventory(host.readText(join(directory, WORKSPACE_INVENTORY_REL))),
|
|
@@ -595,7 +712,14 @@ export function reportHubHealth(host, directory, liveAdvisorVersion) {
|
|
|
595
712
|
dualPin: Object.values(pins).filter((value) => value !== undefined).length > 1,
|
|
596
713
|
extraClossys: [...extra].sort(),
|
|
597
714
|
pinFindings,
|
|
598
|
-
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,
|
|
599
723
|
};
|
|
600
724
|
}
|
|
601
725
|
export function formatHubHealth(report) {
|
|
@@ -611,6 +735,38 @@ export function formatHubHealth(report) {
|
|
|
611
735
|
const inventory = report.inventory.status === "populated" ? `populated (${report.inventory.count})` : report.inventory.status;
|
|
612
736
|
const findings = report.pinFindings.map((finding) => finding.note !== undefined ? `${finding.bucket} ${finding.note}` : `${finding.bucket} ${finding.grade}`);
|
|
613
737
|
const findingLine = findings.length === 0 ? "none" : findings.join("; ");
|
|
738
|
+
const skillParts = [];
|
|
739
|
+
if (report.skillComposition !== undefined) {
|
|
740
|
+
skillParts.push(report.skillComposition.composed.length === 0
|
|
741
|
+
? "skills composed: none"
|
|
742
|
+
: `skills composed: ${report.skillComposition.composed.map((name) => `clossys-${name}`).join(", ")}`);
|
|
743
|
+
for (const skip of report.skillComposition.skipped) {
|
|
744
|
+
skillParts.push(`skill skipped (${skip.packageDir}): ${skip.note}`);
|
|
745
|
+
}
|
|
746
|
+
if (report.skillComposition.rosterTargets !== undefined && report.skillComposition.rosterTargets.length > 0) {
|
|
747
|
+
skillParts.push(`skill roster written: ${report.skillComposition.rosterTargets.join(", ")}`);
|
|
748
|
+
}
|
|
749
|
+
for (const skip of report.skillComposition.rosterSkipped ?? []) {
|
|
750
|
+
skillParts.push(`skill roster skipped (${skip.inventoryId}): ${skip.note}`);
|
|
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
|
+
}
|
|
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}`;
|
|
614
770
|
return [
|
|
615
771
|
`hub marker: ${report.marker}`,
|
|
616
772
|
`inventory: ${inventory}`,
|
|
@@ -619,9 +775,30 @@ export function formatHubHealth(report) {
|
|
|
619
775
|
`extra @clossys/*: ${extra}`,
|
|
620
776
|
`pin findings: ${findingLine}`,
|
|
621
777
|
`degraded: ${report.degraded ? "yes" : "no"}`,
|
|
778
|
+
...(migrationLine === undefined ? [] : [migrationLine]),
|
|
779
|
+
...(linkedHostsLine === undefined ? [] : [linkedHostsLine]),
|
|
780
|
+
...(inventoryDriftLine === undefined ? [] : [inventoryDriftLine]),
|
|
781
|
+
...(skillsManifestLine === undefined ? [] : [skillsManifestLine]),
|
|
782
|
+
...(skillParts.length === 0 ? [] : skillParts),
|
|
622
783
|
`health: ${JSON.stringify(report)}`,
|
|
623
784
|
].join("\n");
|
|
624
785
|
}
|
|
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 ?? [];
|
|
789
|
+
const health = {
|
|
790
|
+
...base,
|
|
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,
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
state: "satisfied",
|
|
798
|
+
message: `${headline}\n${formatHubHealth(health)}`,
|
|
799
|
+
health,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
625
802
|
/**
|
|
626
803
|
* Read-only inventory validation: runs `gh repo view --json name` for each
|
|
627
804
|
* repository id, batched. Tolerates a missing or failing `gh` by skipping with
|
|
@@ -656,29 +833,270 @@ export function checkInventoryEntries(host, directory) {
|
|
|
656
833
|
}
|
|
657
834
|
return { entries, skipped: false };
|
|
658
835
|
}
|
|
659
|
-
function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
836
|
+
function shouldRefreshConsumerAgents(existing) {
|
|
837
|
+
if (existing === null)
|
|
838
|
+
return true;
|
|
839
|
+
if (existing === CONSUMER_AGENTS_MD)
|
|
840
|
+
return false;
|
|
841
|
+
if (existing === LEGACY_CONSUMER_AGENTS_MD)
|
|
842
|
+
return true;
|
|
843
|
+
if (existing.includes("Run `npx @clossys/launcher` again to resume"))
|
|
844
|
+
return true;
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
function writeConsumerAgentsIfNeeded(host, directory) {
|
|
848
|
+
const existing = host.readText(join(directory, "AGENTS.md"));
|
|
849
|
+
if (!shouldRefreshConsumerAgents(existing))
|
|
850
|
+
return;
|
|
851
|
+
writeSkeletonFile(host, directory, "AGENTS.md", CONSUMER_AGENTS_MD);
|
|
852
|
+
}
|
|
853
|
+
function writeSisterConsumerAgentsIfNeeded(host, directory) {
|
|
854
|
+
const existing = host.readText(join(directory, "AGENTS.md"));
|
|
855
|
+
if (existing !== null && existing.trim() !== "" && existing !== SISTER_CONSUMER_AGENTS_MD)
|
|
856
|
+
return;
|
|
857
|
+
writeSkeletonFile(host, directory, "AGENTS.md", SISTER_CONSUMER_AGENTS_MD);
|
|
858
|
+
}
|
|
859
|
+
const CLONE_NOT_BESIDE_HUB_NOTE = "clone not next to the hub; voices appear here after this repository is cloned beside the hub and launcher resumes";
|
|
860
|
+
function parseInventoryRepositoryId(id, hubOwner) {
|
|
861
|
+
const trimmed = id.trim();
|
|
862
|
+
if (trimmed === "")
|
|
863
|
+
return null;
|
|
864
|
+
if (trimmed.includes("/")) {
|
|
865
|
+
const slash = trimmed.indexOf("/");
|
|
866
|
+
const owner = trimmed.slice(0, slash);
|
|
867
|
+
const repository = trimmed.slice(slash + 1);
|
|
868
|
+
if (!OWNER.test(owner) || !REPO.test(repository))
|
|
869
|
+
return null;
|
|
870
|
+
return { owner, repository };
|
|
871
|
+
}
|
|
872
|
+
if (!REPO.test(trimmed))
|
|
873
|
+
return null;
|
|
874
|
+
return { owner: hubOwner, repository: trimmed };
|
|
875
|
+
}
|
|
876
|
+
function resolveSisterCloneTargets(host, hubDirectory, hubOwner) {
|
|
877
|
+
const parent = dirname(resolve(hubDirectory));
|
|
878
|
+
const hubResolved = resolve(hubDirectory);
|
|
879
|
+
const skipped = [];
|
|
880
|
+
const targets = [];
|
|
881
|
+
const inventoryPath = join(hubDirectory, WORKSPACE_INVENTORY_REL);
|
|
882
|
+
let inventoryIds;
|
|
883
|
+
try {
|
|
884
|
+
inventoryIds = readInventoryRepositories(host, inventoryPath, "the hub inventory");
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
return { targets: [], skipped: [] };
|
|
888
|
+
}
|
|
889
|
+
for (const id of inventoryIds) {
|
|
890
|
+
if (id === "")
|
|
891
|
+
continue;
|
|
892
|
+
const parsed = parseInventoryRepositoryId(id, hubOwner);
|
|
893
|
+
if (parsed === null) {
|
|
894
|
+
skipped.push({ inventoryId: id, note: "inventory id is not a valid repository slug" });
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (parsed.owner !== hubOwner) {
|
|
898
|
+
skipped.push({ inventoryId: id, note: "other account; not this roster" });
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const candidate = join(parent, parsed.repository);
|
|
902
|
+
const candidateResolved = resolve(candidate);
|
|
903
|
+
if (candidateResolved === hubResolved)
|
|
904
|
+
continue;
|
|
905
|
+
if (!host.exists(candidate) || !host.isDirectory(candidate)) {
|
|
906
|
+
skipped.push({ inventoryId: id, note: CLONE_NOT_BESIDE_HUB_NOTE });
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (looksLikeFoundry(host, candidate)) {
|
|
910
|
+
skipped.push({ inventoryId: id, note: "foundry supplier tree; skills are not written here" });
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const originResult = host.run("git", ["remote", "get-url", "origin"], { cwd: candidate });
|
|
914
|
+
const originUrl = originResult.status === 0 ? originResult.stdout.trim() : "";
|
|
915
|
+
const remote = originUrl === "" ? null : parseGitHubRemote(originUrl);
|
|
916
|
+
if (remote === null || remote.owner !== parsed.owner || remote.repository !== parsed.repository) {
|
|
917
|
+
skipped.push({ inventoryId: id, note: "git origin does not match inventory id" });
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
targets.push({ inventoryId: id, directory: candidate });
|
|
921
|
+
}
|
|
922
|
+
return { targets, skipped };
|
|
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
|
+
}
|
|
963
|
+
function hubRosterId(host, hubDirectory, hubOwner, hubRepository) {
|
|
964
|
+
const document = readHub(host, hubDirectory);
|
|
965
|
+
if (document !== undefined)
|
|
966
|
+
return document.repository;
|
|
967
|
+
return `${hubOwner}/${hubRepository}`;
|
|
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
|
+
}
|
|
1024
|
+
function composeSkillRoster(host, hubDirectory, hubOwner, hubRepository, options) {
|
|
1025
|
+
const composeOptions = {
|
|
1026
|
+
launcherPackageRoot: options.launcherPackageRoot,
|
|
1027
|
+
...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
|
|
1028
|
+
...(options.contractPath === undefined ? {} : { contractPath: options.contractPath }),
|
|
665
1029
|
};
|
|
1030
|
+
const linkedHosts = recordLinkedHosts(host, hubDirectory);
|
|
1031
|
+
const hubSkill = composeSkills(host, hubDirectory, composeOptions);
|
|
1032
|
+
writeConsumerAgentsIfNeeded(host, hubDirectory);
|
|
1033
|
+
writeClossysReadme(host, hubDirectory);
|
|
1034
|
+
const hubId = hubRosterId(host, hubDirectory, hubOwner, hubRepository);
|
|
1035
|
+
const rosterTargets = [hubId];
|
|
1036
|
+
const { targets, skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
1037
|
+
for (const target of targets) {
|
|
1038
|
+
recordLinkedHosts(host, target.directory);
|
|
1039
|
+
composeSkills(host, target.directory, composeOptions);
|
|
1040
|
+
writeSisterConsumerAgentsIfNeeded(host, target.directory);
|
|
1041
|
+
rosterTargets.push(target.inventoryId);
|
|
1042
|
+
}
|
|
1043
|
+
return { ...hubSkill, rosterTargets, rosterSkipped: skipped, linkedHosts };
|
|
1044
|
+
}
|
|
1045
|
+
function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner, hubRepository, liveAdvisorVersion, skillCatalogueRoot, contractPath, liveLauncherVersion, migration) {
|
|
1046
|
+
const skillComposition = composeSkillRoster(host, directory, hubOwner, hubRepository, {
|
|
1047
|
+
launcherPackageRoot,
|
|
1048
|
+
...(skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot }),
|
|
1049
|
+
...(contractPath === undefined ? {} : { contractPath }),
|
|
1050
|
+
});
|
|
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);
|
|
666
1056
|
}
|
|
667
|
-
/**
|
|
668
|
-
|
|
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 };
|
|
1076
|
+
}
|
|
1077
|
+
/** Applies a create, resume, or adopt plan through the host. Resume refreshes composed skills and stale AGENTS.md guidance. */
|
|
1078
|
+
export function applyWorkspacePlan(host, plan, skeletonRoot, options = {}) {
|
|
1079
|
+
const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
|
|
1080
|
+
const skillCatalogueRoot = options.skillCatalogueRoot;
|
|
1081
|
+
const contractPath = options.contractPath;
|
|
1082
|
+
const liveLauncherVersion = options.liveLauncherVersion;
|
|
669
1083
|
if (plan.action === "resume") {
|
|
670
1084
|
if (plan.clone) {
|
|
671
1085
|
requireZero(host.run("gh", ["repo", "clone", `${plan.owner}/${plan.repository}`, plan.directory]), "gh repo clone");
|
|
672
1086
|
}
|
|
673
|
-
|
|
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);
|
|
674
1089
|
}
|
|
675
1090
|
if (plan.action === "create") {
|
|
676
1091
|
copySkeleton(host, skeletonRoot, plan);
|
|
677
1092
|
requireZero(host.run("gh", ["repo", "create", `${plan.owner}/${plan.repository}`, "--private", "--source", plan.directory, "--remote", "origin", "--push"], { cwd: plan.directory }), "gh repo create");
|
|
678
|
-
return
|
|
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);
|
|
679
1094
|
}
|
|
680
1095
|
adoptHubFiles(host, skeletonRoot, plan);
|
|
681
|
-
return
|
|
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);
|
|
1097
|
+
}
|
|
1098
|
+
export function launcherPackageRootFromModule(moduleUrl) {
|
|
1099
|
+
return resolve(dirname(fileURLToPath(moduleUrl)), "..");
|
|
682
1100
|
}
|
|
683
1101
|
export function skeletonRootFromModule(moduleUrl) {
|
|
684
1102
|
return resolve(dirname(fileURLToPath(moduleUrl)), "..", "skeleton");
|