@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.
- package/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +84 -16
- package/lib/setup/enroll-from-cohort.test.mjs +43 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- 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
|
+
});
|