@cohortapp/agent-sdk 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +84 -16
  97. package/lib/setup/enroll-from-cohort.test.mjs +43 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generate-plan.mjs — compile the agent's PLAN and emit its schedule.
4
+ *
5
+ * `(mandate cache ⊕ capability manifest ⊕ archetype profile ⊕ org context)`
6
+ * → obligations
7
+ * → config/plan.yaml + config/cadences.yaml + config/.cadence-registry.json
8
+ * + config/.cadence-plists.tsv + state/plan/compile.lock
9
+ * → scripts/local-triggers/generate-plists.sh (REAL launchd jobs)
10
+ *
11
+ * This is the standalone door to the same code the `mandate` setup section runs,
12
+ * so `maestro upgrade` and a cron refresh can recompile a plan without the
13
+ * wizard. Pure + deterministic: identical inputs re-emit byte-identical files,
14
+ * so a no-op run churns no plists.
15
+ *
16
+ * Flags:
17
+ * --derive (re)derive a PROPOSED mandate before compiling
18
+ * --no-plists write the TSV but do not run generate-plists.sh
19
+ * --enforcement=observe-only|active
20
+ * --print print the obligation table and exit without writing
21
+ *
22
+ * @module scripts/setup/generate-plan
23
+ */
24
+
25
+ "use strict";
26
+
27
+ import { existsSync } from "node:fs";
28
+ import { join, dirname, resolve } from "node:path";
29
+ import { fileURLToPath, pathToFileURL } from "node:url";
30
+
31
+ const here = dirname(fileURLToPath(import.meta.url));
32
+ const MAESTRO_ROOT =
33
+ process.env.MAESTRO_ROOT && existsSync(join(process.env.MAESTRO_ROOT, "lib", "plan", "compile.mjs"))
34
+ ? process.env.MAESTRO_ROOT
35
+ : resolve(here, "..", "..");
36
+ const AGENT_ROOT = process.env.AGENT_ROOT || process.cwd();
37
+
38
+ const load = (rel) => import(pathToFileURL(join(MAESTRO_ROOT, rel)).href);
39
+
40
+ async function main() {
41
+ const argv = process.argv.slice(2);
42
+ const wantDerive = argv.includes("--derive");
43
+ const printOnly = argv.includes("--print");
44
+ const enforcement = (argv.find((a) => a.startsWith("--enforcement=")) || "").split("=")[1] || "observe-only";
45
+
46
+ const section = await load("lib/setup/sections/mandate.mjs");
47
+ const cache = await load("lib/mandate/cache.mjs");
48
+ const compile = await load("lib/plan/compile.mjs");
49
+
50
+ const inputs = await section.loadCompilerInputs({ agentRoot: AGENT_ROOT, maestroRoot: MAESTRO_ROOT });
51
+ for (const d of inputs.degradations) console.warn(`[generate-plan] ${d.kind}: ${d.reason}`);
52
+
53
+ let record = cache.readCache(AGENT_ROOT);
54
+ if (wantDerive || !record) {
55
+ const res = await section.resolveMandate({ agentRoot: AGENT_ROOT, inputs }, { logImpl: (m) => console.warn(m) });
56
+ record = res.record;
57
+ if (res.derived) {
58
+ const c = res.derived.counts;
59
+ console.log(`[generate-plan] derived ${c.proposed} PROPOSED objectives (${c.withMethodSensor} with a method sensor). None are adopted — adopt in Cohort to activate.`);
60
+ }
61
+ }
62
+
63
+ if (printOnly) {
64
+ const plan = compile.compilePlan({
65
+ mandate: record.body || {}, mandateVersion: record.version || 0,
66
+ manifest: inputs.manifest || { entries: [] }, profile: inputs.profile, orgContext: inputs.orgContext,
67
+ standardCadences: inputs.standardCadences, archetypeCadences: inputs.archetypeCadences,
68
+ });
69
+ for (const ob of plan.obligations) {
70
+ const when = ob.schedule ? (ob.schedule.interval != null ? `every ${ob.schedule.interval}s` : JSON.stringify(ob.schedule.calendar)) : (ob.trigger ? `on ${ob.trigger.topic}.${ob.trigger.kind || "*"}` : "-");
71
+ console.log(` ${ob.kind.padEnd(9)} ${ob.key.padEnd(40)} ${String(when).padEnd(34)} uses=[${(ob.uses || []).join(",")}]`);
72
+ }
73
+ console.log(`[generate-plan] ${plan.counts.total} obligations · obligationsHash ${plan.obligationsHash.slice(0, 16)} (dry run — nothing written)`);
74
+ return 0;
75
+ }
76
+
77
+ const { ok, plan, emit } = await section.compileAndEmit({
78
+ agentRoot: AGENT_ROOT, maestroRoot: MAESTRO_ROOT, inputs, record, enforcement,
79
+ }, { skipPlists: argv.includes("--no-plists") });
80
+
81
+ if (!ok) {
82
+ console.error("[generate-plan] plan FAILED validation — nothing written:");
83
+ for (const e of emit.errors.slice(0, 20)) console.error(` - ${e}`);
84
+ return 1;
85
+ }
86
+
87
+ console.log(`[generate-plan] ${plan.counts.total} obligations (${plan.counts.SCHEDULE} SCHEDULE · ${plan.counts.OUTCOME} OUTCOME · ${plan.counts.REACT} REACT)`);
88
+ console.log(`[generate-plan] from ${plan.counts.adoptedObjectives} adopted / ${plan.counts.proposedObjectives} proposed objectives · obligationsHash ${plan.obligationsHash.slice(0, 16)}`);
89
+ console.log(`[generate-plan] wrote ${emit.files.length} files; ${emit.tsv.length} archetype/mandate plist entries`);
90
+ for (const d of plan.drift) console.warn(`[generate-plan] drift ${d.kind}${d.key ? ` (${d.key})` : ""}: ${JSON.stringify(d.detail)}`);
91
+
92
+ if (!argv.includes("--no-plists")) {
93
+ const gen = await load("lib/setup/run-generator.mjs");
94
+ const plistGen = join(AGENT_ROOT, "scripts", "local-triggers", "generate-plists.sh");
95
+ if (existsSync(plistGen)) {
96
+ const pr = gen.runScriptAbs(plistGen, AGENT_ROOT, { bash: true, maestroRoot: MAESTRO_ROOT });
97
+ if (!pr.ok) console.warn(`[generate-plan] generate-plists.sh did not complete: ${(pr.stderr || "").trim().slice(0, 300)}`);
98
+ else console.log("[generate-plan] launchd plists regenerated");
99
+ } else {
100
+ console.warn("[generate-plan] no scripts/local-triggers/generate-plists.sh — TSV written, launchd jobs not emitted");
101
+ }
102
+ }
103
+ return 0;
104
+ }
105
+
106
+ main()
107
+ .then((c) => process.exit(c))
108
+ .catch((e) => { console.error(`[generate-plan] FAILED: ${e && e.message ? e.message : e}`); process.exit(1); });
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * init-capability-manifest.mjs — build/refresh config/capability-manifest.json.
4
+ *
5
+ * The standalone entry point for the capability inventory, so an EXISTING agent
6
+ * picks the feature up via `maestro upgrade` (framework-features.json →
7
+ * `capability-manifest`, auto:true) without re-running the whole wizard. Same
8
+ * code path as the `inventory` setup section — one implementation, two doors.
9
+ *
10
+ * Idempotent: re-running with nothing changed rewrites the same manifest with a
11
+ * new `generatedAt` and the SAME `checksum`, so downstream compilers can tell
12
+ * "nothing moved".
13
+ *
14
+ * Flags:
15
+ * --repair register the skill marketplaces + enable the plugins (default)
16
+ * --no-repair probe only; never write outside config/
17
+ * --quiet
18
+ *
19
+ * Execution contract mirrors the other generators: cwd = agent repo, with
20
+ * MAESTRO_ROOT / AGENT_ROOT in env.
21
+ *
22
+ * @module scripts/setup/init-capability-manifest
23
+ */
24
+
25
+ "use strict";
26
+
27
+ import { existsSync } from "node:fs";
28
+ import { join, dirname, resolve } from "node:path";
29
+ import { fileURLToPath, pathToFileURL } from "node:url";
30
+
31
+ const here = dirname(fileURLToPath(import.meta.url));
32
+ const MAESTRO_ROOT =
33
+ process.env.MAESTRO_ROOT && existsSync(join(process.env.MAESTRO_ROOT, "lib", "capability", "inventory.mjs"))
34
+ ? process.env.MAESTRO_ROOT
35
+ : resolve(here, "..", "..");
36
+ const AGENT_ROOT = process.env.AGENT_ROOT || process.cwd();
37
+
38
+ async function main() {
39
+ const argv = process.argv.slice(2);
40
+ const repair = !argv.includes("--no-repair");
41
+ const quiet = argv.includes("--quiet");
42
+
43
+ const inv = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "capability", "inventory.mjs")).href);
44
+ const sot = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "setup", "sot.mjs")).href);
45
+
46
+ let archetype = null;
47
+ try {
48
+ const cfg = sot.readAgentJson(AGENT_ROOT) || {};
49
+ if (cfg.function && cfg.altitude) archetype = { function: cfg.function, altitude: cfg.altitude };
50
+ } catch { /* identity section owns this gap; the inventory records it as a degradation */ }
51
+
52
+ const manifest = await inv.buildInventory(
53
+ { agentRoot: AGENT_ROOT, maestroRoot: MAESTRO_ROOT, archetype, repair },
54
+ { logImpl: quiet ? () => {} : (m) => console.warn(m) },
55
+ );
56
+ const path = inv.writeManifest(AGENT_ROOT, manifest);
57
+
58
+ const c = manifest.counts || {};
59
+ console.log(`[capability-manifest] ${manifest.entries.length} capabilities · ${c.reachable} reachable · ${c.unreachable} unreachable`);
60
+ console.log(`[capability-manifest] builtin ${c.builtin} · skill ${c.skill} · mcp ${c.mcp} · org ${c.org}`);
61
+ console.log(`[capability-manifest] checksum ${manifest.checksum.slice(0, 16)} → ${path}`);
62
+ if ((manifest.degradations || []).length) {
63
+ console.log(`[capability-manifest] ${manifest.degradations.length} degradation(s) — unreachable capabilities may not back any obligation.`);
64
+ }
65
+ return 0;
66
+ }
67
+
68
+ main()
69
+ .then((c) => process.exit(c))
70
+ .catch((e) => { console.error(`[capability-manifest] FAILED: ${e && e.message ? e.message : e}`); process.exit(1); });
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * init-skill-marketplace.mjs — make the agent's SKILL files actually invocable.
4
+ *
5
+ * Rung 1 of the execution ladder ("repeatable procedure, 2-10 calls, a skill
6
+ * exists in the manifest with reachable:true") was a LIE: maestro ships dozens of
7
+ * skill markdown files under `plugins/<id>/skills/`, but Claude Code only loads a
8
+ * skill when its plugin is (a) declared by a `.claude-plugin/marketplace.json`
9
+ * and (b) listed in `~/.claude/settings.json` `enabledPlugins`. Neither was ever
10
+ * written, so every one of those files was an orphan — present on disk, dead at
11
+ * runtime, and silently so.
12
+ *
13
+ * This is the standalone repair door, so an EXISTING agent picks it up through
14
+ * `maestro upgrade` (framework-features.json → `skill-marketplace`, auto:true)
15
+ * without re-running the wizard. It shares ONE implementation with the
16
+ * `inventory` setup section — `lib/capability/probe.probeSkillPlugin({repair})`
17
+ * — rather than growing a second registration mechanism beside it.
18
+ *
19
+ * Idempotent: an already-registered, already-enabled plugin is left untouched and
20
+ * reported as such. Fail-open but NEVER silent — a plugin that cannot be repaired
21
+ * is printed with its reason and leaves its skills `reachable:false`, which the
22
+ * plan compiler then refuses to cite.
23
+ *
24
+ * Flags:
25
+ * --dry-run report what WOULD be registered; write nothing
26
+ * --quiet
27
+ *
28
+ * Execution contract mirrors the other generators: cwd = agent repo, with
29
+ * MAESTRO_ROOT / AGENT_ROOT in env.
30
+ *
31
+ * @module scripts/setup/init-skill-marketplace
32
+ */
33
+
34
+ "use strict";
35
+
36
+ import { existsSync, readdirSync } from "node:fs";
37
+ import { join, dirname, resolve } from "node:path";
38
+ import { fileURLToPath, pathToFileURL } from "node:url";
39
+
40
+ const here = dirname(fileURLToPath(import.meta.url));
41
+ const MAESTRO_ROOT =
42
+ process.env.MAESTRO_ROOT && existsSync(join(process.env.MAESTRO_ROOT, "lib", "capability", "probe.mjs"))
43
+ ? process.env.MAESTRO_ROOT
44
+ : resolve(here, "..", "..");
45
+ const AGENT_ROOT = process.env.AGENT_ROOT || process.cwd();
46
+
47
+ /**
48
+ * Every plugin directory that carries at least one skill file, across the agent
49
+ * repo and the framework repo. Pure + injectable so the test does not need a
50
+ * real plugin tree.
51
+ * @param {{roots:string[]}} o
52
+ * @param {{existsSync?:Function, readdirSync?:Function}} [deps]
53
+ * @returns {Array<{pluginId:string, pluginDir:string, skills:string[]}>}
54
+ */
55
+ export function findSkillPlugins(o = {}, deps = {}) {
56
+ const ex = deps.existsSync || existsSync;
57
+ const rdd = deps.readdirSync || readdirSync;
58
+ const out = [];
59
+ const seen = new Set();
60
+
61
+ for (const root of (o.roots || []).filter(Boolean)) {
62
+ const pluginsDir = join(root, "plugins");
63
+ if (!ex(pluginsDir)) continue;
64
+ let ids = [];
65
+ try {
66
+ ids = rdd(pluginsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
67
+ } catch {
68
+ continue; // the inventory records the unreadable-dir degradation
69
+ }
70
+ for (const pluginId of ids) {
71
+ if (seen.has(pluginId)) continue;
72
+ const pluginDir = join(pluginsDir, pluginId);
73
+ const skillsDir = join(pluginDir, "skills");
74
+ if (!ex(skillsDir)) continue;
75
+ let skills = [];
76
+ try { skills = rdd(skillsDir).filter((f) => String(f).endsWith(".md")).sort(); } catch { skills = []; }
77
+ if (!skills.length) continue;
78
+ seen.add(pluginId);
79
+ out.push({ pluginId, pluginDir, skills });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Register + enable every skill plugin found. Returns a per-plugin report.
87
+ * @param {object} o - { agentRoot, maestroRoot, dryRun? }
88
+ * @param {object} [deps] - { probeSkillPlugin, existsSync, readdirSync, ... }
89
+ * @returns {Promise<{plugins:Array, repaired:string[], failed:Array, skillCount:number}>}
90
+ */
91
+ export async function registerMarketplaces(o = {}, deps = {}) {
92
+ const probeSkillPlugin = deps.probeSkillPlugin
93
+ || (await import(pathToFileURL(join(o.maestroRoot || MAESTRO_ROOT, "lib", "capability", "probe.mjs")).href)).probeSkillPlugin;
94
+
95
+ const found = findSkillPlugins({ roots: [o.agentRoot, o.maestroRoot] }, deps);
96
+ const plugins = [];
97
+ const repaired = [];
98
+ const failed = [];
99
+ let skillCount = 0;
100
+
101
+ for (const p of found) {
102
+ skillCount += p.skills.length;
103
+ const verdict = probeSkillPlugin({ pluginDir: p.pluginDir, pluginId: p.pluginId, repair: !o.dryRun }, deps);
104
+ const row = {
105
+ pluginId: p.pluginId,
106
+ skills: p.skills.length,
107
+ ok: !!verdict.ok,
108
+ detail: verdict.detail || null,
109
+ degraded: verdict.degraded || null,
110
+ repaired: Array.isArray(verdict.repaired) ? verdict.repaired : [],
111
+ };
112
+ plugins.push(row);
113
+ for (const r of row.repaired) repaired.push(`${p.pluginId}: ${r}`);
114
+ if (!row.ok) failed.push(row);
115
+ }
116
+
117
+ return { plugins, repaired, failed, skillCount };
118
+ }
119
+
120
+ async function main() {
121
+ const argv = process.argv.slice(2);
122
+ const dryRun = argv.includes("--dry-run");
123
+ const quiet = argv.includes("--quiet");
124
+ const log = quiet ? () => {} : (m) => console.log(m);
125
+
126
+ const res = await registerMarketplaces({ agentRoot: AGENT_ROOT, maestroRoot: MAESTRO_ROOT, dryRun });
127
+
128
+ if (!res.plugins.length) {
129
+ console.log("[skill-marketplace] no plugin directories carry skill files — nothing to register");
130
+ return 0;
131
+ }
132
+
133
+ const reachable = res.plugins.filter((p) => p.ok);
134
+ log(`[skill-marketplace] ${reachable.length}/${res.plugins.length} skill plugins registered + enabled (${res.skillCount} skill files)${dryRun ? " [DRY RUN — nothing written]" : ""}`);
135
+ for (const p of res.plugins) {
136
+ log(`[skill-marketplace] ${p.ok ? "ok " : "FAIL"} ${p.pluginId.padEnd(20)} ${p.skills} skill(s)${p.ok ? "" : ` — ${p.degraded}: ${p.detail}`}`);
137
+ }
138
+ for (const r of res.repaired) log(`[skill-marketplace] repaired: ${r}`);
139
+
140
+ if (res.failed.length) {
141
+ // Loud, but not fatal: an un-registerable plugin leaves its skills
142
+ // reachable:false, and the plan compiler already refuses to cite those.
143
+ console.warn(`[skill-marketplace] ${res.failed.length} plugin(s) could not be registered — their skills stay unreachable and may not back any obligation.`);
144
+ }
145
+ return 0;
146
+ }
147
+
148
+ // Only run when invoked directly (so the test can import the helpers).
149
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
150
+ main()
151
+ .then((c) => process.exit(c))
152
+ .catch((e) => { console.error(`[skill-marketplace] FAILED: ${e && e.message ? e.message : e}`); process.exit(1); });
153
+ }
154
+
155
+ export default { findSkillPlugins, registerMarketplaces };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * init-skill-marketplace.test.mjs — rung 1 of the execution ladder.
3
+ * Run: node --test scripts/setup/init-skill-marketplace.test.mjs
4
+ *
5
+ * The regression under guard: skill markdown files shipped under
6
+ * `plugins/<id>/skills/*.md` are DEAD at runtime unless their plugin is declared
7
+ * by a `.claude-plugin/marketplace.json` and enabled in `~/.claude/settings.json`.
8
+ * They were neither, and nothing said so. These tests use an injected
9
+ * `probeSkillPlugin` and an injected fs so no real plugin dir or operator
10
+ * settings file is ever touched.
11
+ */
12
+ "use strict";
13
+
14
+ import { test } from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ import { findSkillPlugins, registerMarketplaces } from "./init-skill-marketplace.mjs";
21
+
22
+ /** An injected fs describing a plugin tree, without creating one. */
23
+ function fakeFs(tree) {
24
+ return {
25
+ existsSync: (p) => Object.prototype.hasOwnProperty.call(tree, String(p)),
26
+ readdirSync: (p, opts) => {
27
+ const v = tree[String(p)];
28
+ if (!Array.isArray(v)) throw new Error(`ENOTDIR ${p}`);
29
+ if (opts && opts.withFileTypes) return v.map((n) => ({ name: n, isDirectory: () => !String(n).endsWith(".md") }));
30
+ return v;
31
+ },
32
+ };
33
+ }
34
+
35
+ test("findSkillPlugins discovers plugins across BOTH the agent repo and the framework", () => {
36
+ const deps = fakeFs({
37
+ "/agent/plugins": ["agent-skills"],
38
+ "/agent/plugins/agent-skills": ["skills"],
39
+ "/agent/plugins/agent-skills/skills": ["morning-brief.md", "outreach.md"],
40
+ "/fw/plugins": ["maestro-skills"],
41
+ "/fw/plugins/maestro-skills": ["skills"],
42
+ "/fw/plugins/maestro-skills/skills": ["triage.md"],
43
+ });
44
+ const found = findSkillPlugins({ roots: ["/agent", "/fw"] }, deps);
45
+ assert.deepEqual(found.map((p) => p.pluginId), ["agent-skills", "maestro-skills"]);
46
+ assert.deepEqual(found[0].skills, ["morning-brief.md", "outreach.md"]);
47
+ assert.equal(found[1].skills.length, 1);
48
+ });
49
+
50
+ test("findSkillPlugins ignores plugin dirs that carry no skill files", () => {
51
+ const deps = fakeFs({
52
+ "/agent/plugins": ["hooks-only", "agent-skills"],
53
+ "/agent/plugins/hooks-only": ["hooks"],
54
+ "/agent/plugins/agent-skills": ["skills"],
55
+ "/agent/plugins/agent-skills/skills": ["a.md"],
56
+ });
57
+ const found = findSkillPlugins({ roots: ["/agent"] }, deps);
58
+ assert.deepEqual(found.map((p) => p.pluginId), ["agent-skills"]);
59
+ });
60
+
61
+ test("findSkillPlugins: the agent repo wins a plugin-id collision, and no root is required", () => {
62
+ const deps = fakeFs({
63
+ "/agent/plugins": ["agent-skills"],
64
+ "/agent/plugins/agent-skills": ["skills"],
65
+ "/agent/plugins/agent-skills/skills": ["local.md"],
66
+ "/fw/plugins": ["agent-skills"],
67
+ "/fw/plugins/agent-skills": ["skills"],
68
+ "/fw/plugins/agent-skills/skills": ["framework.md", "another.md"],
69
+ });
70
+ const found = findSkillPlugins({ roots: ["/agent", "/fw"] }, deps);
71
+ assert.equal(found.length, 1, "the id is de-duplicated");
72
+ assert.deepEqual(found[0].skills, ["local.md"], "the agent's own copy wins");
73
+ // A missing plugins dir is a no-op, not a throw.
74
+ assert.deepEqual(findSkillPlugins({ roots: ["/nothing/here"] }, deps), []);
75
+ assert.deepEqual(findSkillPlugins({}, deps), []);
76
+ });
77
+
78
+ test("registerMarketplaces repairs every plugin and reports what it wrote", async () => {
79
+ const calls = [];
80
+ const deps = {
81
+ ...fakeFs({
82
+ "/agent/plugins": ["agent-skills"],
83
+ "/agent/plugins/agent-skills": ["skills"],
84
+ "/agent/plugins/agent-skills/skills": ["a.md", "b.md"],
85
+ }),
86
+ probeSkillPlugin: (o) => {
87
+ calls.push(o);
88
+ return { ok: true, detail: "marketplace+enabledPlugins", repaired: ["wrote .claude-plugin/marketplace.json", "enabled in settings.json"] };
89
+ },
90
+ };
91
+ const res = await registerMarketplaces({ agentRoot: "/agent", maestroRoot: "/fw" }, deps);
92
+
93
+ assert.equal(calls.length, 1);
94
+ assert.equal(calls[0].repair, true, "a non-dry run repairs");
95
+ assert.equal(calls[0].pluginId, "agent-skills");
96
+ assert.equal(res.skillCount, 2);
97
+ assert.equal(res.failed.length, 0);
98
+ assert.deepEqual(res.repaired, [
99
+ "agent-skills: wrote .claude-plugin/marketplace.json",
100
+ "agent-skills: enabled in settings.json",
101
+ ]);
102
+ });
103
+
104
+ test("--dry-run probes but never repairs", async () => {
105
+ const calls = [];
106
+ const deps = {
107
+ ...fakeFs({
108
+ "/agent/plugins": ["agent-skills"],
109
+ "/agent/plugins/agent-skills": ["skills"],
110
+ "/agent/plugins/agent-skills/skills": ["a.md"],
111
+ }),
112
+ probeSkillPlugin: (o) => { calls.push(o); return { ok: false, degraded: "marketplace-missing", detail: "no manifest", repaired: [] }; },
113
+ };
114
+ const res = await registerMarketplaces({ agentRoot: "/agent", maestroRoot: "/fw", dryRun: true }, deps);
115
+ assert.equal(calls[0].repair, false, "dry run must not write");
116
+ assert.equal(res.repaired.length, 0);
117
+ });
118
+
119
+ test("a plugin that cannot be repaired is reported LOUDLY, never swallowed", async () => {
120
+ const deps = {
121
+ ...fakeFs({
122
+ "/agent/plugins": ["good", "broken"],
123
+ "/agent/plugins/good": ["skills"],
124
+ "/agent/plugins/good/skills": ["a.md"],
125
+ "/agent/plugins/broken": ["skills"],
126
+ "/agent/plugins/broken/skills": ["b.md", "c.md"],
127
+ }),
128
+ probeSkillPlugin: (o) => (o.pluginId === "broken"
129
+ ? { ok: false, degraded: "marketplace-unwritable", detail: "EACCES", repaired: [] }
130
+ : { ok: true, detail: "ok", repaired: [] }),
131
+ };
132
+ const res = await registerMarketplaces({ agentRoot: "/agent", maestroRoot: "/fw" }, deps);
133
+
134
+ assert.equal(res.failed.length, 1);
135
+ assert.equal(res.failed[0].pluginId, "broken");
136
+ assert.equal(res.failed[0].degraded, "marketplace-unwritable");
137
+ assert.equal(res.failed[0].detail, "EACCES");
138
+ // Its skills stay unreachable, which is what the plan compiler then refuses to cite.
139
+ assert.equal(res.plugins.find((p) => p.pluginId === "broken").ok, false);
140
+ assert.equal(res.plugins.find((p) => p.pluginId === "good").ok, true);
141
+ });
142
+
143
+ test("idempotent: a second run over an already-registered plugin writes nothing new", async () => {
144
+ let registered = false;
145
+ const deps = {
146
+ ...fakeFs({
147
+ "/agent/plugins": ["agent-skills"],
148
+ "/agent/plugins/agent-skills": ["skills"],
149
+ "/agent/plugins/agent-skills/skills": ["a.md"],
150
+ }),
151
+ probeSkillPlugin: () => {
152
+ const repaired = registered ? [] : ["wrote marketplace.json"];
153
+ registered = true;
154
+ return { ok: true, detail: "ok", repaired };
155
+ },
156
+ };
157
+ const first = await registerMarketplaces({ agentRoot: "/agent" }, deps);
158
+ const second = await registerMarketplaces({ agentRoot: "/agent" }, deps);
159
+ assert.equal(first.repaired.length, 1);
160
+ assert.equal(second.repaired.length, 0, "no churn on a no-op re-run");
161
+ assert.deepEqual(first.plugins.map((p) => p.pluginId), second.plugins.map((p) => p.pluginId));
162
+ });
163
+
164
+ test("the REAL probe writes a valid marketplace manifest into a scratch plugin dir", async () => {
165
+ // One end-to-end pass against the actual probe, in a tmp tree, to prove the
166
+ // injected contract above matches the real implementation's shape.
167
+ const root = mkdtempSync(join(tmpdir(), "maestro-skillmkt-"));
168
+ const home = mkdtempSync(join(tmpdir(), "maestro-skillhome-"));
169
+ mkdirSync(join(root, "plugins", "agent-skills", "skills"), { recursive: true });
170
+ writeFileSync(join(root, "plugins", "agent-skills", "skills", "morning-brief.md"), "# morning-brief\n");
171
+
172
+ const { probeSkillPlugin } = await import("../../lib/capability/probe.mjs");
173
+ const res = await registerMarketplaces(
174
+ { agentRoot: root, maestroRoot: root },
175
+ { probeSkillPlugin: (o, d) => probeSkillPlugin(o, { ...d, homeDir: home }) },
176
+ );
177
+
178
+ assert.equal(res.plugins.length, 1);
179
+ assert.equal(res.skillCount, 1);
180
+ const manifestPath = join(root, "plugins", "agent-skills", ".claude-plugin", "marketplace.json");
181
+ assert.ok(existsSync(manifestPath), "the marketplace manifest was written");
182
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
183
+ assert.equal(manifest.name, "agent-skills");
184
+ assert.ok(Array.isArray(manifest.plugins) && manifest.plugins.length === 1);
185
+ });
186
+
187
+ test("the feature is registered in framework-features.json so `maestro upgrade` picks it up", async () => {
188
+ const features = JSON.parse(readFileSync(new URL("../../framework-features.json", import.meta.url), "utf-8"));
189
+ const f = (features.features || features)["skill-marketplace"];
190
+ assert.ok(f, "skill-marketplace feature entry exists");
191
+ assert.equal(f.init.auto, true, "it is an auto migration (idempotent, ~instant)");
192
+ assert.match(f.init.command, /init-skill-marketplace\.mjs/);
193
+ });