@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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify.test.mjs — the INIT-pipeline gate in the verify section (SPEC §5.8).
|
|
3
|
+
* Run: node --test lib/setup/sections/verify.test.mjs
|
|
4
|
+
*
|
|
5
|
+
* The four checks that decide whether the plan an agent just compiled is
|
|
6
|
+
* TRUSTWORTHY:
|
|
7
|
+
* 1. it compiles and its obligationsHash matches the compile lock
|
|
8
|
+
* 2. no obligation cites an unreachable capability
|
|
9
|
+
* 3. no OUTCOME rests on an LLM-sourced sensor
|
|
10
|
+
* 4. the mandate cache is fresh (< 24h)
|
|
11
|
+
*
|
|
12
|
+
* All four are SOFT — an un-adopted agent legitimately has no plan and setup must
|
|
13
|
+
* not exit non-zero for that — so the thing under test is that every degradation
|
|
14
|
+
* is NAMED with a remedy rather than passing silently.
|
|
15
|
+
*/
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
import { test } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import yaml from "js-yaml";
|
|
21
|
+
|
|
22
|
+
import verifySection, { planChecks, MANDATE_FRESH_MS } from "./verify.mjs";
|
|
23
|
+
import { loadSections } from "../runner.mjs";
|
|
24
|
+
|
|
25
|
+
const NOW = Date.parse("2026-08-11T12:00:00.000Z");
|
|
26
|
+
|
|
27
|
+
const MANIFEST = {
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
entries: [
|
|
30
|
+
{ id: "crm_list_deals", plane: "org", reachable: true },
|
|
31
|
+
{ id: "board_ready", plane: "org", reachable: true },
|
|
32
|
+
{ id: "email_inbox", plane: "org", reachable: false },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function planDoc(over = {}) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
obligationsHash: "HASH_OK",
|
|
40
|
+
obligations: [
|
|
41
|
+
{ key: "schedule.measure.pipeline", kind: "SCHEDULE", status: "active", uses: ["crm_list_deals"] },
|
|
42
|
+
{ key: "outcome.pipeline", kind: "OUTCOME", status: "active", uses: ["crm_list_deals"], sensor: { capability: "crm_list_deals", source: "method" } },
|
|
43
|
+
],
|
|
44
|
+
...over,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A fully injected world. Nothing here touches the real filesystem — the point
|
|
50
|
+
* of the deps seam is that a check can be proven without a scaffolded agent.
|
|
51
|
+
*/
|
|
52
|
+
function deps({ plan = planDoc(), manifest = MANIFEST, lock = { obligationsHash: "HASH_OK" }, cache = { fetchedAt: "2026-08-11T09:00:00.000Z" }, planMissing = false } = {}) {
|
|
53
|
+
return {
|
|
54
|
+
yaml,
|
|
55
|
+
existsSync: (p) => (String(p).endsWith("plan.yaml") ? !planMissing : true),
|
|
56
|
+
readFileSync: (p) => {
|
|
57
|
+
if (String(p).endsWith("plan.yaml")) return yaml.dump(plan);
|
|
58
|
+
throw new Error(`unexpected read: ${p}`);
|
|
59
|
+
},
|
|
60
|
+
readManifest: () => manifest,
|
|
61
|
+
readLock: () => lock,
|
|
62
|
+
readCache: () => cache,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const byName = (checks, re) => checks.find((c) => re.test(c.name));
|
|
67
|
+
|
|
68
|
+
test("a healthy plan passes all four checks", async () => {
|
|
69
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps());
|
|
70
|
+
assert.ok(checks.every((c) => c.ok), `all ok, got: ${JSON.stringify(checks.filter((c) => !c.ok))}`);
|
|
71
|
+
assert.ok(byName(checks, /plan compiled/).ok);
|
|
72
|
+
assert.ok(byName(checks, /obligationsHash/).ok);
|
|
73
|
+
assert.ok(byName(checks, /unreachable capability/).ok);
|
|
74
|
+
assert.ok(byName(checks, /method sensor/).ok);
|
|
75
|
+
assert.ok(byName(checks, /mandate cache fresh/).ok);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("no plan on disk is SOFT but named — it never fails setup silently", async () => {
|
|
79
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ planMissing: true }));
|
|
80
|
+
assert.equal(checks.length, 1);
|
|
81
|
+
assert.equal(checks[0].ok, true, "an un-adopted agent is a legitimate state");
|
|
82
|
+
assert.match(checks[0].remedy, /standard \+ archetype cadences only/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a hand-edited plan.yaml is caught by the compile-lock hash", async () => {
|
|
86
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ lock: { obligationsHash: "SOMETHING_ELSE" } }));
|
|
87
|
+
const c = byName(checks, /obligationsHash/);
|
|
88
|
+
assert.equal(c.ok, false);
|
|
89
|
+
assert.match(c.remedy, /edited by hand|stale/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a missing compile lock is reported, not treated as a pass", async () => {
|
|
93
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ lock: null }));
|
|
94
|
+
const c = byName(checks, /obligationsHash/);
|
|
95
|
+
assert.equal(c.ok, false);
|
|
96
|
+
assert.match(c.remedy, /no state\/plan\/compile\.lock/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("THE LAW: an obligation citing an unreachable capability fails the check", async () => {
|
|
100
|
+
// email_inbox is reachable:false in the manifest.
|
|
101
|
+
const plan = planDoc({
|
|
102
|
+
obligations: [{ key: "schedule.inbox", kind: "SCHEDULE", status: "active", uses: ["crm_list_deals", "email_inbox"] }],
|
|
103
|
+
});
|
|
104
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ plan }));
|
|
105
|
+
const c = byName(checks, /unreachable capability/);
|
|
106
|
+
assert.equal(c.ok, false);
|
|
107
|
+
assert.match(c.remedy, /schedule\.inbox → email_inbox/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("no capability manifest → the reachability law cannot be checked, and says so", async () => {
|
|
111
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ manifest: null }));
|
|
112
|
+
const c = byName(checks, /unreachable capability/);
|
|
113
|
+
assert.equal(c.ok, false);
|
|
114
|
+
assert.match(c.remedy, /capability-manifest\.json/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("an OUTCOME resting on an LLM sensor fails the honesty gate", async () => {
|
|
118
|
+
const plan = planDoc({
|
|
119
|
+
obligations: [{ key: "outcome.vibes", kind: "OUTCOME", status: "active", uses: ["crm_list_deals"], sensor: { capability: "crm_list_deals", source: "llm" } }],
|
|
120
|
+
});
|
|
121
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ plan }));
|
|
122
|
+
const c = byName(checks, /method sensor/);
|
|
123
|
+
assert.equal(c.ok, false);
|
|
124
|
+
assert.match(c.remedy, /outcome\.vibes/);
|
|
125
|
+
assert.match(c.remedy, /cannot create work/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a stale mandate cache (>24h) fails freshness", async () => {
|
|
129
|
+
const stale = { fetchedAt: new Date(NOW - MANDATE_FRESH_MS - 60_000).toISOString() };
|
|
130
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ cache: stale }));
|
|
131
|
+
const c = byName(checks, /mandate cache fresh/);
|
|
132
|
+
assert.equal(c.ok, false);
|
|
133
|
+
assert.match(c.remedy, /self-directed work is suspended/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("no mandate cache is soft (un-enrolled agent) but still named", async () => {
|
|
137
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, deps({ cache: null }));
|
|
138
|
+
const c = byName(checks, /mandate cache present/);
|
|
139
|
+
assert.equal(c.ok, true);
|
|
140
|
+
assert.match(c.remedy, /no adopted mandate yet/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("an unreadable plan.yaml is a reported failure, not a thrown setup", async () => {
|
|
144
|
+
const d = deps();
|
|
145
|
+
d.readFileSync = () => "{{{ not yaml :::";
|
|
146
|
+
const checks = await planChecks({ agentRoot: "/agent", now: NOW }, d);
|
|
147
|
+
assert.equal(checks[0].ok, false);
|
|
148
|
+
assert.match(checks[0].remedy, /generate-plan\.mjs/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("planChecks is wired into the verify section's rows under section 'plan'", async () => {
|
|
152
|
+
const rows = [];
|
|
153
|
+
const res = await verifySection.verify({
|
|
154
|
+
agentRoot: "/agent",
|
|
155
|
+
sections: [],
|
|
156
|
+
planDeps: deps(),
|
|
157
|
+
}).catch((e) => ({ error: e }));
|
|
158
|
+
// The core probes (completeness/claude) may legitimately fail on a fake root;
|
|
159
|
+
// what must hold is that the plan rows are present and attributed.
|
|
160
|
+
const planRows = ((res && res.rows) || rows).filter((r) => r.section === "plan");
|
|
161
|
+
assert.ok(planRows.length >= 4, `expected the plan checks in the rows, got ${JSON.stringify((res && res.rows) || []).slice(0, 200)}`);
|
|
162
|
+
assert.ok(planRows.some((r) => /obligationsHash/.test(r.name)));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("the verify section is registered by the directory glob at order 90", async () => {
|
|
166
|
+
const sections = await loadSections();
|
|
167
|
+
const v = sections.find((s) => s.id === "verify");
|
|
168
|
+
assert.ok(v, "verify is discovered by loadSections()");
|
|
169
|
+
assert.equal(v.order, 90);
|
|
170
|
+
// It must run AFTER the two init-pipeline sections it gates.
|
|
171
|
+
const inventory = sections.find((s) => s.id === "inventory");
|
|
172
|
+
const mandate = sections.find((s) => s.id === "mandate");
|
|
173
|
+
assert.ok(inventory.order < v.order, "inventory (55) precedes verify (90)");
|
|
174
|
+
assert.ok(mandate.order < v.order, "mandate (78) precedes verify (90)");
|
|
175
|
+
});
|
package/lib/setup/sot.mjs
CHANGED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/subagents/cli.mjs — backs `maestro subagents <sub>` (§2.4).
|
|
3
|
+
*
|
|
4
|
+
* WHY A LIBRARY AND NOT A `case` BLOCK IN bin/maestro.mjs. `bin/maestro.mjs` is a
|
|
5
|
+
* 3000-line dispatcher that no test can import without executing; putting the
|
|
6
|
+
* registry's whole verb surface in it would make it untestable, which for a
|
|
7
|
+
* module that writes files and calls a remote API is not acceptable. `run()`
|
|
8
|
+
* returns `{ok, code, lines}` and only PRINTS when asked to, so every subcommand
|
|
9
|
+
* has a unit test. bin/maestro.mjs keeps a four-line `case "subagents"`.
|
|
10
|
+
*
|
|
11
|
+
* Commands (all local-first; only `pull`/`publish`/`add`/`fork`/`drop` network,
|
|
12
|
+
* and all of those degrade to the outbox):
|
|
13
|
+
*
|
|
14
|
+
* list resolved roster + winning layer + version per slug
|
|
15
|
+
* status the §2.3 three-way diff (what `doctor` renders)
|
|
16
|
+
* show <slug> resolved body + provenance
|
|
17
|
+
* diff <slug> local file vs the layer it was materialised from
|
|
18
|
+
* add <slug> new local def from the generator skeleton → create+pin
|
|
19
|
+
* fork <slug> [--as <n>] copy the resolved body into a new org definition
|
|
20
|
+
* publish <slug> publishVersion from the on-disk file
|
|
21
|
+
* pull [--force] re-resolve + re-materialise (--force overwrites edits)
|
|
22
|
+
* drop <slug> [--yank] unpin (and yank with --yank) — never a row delete
|
|
23
|
+
* export <slug> --to-sdk write into a local SDK checkout + print the PR line
|
|
24
|
+
*
|
|
25
|
+
* `export --to-sdk` is the ENTIRE "push upstream to the global set" story in v1:
|
|
26
|
+
* no new auth, no PAT, no hq surface (§5.4). The global channel is the npm
|
|
27
|
+
* package; promotion is a human PR.
|
|
28
|
+
*
|
|
29
|
+
* Node builtins only. ESM.
|
|
30
|
+
*
|
|
31
|
+
* @module lib/subagents/cli
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
"use strict";
|
|
35
|
+
|
|
36
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
37
|
+
import { dirname, join } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import { parseArgs } from "node:util";
|
|
40
|
+
import { writeFileAtomic, writeJsonAtomic } from "../fs-atomic.mjs";
|
|
41
|
+
import { resolveAgentRoot } from "../agent-root.mjs";
|
|
42
|
+
import * as client from "./client.mjs";
|
|
43
|
+
import { acceptDrop, mergeLock, readLock } from "./lock.mjs";
|
|
44
|
+
import { buildManifest, manifestPath, readManifest } from "./manifest.mjs";
|
|
45
|
+
import { computeGaps, contextFromConfig, rosterFromSurface } from "./gap.mjs";
|
|
46
|
+
import { scanAgentRefs } from "./refs.mjs";
|
|
47
|
+
import { agentFilePath, materialiseAll, renderEntry, resolveSubagents } from "./resolve.mjs";
|
|
48
|
+
import { checkAgentFile, hashFile, parseAgentMd, stringifyAgentMd, stripProvenance } from "./schema.mjs";
|
|
49
|
+
|
|
50
|
+
/** Flags every subcommand shares. */
|
|
51
|
+
const FLAGS = {
|
|
52
|
+
force: { type: "boolean", default: false },
|
|
53
|
+
yank: { type: "boolean", default: false },
|
|
54
|
+
json: { type: "boolean", default: false },
|
|
55
|
+
as: { type: "string" },
|
|
56
|
+
"to-sdk": { type: "string" },
|
|
57
|
+
"no-network": { type: "boolean", default: false },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Read config/agent.json. Fail-open → {}. */
|
|
61
|
+
function readAgentConfig(agentRoot) {
|
|
62
|
+
const p = join(agentRoot, "config", "agent.json");
|
|
63
|
+
if (!existsSync(p)) return {};
|
|
64
|
+
try { return JSON.parse(readFileSync(p, "utf8")) || {}; } catch { return {}; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Assemble everything the local-first commands need in one pass: the pins
|
|
69
|
+
* (network-optional), the resolution, the roster and the reference scan.
|
|
70
|
+
*
|
|
71
|
+
* `pins` come from `subagent.resolve` when the network is available and from the
|
|
72
|
+
* cache otherwise — the caller never has to know which, which is the point of
|
|
73
|
+
* §4's local-first contract.
|
|
74
|
+
*
|
|
75
|
+
* `force` is passed straight through to the resolver, where it DEMOTES the local
|
|
76
|
+
* layer so `pull --force` can actually re-resolve an edited file to its upstream
|
|
77
|
+
* layer. Every other command leaves it off, so a local edit always wins there.
|
|
78
|
+
*
|
|
79
|
+
* @param {{agentRoot:string, maestroRoot:string, network?:boolean, force?:boolean, fetchImpl?:Function, env?:object}} o
|
|
80
|
+
*/
|
|
81
|
+
export async function loadState(o = {}) {
|
|
82
|
+
const agentRoot = o.agentRoot;
|
|
83
|
+
const maestroRoot = o.maestroRoot;
|
|
84
|
+
const cfg = client.resolveConfig({ agentRoot, env: o.env });
|
|
85
|
+
const lock = readLock(agentRoot);
|
|
86
|
+
const manifest = readManifest(maestroRoot);
|
|
87
|
+
const agentConfig = readAgentConfig(agentRoot);
|
|
88
|
+
|
|
89
|
+
let pins = [];
|
|
90
|
+
let degraded = [];
|
|
91
|
+
let rosterVersion = lock.rosterVersion;
|
|
92
|
+
if (o.network !== false && cfg.enabled) {
|
|
93
|
+
const r = await client.fetchResolve({
|
|
94
|
+
cfg,
|
|
95
|
+
agentRoot,
|
|
96
|
+
memberId: cfg.memberId || undefined,
|
|
97
|
+
function: agentConfig.function,
|
|
98
|
+
altitude: agentConfig.altitude,
|
|
99
|
+
fetchImpl: o.fetchImpl,
|
|
100
|
+
});
|
|
101
|
+
if (r.ok) { pins = r.entries; rosterVersion = r.rosterVersion; }
|
|
102
|
+
else degraded = ["workspace"];
|
|
103
|
+
} else if (!cfg.enabled) {
|
|
104
|
+
// Not enrolled is not a failure — layer 1 simply does not exist (§4).
|
|
105
|
+
degraded = [];
|
|
106
|
+
} else {
|
|
107
|
+
degraded = ["workspace"];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// With no live pins, reconstruct layer 1 from the lock + cache so an offline
|
|
111
|
+
// run still resolves workspace-pinned agents rather than silently demoting
|
|
112
|
+
// them to SDK (which would materialise the WRONG body).
|
|
113
|
+
if (!pins.length) {
|
|
114
|
+
pins = Object.entries(lock.agents || {})
|
|
115
|
+
.filter(([, e]) => e && e.layer === "workspace")
|
|
116
|
+
.map(([slug, e]) => ({ slug, definitionId: e.definitionId, version: e.version, contentHash: e.contentHash }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolved = resolveSubagents({ agentRoot, maestroRoot, pins, manifest, lock, orgSlug: cfg.orgId, force: !!o.force });
|
|
120
|
+
const refs = scanAgentRefs({ agentRoot });
|
|
121
|
+
return { cfg, lock, manifest, agentConfig, pins, resolved, refs, degraded, rosterVersion };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolve the authoritative roster from the capability surface. Fail-open → []. */
|
|
125
|
+
export async function loadRoster(agentConfig, opts = {}) {
|
|
126
|
+
if (typeof opts.resolveSurfaceImpl === "function") {
|
|
127
|
+
try { return rosterFromSurface(await opts.resolveSurfaceImpl({ function: agentConfig.function, altitude: agentConfig.altitude })); }
|
|
128
|
+
catch { return []; }
|
|
129
|
+
}
|
|
130
|
+
if (!agentConfig.function || !agentConfig.altitude) return [];
|
|
131
|
+
try {
|
|
132
|
+
const mod = await import("../capability.mjs");
|
|
133
|
+
const surface = await mod.resolveCapabilitySurface({ function: agentConfig.function, altitude: agentConfig.altitude });
|
|
134
|
+
return rosterFromSurface(surface);
|
|
135
|
+
} catch {
|
|
136
|
+
// No capability pack for this function → no authoritative roster. Everything
|
|
137
|
+
// on disk still resolves; only `missing`/`idle` go unreported.
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run a `maestro subagents` subcommand.
|
|
144
|
+
*
|
|
145
|
+
* @param {string[]} argv args AFTER `subagents`
|
|
146
|
+
* @param {{agentRoot?:string, maestroRoot?:string, fetchImpl?:Function, env?:object, print?:boolean, resolveSurfaceImpl?:Function}} [opts]
|
|
147
|
+
* @returns {Promise<{ok:boolean, code:number, lines:string[], data?:object}>}
|
|
148
|
+
*/
|
|
149
|
+
export async function run(argv = [], opts = {}) {
|
|
150
|
+
const agentRoot = opts.agentRoot || resolveAgentRoot();
|
|
151
|
+
const maestroRoot = opts.maestroRoot || join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
152
|
+
const lines = [];
|
|
153
|
+
const say = (s) => { lines.push(s); if (opts.print !== false) console.log(s); };
|
|
154
|
+
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = parseArgs({ args: argv, options: FLAGS, allowPositionals: true, strict: false });
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return { ok: false, code: 2, lines: [`subagents: ${e && e.message ? e.message : e}`] };
|
|
160
|
+
}
|
|
161
|
+
const [sub, ...rest] = parsed.positionals;
|
|
162
|
+
const flags = parsed.values;
|
|
163
|
+
const network = !flags["no-network"];
|
|
164
|
+
const ctx = { agentRoot, maestroRoot, network, fetchImpl: opts.fetchImpl, env: opts.env };
|
|
165
|
+
|
|
166
|
+
switch (sub) {
|
|
167
|
+
case undefined:
|
|
168
|
+
case "help":
|
|
169
|
+
say(usage());
|
|
170
|
+
return { ok: true, code: 0, lines };
|
|
171
|
+
|
|
172
|
+
case "list": {
|
|
173
|
+
const st = await loadState(ctx);
|
|
174
|
+
if (flags.json) { say(JSON.stringify(st.resolved.entries.map(slim), null, 2)); return { ok: true, code: 0, lines, data: st.resolved }; }
|
|
175
|
+
say(`Sub-agents (${st.resolved.entries.length}) — agentRoot ${agentRoot}`);
|
|
176
|
+
for (const e of st.resolved.entries) {
|
|
177
|
+
const v = e.version == null ? "-" : String(e.version);
|
|
178
|
+
say(` ${e.slug.padEnd(28)} ${e.layer.padEnd(10)} v${v.padEnd(8)} ${e.localEdits ? "(local edit)" : e.orphaned ? "(orphaned)" : ""}`);
|
|
179
|
+
}
|
|
180
|
+
if (st.degraded.length) say(` ! degraded: ${st.degraded.join(", ")} — resolved from cache/local only`);
|
|
181
|
+
return { ok: true, code: 0, lines, data: st.resolved };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "status": {
|
|
185
|
+
const st = await loadState(ctx);
|
|
186
|
+
const roster = await loadRoster(st.agentConfig, opts);
|
|
187
|
+
const gaps = computeGaps({
|
|
188
|
+
roster, resolved: st.resolved, refs: st.refs, lock: st.lock,
|
|
189
|
+
context: contextFromConfig({ agentConfig: st.agentConfig }),
|
|
190
|
+
});
|
|
191
|
+
if (flags.json) { say(JSON.stringify(gaps, null, 2)); return { ok: gaps.status === "complete", code: 0, lines, data: gaps }; }
|
|
192
|
+
say(`Sub-agent registry — ${gaps.summary}`);
|
|
193
|
+
if (gaps.missing.length) {
|
|
194
|
+
say(`\n MISSING (${gaps.missing.length}) — on the roster, no layer resolves them:`);
|
|
195
|
+
for (const m of gaps.missing) say(` ${m.slug.padEnd(28)} ${m.role || ""}`);
|
|
196
|
+
}
|
|
197
|
+
if (gaps.unrostered.length) {
|
|
198
|
+
say(`\n UNROSTERED (${gaps.unrostered.length}) — referenced but not on the roster:`);
|
|
199
|
+
for (const u of gaps.unrostered) {
|
|
200
|
+
const at = u.locations[0] ? `${u.locations[0].file}:${u.locations[0].line}` : "";
|
|
201
|
+
say(` ${u.ref.padEnd(28)} ${u.action}${u.suggest ? ` → ${u.suggest}` : ""}${u.accepted ? " (drop accepted)" : ""} ${at}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (gaps.idle.length) {
|
|
205
|
+
say(`\n IDLE (${gaps.idle.length}) — on the roster, referenced by nothing:`);
|
|
206
|
+
for (const i of gaps.idle) say(` ${i.slug}`);
|
|
207
|
+
}
|
|
208
|
+
if (gaps.localEdits.length) say(`\n LOCAL EDITS (${gaps.localEdits.length}): ${gaps.localEdits.join(", ")}`);
|
|
209
|
+
if (gaps.emptyDirs.length) say(`\n EMPTY DIRS (${gaps.emptyDirs.length}) — agents/<slug>/ with no agent.md: ${gaps.emptyDirs.join(", ")}`);
|
|
210
|
+
return { ok: gaps.status === "complete", code: 0, lines, data: gaps };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case "show": {
|
|
214
|
+
const slug = rest[0];
|
|
215
|
+
if (!slug) return fail(lines, "usage: maestro subagents show <slug>");
|
|
216
|
+
const st = await loadState(ctx);
|
|
217
|
+
const e = st.resolved.bySlug.get(slug);
|
|
218
|
+
if (!e) return fail(lines, `no sub-agent resolves for "${slug}"`);
|
|
219
|
+
say(`# ${slug}`);
|
|
220
|
+
say(`layer: ${e.layer}`);
|
|
221
|
+
say(`source: ${e.source}`);
|
|
222
|
+
say(`version: ${e.version == null ? "-" : e.version}`);
|
|
223
|
+
say(`hash: ${e.contentHash}`);
|
|
224
|
+
say("");
|
|
225
|
+
say(e.body);
|
|
226
|
+
return { ok: true, code: 0, lines, data: e };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "diff": {
|
|
230
|
+
const slug = rest[0];
|
|
231
|
+
if (!slug) return fail(lines, "usage: maestro subagents diff <slug>");
|
|
232
|
+
const st = await loadState(ctx);
|
|
233
|
+
const e = st.resolved.bySlug.get(slug);
|
|
234
|
+
if (!e) return fail(lines, `no sub-agent resolves for "${slug}"`);
|
|
235
|
+
const p = agentFilePath(agentRoot, slug);
|
|
236
|
+
const onDisk = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
237
|
+
// Render through the SAME function materialise uses. Deriving the expected
|
|
238
|
+
// bytes any other way skips normalisation and reports every managed file as
|
|
239
|
+
// differing from the layer it was written from.
|
|
240
|
+
const rendered = renderEntry(e);
|
|
241
|
+
if (!rendered.ok) return fail(lines, `${slug}: cannot render the ${e.layer} layer — ${rendered.reason}`);
|
|
242
|
+
const want = rendered.text;
|
|
243
|
+
if (onDisk === want) { say(`${slug}: identical to the ${e.layer} layer`); return { ok: true, code: 0, lines }; }
|
|
244
|
+
say(`${slug}: differs from the ${e.layer} layer`);
|
|
245
|
+
for (const l of lineDiff(onDisk, want)) say(l);
|
|
246
|
+
return { ok: true, code: 0, lines, data: { slug, onDiskHash: hashFile(onDisk), layerHash: hashFile(want) } };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "pull": {
|
|
250
|
+
// --force here is two things at once: demote the local layer during
|
|
251
|
+
// resolution AND permit the write. Both are needed — either alone is a
|
|
252
|
+
// no-op against an edited file.
|
|
253
|
+
const st = await loadState({ ...ctx, force: !!flags.force });
|
|
254
|
+
const out = materialiseAll({ agentRoot, resolved: st.resolved, force: !!flags.force });
|
|
255
|
+
mergeLock(agentRoot, { agents: out.lockAgents, rosterVersion: st.rosterVersion, degraded: st.degraded });
|
|
256
|
+
say(`pull: ${out.written} written, ${out.results.length - out.written} unchanged/refused`);
|
|
257
|
+
for (const r of out.results) if (!r.written && r.errors) say(` ! ${r.slug}: ${r.reason}`);
|
|
258
|
+
if (st.degraded.length) say(` ! degraded: ${st.degraded.join(", ")}`);
|
|
259
|
+
return { ok: true, code: 0, lines, data: out };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case "add": {
|
|
263
|
+
const slug = rest[0];
|
|
264
|
+
if (!slug) return fail(lines, "usage: maestro subagents add <slug>");
|
|
265
|
+
const st = await loadState(ctx);
|
|
266
|
+
const p = agentFilePath(agentRoot, slug);
|
|
267
|
+
if (existsSync(p) && !flags.force) return fail(lines, `agents/${slug}/agent.md already exists (use --force to re-scaffold)`);
|
|
268
|
+
const text = skeleton(slug, st.agentConfig);
|
|
269
|
+
mkdirSync(join(agentRoot, "agents", slug), { recursive: true });
|
|
270
|
+
writeFileAtomic(p, text);
|
|
271
|
+
say(`wrote agents/${slug}/agent.md`);
|
|
272
|
+
const parsedMd = parseAgentMd(text);
|
|
273
|
+
const r = await client.createDefinition(
|
|
274
|
+
{ slug, title: slug, summary: parsedMd.frontmatter.description || slug, frontmatter: parsedMd.frontmatter, body: parsedMd.body, generator: { source: "human" } },
|
|
275
|
+
{ cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl },
|
|
276
|
+
);
|
|
277
|
+
if (r.ok) {
|
|
278
|
+
say(`created workspace definition ${slug}`);
|
|
279
|
+
const pinRes = await client.pinDefinition({ slug, definitionId: r.result && r.result.definitionId, version: r.result && r.result.version }, { cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl });
|
|
280
|
+
say(pinRes.ok ? `pinned ${slug}` : `pin queued/failed: ${describe(pinRes)}`);
|
|
281
|
+
} else {
|
|
282
|
+
say(`workspace create ${describe(r)}`);
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, code: 0, lines, data: { slug, path: p } };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "fork": {
|
|
288
|
+
const slug = rest[0];
|
|
289
|
+
if (!slug) return fail(lines, "usage: maestro subagents fork <slug> [--as <new-slug>]");
|
|
290
|
+
const st = await loadState(ctx);
|
|
291
|
+
const src = st.resolved.bySlug.get(slug);
|
|
292
|
+
if (!src) return fail(lines, `no sub-agent resolves for "${slug}"`);
|
|
293
|
+
const target = String(flags.as || slug);
|
|
294
|
+
// A fork OWNS its body: the copy's frontmatter.name must be the NEW slug or
|
|
295
|
+
// it will fail validation the moment it is materialised (name == dirname).
|
|
296
|
+
const fm = { ...stripProvenance(src.frontmatter), name: target };
|
|
297
|
+
const r = await client.forkDefinition(
|
|
298
|
+
{ slug: target, baseLayer: src.layer === "sdk" ? "sdk" : "none", baseSlug: slug, title: target, summary: fm.description || target, frontmatter: fm, body: src.body },
|
|
299
|
+
{ cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl },
|
|
300
|
+
);
|
|
301
|
+
say(r.ok ? `forked ${slug} → ${target}` : `fork ${describe(r)}`);
|
|
302
|
+
if (r.ok) {
|
|
303
|
+
const pinRes = await client.pinDefinition({ slug: target, definitionId: r.result && r.result.definitionId, version: r.result && r.result.version }, { cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl });
|
|
304
|
+
say(pinRes.ok ? `pinned ${target}` : `pin ${describe(pinRes)}`);
|
|
305
|
+
}
|
|
306
|
+
return { ok: !!r.ok, code: r.ok ? 0 : 1, lines, data: r };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case "publish": {
|
|
310
|
+
const slug = rest[0];
|
|
311
|
+
if (!slug) return fail(lines, "usage: maestro subagents publish <slug>");
|
|
312
|
+
const p = agentFilePath(agentRoot, slug);
|
|
313
|
+
if (!existsSync(p)) return fail(lines, `agents/${slug}/agent.md does not exist`);
|
|
314
|
+
const text = readFileSync(p, "utf8");
|
|
315
|
+
const checked = checkAgentFile(text, slug);
|
|
316
|
+
if (!checked.ok) {
|
|
317
|
+
// Refuse locally rather than let hq reject it — the operator gets the
|
|
318
|
+
// full error list instead of one server-side message.
|
|
319
|
+
for (const err of checked.errors) say(` ! ${err}`);
|
|
320
|
+
return fail(lines, `${slug} does not validate; fix it before publishing`);
|
|
321
|
+
}
|
|
322
|
+
const st = await loadState(ctx);
|
|
323
|
+
const lockEntry = st.lock.agents[slug] || {};
|
|
324
|
+
const r = await client.publishVersion(
|
|
325
|
+
{ slug, definitionId: lockEntry.definitionId || undefined, frontmatter: stripProvenance(checked.frontmatter), body: checked.body, generator: { source: "human" } },
|
|
326
|
+
{ cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl },
|
|
327
|
+
);
|
|
328
|
+
say(r.ok ? `published ${slug} v${(r.result && r.result.version) || "?"}` : `publish ${describe(r)}`);
|
|
329
|
+
return { ok: !!r.ok, code: r.ok ? 0 : 1, lines, data: r };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case "drop": {
|
|
333
|
+
const slug = rest[0];
|
|
334
|
+
if (!slug) return fail(lines, "usage: maestro subagents drop <slug> [--yank]");
|
|
335
|
+
const st = await loadState(ctx);
|
|
336
|
+
const lockEntry = st.lock.agents[slug] || {};
|
|
337
|
+
const r = await client.unpinDefinition({ slug, definitionId: lockEntry.definitionId || undefined }, { cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl });
|
|
338
|
+
say(r.ok ? `unpinned ${slug}` : `unpin ${describe(r)}`);
|
|
339
|
+
if (flags.yank) {
|
|
340
|
+
const y = await client.yankDefinition({ slug, definitionId: lockEntry.definitionId || undefined, reason: "dropped from CLI" }, { cfg: st.cfg, agentRoot, fetchImpl: opts.fetchImpl });
|
|
341
|
+
say(y.ok ? `yanked ${slug}` : `yank ${describe(y)}`);
|
|
342
|
+
}
|
|
343
|
+
// The file is NOT deleted: dropping a pin removes the workspace layer, and
|
|
344
|
+
// the next `pull` will re-resolve the slug to SDK or leave the local file.
|
|
345
|
+
acceptDrop(agentRoot, { ref: slug, reason: "unpinned via CLI" });
|
|
346
|
+
return { ok: !!r.ok, code: r.ok ? 0 : 1, lines, data: r };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case "export": {
|
|
350
|
+
const slug = rest[0];
|
|
351
|
+
const toSdk = flags["to-sdk"];
|
|
352
|
+
if (!slug || !toSdk) return fail(lines, "usage: maestro subagents export <slug> --to-sdk <path-to-sdk-checkout>");
|
|
353
|
+
const st = await loadState(ctx);
|
|
354
|
+
const e = st.resolved.bySlug.get(slug);
|
|
355
|
+
if (!e) return fail(lines, `no sub-agent resolves for "${slug}"`);
|
|
356
|
+
// Provenance is stripped: what lands in the SDK is the DEFINITION, not a
|
|
357
|
+
// record of one org's copy of it.
|
|
358
|
+
const text = stringifyAgentMd({ ...stripProvenance(e.frontmatter), name: slug }, e.body);
|
|
359
|
+
const dest = join(toSdk, "agents", slug, "agent.md");
|
|
360
|
+
mkdirSync(join(toSdk, "agents", slug), { recursive: true });
|
|
361
|
+
writeFileAtomic(dest, text);
|
|
362
|
+
const built = buildManifest({ maestroRoot: toSdk, sdkVersion: readManifest(toSdk).sdkVersion, previous: readManifest(toSdk) });
|
|
363
|
+
writeJsonAtomic(manifestPath(toSdk), built.manifest);
|
|
364
|
+
say(`wrote ${dest}`);
|
|
365
|
+
say(`updated ${manifestPath(toSdk)}`);
|
|
366
|
+
say("");
|
|
367
|
+
say("Promotion to the global set is a human PR (v1 has no hq global catalogue):");
|
|
368
|
+
say(` cd ${toSdk} && git checkout -b subagent/${slug} && git add agents/${slug} agents/manifest.json \\`);
|
|
369
|
+
say(` && git commit -m "feat(agents): add ${slug}" \\`);
|
|
370
|
+
say(` && gh pr create --title "feat(agents): add ${slug}" --body "Promoted from a workspace definition."`);
|
|
371
|
+
return { ok: true, code: 0, lines, data: { dest } };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
default:
|
|
375
|
+
say(usage());
|
|
376
|
+
return { ok: false, code: 2, lines };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function fail(lines, msg) {
|
|
381
|
+
lines.push(msg);
|
|
382
|
+
console.error(msg);
|
|
383
|
+
return { ok: false, code: 1, lines };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function describe(r) {
|
|
387
|
+
if (r.queued) return `queued offline → ${r.queued}`;
|
|
388
|
+
if (r.degraded) return "not sent (server unreachable, not enrolled, or no outbox)";
|
|
389
|
+
return `failed: ${(r.error && r.error.message) || "unknown error"}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function slim(e) {
|
|
393
|
+
return { slug: e.slug, layer: e.layer, version: e.version, source: e.source, contentHash: e.contentHash, localEdits: !!e.localEdits };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A minimal unified-ish line diff — enough to see what changed, no dependency. */
|
|
397
|
+
function lineDiff(a, b) {
|
|
398
|
+
const al = String(a).split("\n");
|
|
399
|
+
const bl = String(b).split("\n");
|
|
400
|
+
const out = [];
|
|
401
|
+
const max = Math.max(al.length, bl.length);
|
|
402
|
+
for (let i = 0; i < max; i++) {
|
|
403
|
+
if (al[i] === bl[i]) continue;
|
|
404
|
+
if (al[i] !== undefined) out.push(` - ${al[i]}`);
|
|
405
|
+
if (bl[i] !== undefined) out.push(` + ${bl[i]}`);
|
|
406
|
+
}
|
|
407
|
+
return out.slice(0, 200);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The `add` skeleton — the SAME shape `scripts/setup/generate-capability.mjs`
|
|
412
|
+
* renders, with a FULL model id and a `tools` array (the two fields that used to
|
|
413
|
+
* drift between shipped and generated agents).
|
|
414
|
+
*/
|
|
415
|
+
export function skeleton(slug, agentConfig = {}) {
|
|
416
|
+
const who = agentConfig.fullName || "the agent";
|
|
417
|
+
const fm = {
|
|
418
|
+
name: slug,
|
|
419
|
+
description: `TODO — one sentence describing when to delegate to ${slug}.`,
|
|
420
|
+
model: "claude-sonnet-4-6",
|
|
421
|
+
tools: ["Read", "Write", "Edit", "Glob", "Grep"],
|
|
422
|
+
tokens: [],
|
|
423
|
+
};
|
|
424
|
+
const body = `# ${slug}
|
|
425
|
+
|
|
426
|
+
## Mandate
|
|
427
|
+
|
|
428
|
+
TODO — what this sub-agent owns, in one paragraph.
|
|
429
|
+
|
|
430
|
+
## Operating rules
|
|
431
|
+
|
|
432
|
+
- Act within {{agent.firstName}}'s autonomy model and decision rights — see \`config/operating-charter.md\`.
|
|
433
|
+
- Follow \`policies/communication-style.md\` for any outbound communication.
|
|
434
|
+
- Escalate anything beyond your remit to {{agent.firstName}}.
|
|
435
|
+
|
|
436
|
+
_Scaffolded by \`maestro subagents add ${slug}\` for ${who}. Enrich this mandate before publishing._
|
|
437
|
+
`;
|
|
438
|
+
// Tokens are DECLARED from the body so the file validates on the first pass.
|
|
439
|
+
const used = [...new Set((body.match(/\{\{\s*([\w.]+)\s*\}\}/g) || []).map((m) => m.replace(/[{}\s]/g, "")))].sort();
|
|
440
|
+
fm.tokens = used;
|
|
441
|
+
return stringifyAgentMd(fm, body);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function usage() {
|
|
445
|
+
return [
|
|
446
|
+
"usage: maestro subagents <command>",
|
|
447
|
+
"",
|
|
448
|
+
" list resolved roster (winning layer + version per slug)",
|
|
449
|
+
" status [--json] the three-way gap diff (roster / refs / layers)",
|
|
450
|
+
" show <slug> resolved body + provenance",
|
|
451
|
+
" diff <slug> local file vs the layer it came from",
|
|
452
|
+
" add <slug> scaffold a new local def, then create + pin it",
|
|
453
|
+
" fork <slug> [--as <new>] copy the resolved body into a new org definition",
|
|
454
|
+
" publish <slug> promote the on-disk file as a new version",
|
|
455
|
+
" pull [--force] re-resolve + re-materialise (--force overwrites edits)",
|
|
456
|
+
" drop <slug> [--yank] unpin (and yank) — never a row delete",
|
|
457
|
+
" export <slug> --to-sdk <path> write into an SDK checkout + print the PR line",
|
|
458
|
+
"",
|
|
459
|
+
" --no-network never call hq (resolve from lock + cache + SDK)",
|
|
460
|
+
].join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export { usage };
|