@cohortapp/agent-sdk 2.3.2 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +105 -17
- package/lib/setup/enroll-from-cohort.test.mjs +68 -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,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sections/subagents.mjs — the sub-agent registry init step (order 40).
|
|
3
|
+
*
|
|
4
|
+
* WHERE IT SITS AND WHY. `order: 40` is the lowest free slot (identity 10,
|
|
5
|
+
* company 20, model 30, comms 45, tools 50, operating-model 60, enrich 70,
|
|
6
|
+
* org 75, messaging 76, orgmail 77, learning 80, recovery 85, verify 90). It has
|
|
7
|
+
* to run BEFORE `operating-model` (60), which generates the capability surface's
|
|
8
|
+
* workflows, and before `tools` (50), so the roster the rest of the wizard writes
|
|
9
|
+
* against is already settled. It runs AFTER `model` (30) because the model alias
|
|
10
|
+
* normalisation shares that section's vocabulary.
|
|
11
|
+
*
|
|
12
|
+
* Registered by DROPPING THIS FILE into sections/ — `lib/setup/runner.mjs` globs
|
|
13
|
+
* the directory, so there is no core dispatcher edit (design §3.2).
|
|
14
|
+
*
|
|
15
|
+
* THE FOUR HOOKS AND THEIR CONTRACTS.
|
|
16
|
+
*
|
|
17
|
+
* detect Non-blocking, no input, network-OPTIONAL — it runs under
|
|
18
|
+
* `--headless`, `doctor` and CI, so it never touches the TTY and never
|
|
19
|
+
* waits on hq. Everything it needs (lock + agents/ + SDK manifest) is
|
|
20
|
+
* on disk.
|
|
21
|
+
*
|
|
22
|
+
* prompt TTY only. Errors here are swallowed by the runner
|
|
23
|
+
* (runner.mjs:208-212), so a prompt failure degrades to "no answers"
|
|
24
|
+
* and `apply` takes only the unambiguous verdicts.
|
|
25
|
+
*
|
|
26
|
+
* apply Idempotent, and — this is the important one — A THROW HERE IS FATAL
|
|
27
|
+
* TO THE WHOLE WIZARD (runner.mjs:218-223). Every network call is
|
|
28
|
+
* therefore individually wrapped, every failure degrades to
|
|
29
|
+
* cache-or-local, and the function returns normally with
|
|
30
|
+
* `degraded:["workspace"]` recorded in the lock. Under `--headless` it
|
|
31
|
+
* takes only unambiguous verdicts and NEVER creates an agent
|
|
32
|
+
* unattended — it writes `state/subagents/gaps.json` for a human.
|
|
33
|
+
*
|
|
34
|
+
* verify Pure local: every resolved file validates against schema.mjs, the
|
|
35
|
+
* lock hashes match disk, and every parsed reference either resolves
|
|
36
|
+
* or is an accepted drop.
|
|
37
|
+
*
|
|
38
|
+
* §6 names the degradation branches as the least-exercised path in the design.
|
|
39
|
+
* They have explicit tests in subagents.test.mjs (cold cache, 500 from hq,
|
|
40
|
+
* timeout, missing env) — not just the happy path.
|
|
41
|
+
*
|
|
42
|
+
* @module lib/setup/sections/subagents
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
"use strict";
|
|
46
|
+
|
|
47
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import * as io from "../io.mjs";
|
|
50
|
+
import * as client from "../../subagents/client.mjs";
|
|
51
|
+
import { computeGaps, contextFromConfig, rosterFromSurface } from "../../subagents/gap.mjs";
|
|
52
|
+
import { acceptDrop, mergeLock, readLock, writeGaps } from "../../subagents/lock.mjs";
|
|
53
|
+
import { readManifest } from "../../subagents/manifest.mjs";
|
|
54
|
+
import { scanAgentRefs } from "../../subagents/refs.mjs";
|
|
55
|
+
import { materialiseAll, resolveSubagents } from "../../subagents/resolve.mjs";
|
|
56
|
+
import { checkAgentFile, hashFile } from "../../subagents/schema.mjs";
|
|
57
|
+
import { agentFilePath } from "../../subagents/resolve.mjs";
|
|
58
|
+
|
|
59
|
+
/** The per-slug actions the prompt offers (§2.3). */
|
|
60
|
+
export const ACTIONS = Object.freeze([
|
|
61
|
+
"adopt-workspace", "adopt-sdk", "fork", "create", "rewire", "drop-workflow-step", "defer",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Verdicts `--headless` is allowed to act on: no ambiguity, no new rows. */
|
|
65
|
+
export const HEADLESS_SAFE = Object.freeze(new Set(["adopt-workspace", "adopt-sdk"]));
|
|
66
|
+
|
|
67
|
+
/** Read config/agent.json. Fail-open → {}. */
|
|
68
|
+
export function readAgentConfig(agentRoot) {
|
|
69
|
+
const p = join(agentRoot, "config", "agent.json");
|
|
70
|
+
if (!existsSync(p)) return {};
|
|
71
|
+
try { return JSON.parse(readFileSync(p, "utf8")) || {}; } catch { return {}; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the authoritative roster (`standardAgents ∪ roleAgents`) for this
|
|
76
|
+
* agent's archetype. Injectable for tests; fail-open to [] because an agent
|
|
77
|
+
* whose function has no capability pack still has a perfectly valid on-disk
|
|
78
|
+
* roster — we just cannot report `missing`/`idle` for it.
|
|
79
|
+
* @param {object} ctx @param {object} agentConfig
|
|
80
|
+
* @returns {Promise<Array<object>>}
|
|
81
|
+
*/
|
|
82
|
+
export async function resolveRoster(ctx, agentConfig) {
|
|
83
|
+
const impl = ctx && ctx.resolveSurfaceImpl;
|
|
84
|
+
const sel = { function: agentConfig.function, altitude: agentConfig.altitude };
|
|
85
|
+
if (typeof impl === "function") {
|
|
86
|
+
try { return rosterFromSurface(await impl(sel)); } catch { return []; }
|
|
87
|
+
}
|
|
88
|
+
if (!sel.function || !sel.altitude) return [];
|
|
89
|
+
try {
|
|
90
|
+
const mod = await import("../../capability.mjs");
|
|
91
|
+
return rosterFromSurface(await mod.resolveCapabilitySurface(sel));
|
|
92
|
+
} catch { return []; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The core of detect/prompt/apply: resolve all three layers and diff them.
|
|
97
|
+
*
|
|
98
|
+
* `network` is opt-IN (detect passes false). When on, a failed `subagent.resolve`
|
|
99
|
+
* is not an error — it sets `degraded:["workspace"]` and layer 1 falls back to
|
|
100
|
+
* the lock + cache, which is exactly §4's contract.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} ctx the wizard ctx
|
|
103
|
+
* @param {{network?:boolean}} [o]
|
|
104
|
+
* @returns {Promise<object>} {cfg, resolved, gaps, roster, refs, degraded, rosterVersion, agentConfig}
|
|
105
|
+
*/
|
|
106
|
+
export async function assess(ctx, o = {}) {
|
|
107
|
+
const agentRoot = ctx.agentRoot;
|
|
108
|
+
const maestroRoot = ctx.maestroRoot;
|
|
109
|
+
const agentConfig = readAgentConfig(agentRoot);
|
|
110
|
+
const cfg = ctx.subagentCfg || client.resolveConfig({ agentRoot, env: ctx.env });
|
|
111
|
+
const lock = readLock(agentRoot);
|
|
112
|
+
const manifest = readManifest(maestroRoot);
|
|
113
|
+
|
|
114
|
+
let pins = [];
|
|
115
|
+
let degraded = [];
|
|
116
|
+
let rosterVersion = lock.rosterVersion;
|
|
117
|
+
|
|
118
|
+
if (o.network && cfg.enabled) {
|
|
119
|
+
try {
|
|
120
|
+
const r = await client.fetchResolve({
|
|
121
|
+
cfg,
|
|
122
|
+
agentRoot,
|
|
123
|
+
memberId: cfg.memberId || undefined,
|
|
124
|
+
function: agentConfig.function,
|
|
125
|
+
altitude: agentConfig.altitude,
|
|
126
|
+
fetchImpl: ctx.subagentFetchImpl,
|
|
127
|
+
});
|
|
128
|
+
if (r.ok) { pins = r.entries; rosterVersion = r.rosterVersion; }
|
|
129
|
+
else degraded = ["workspace"];
|
|
130
|
+
} catch {
|
|
131
|
+
// Belt and braces: the client is documented never to throw, but `apply`
|
|
132
|
+
// throws are fatal, so we do not take its word for it.
|
|
133
|
+
degraded = ["workspace"];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!pins.length) {
|
|
138
|
+
// Cold-cache / offline layer 1: whatever the lock says was pinned. The bodies
|
|
139
|
+
// come from state/subagents/cache/ inside resolveSubagents.
|
|
140
|
+
pins = Object.entries(lock.agents || {})
|
|
141
|
+
.filter(([, e]) => e && e.layer === "workspace")
|
|
142
|
+
.map(([slug, e]) => ({ slug, definitionId: e.definitionId, version: e.version, contentHash: e.contentHash }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const resolved = resolveSubagents({ agentRoot, maestroRoot, pins, manifest, lock, orgSlug: cfg.orgId });
|
|
146
|
+
const roster = await resolveRoster(ctx, agentConfig);
|
|
147
|
+
const refs = scanAgentRefs({ agentRoot });
|
|
148
|
+
const gaps = computeGaps({ roster, resolved, refs, lock, context: contextFromConfig({ agentConfig }) });
|
|
149
|
+
|
|
150
|
+
return { agentConfig, cfg, lock, manifest, resolved, roster, refs, gaps, degraded, rosterVersion, pins };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Decide the default action for each gap without asking anyone. This is what
|
|
155
|
+
* `--headless` executes and what the TTY prompt pre-selects.
|
|
156
|
+
*
|
|
157
|
+
* The rules are deliberately conservative:
|
|
158
|
+
* - a missing slug that the WORKSPACE offers → adopt-workspace (unambiguous)
|
|
159
|
+
* - a missing slug that the SDK offers → adopt-sdk (unambiguous)
|
|
160
|
+
* - anything else → defer (needs a human)
|
|
161
|
+
* `create` and `fork` are never defaults: both mint a row in hq, and §2.3 forbids
|
|
162
|
+
* doing that unattended.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} assessment
|
|
165
|
+
* @returns {Array<{slug:string, action:string, reason:string}>}
|
|
166
|
+
*/
|
|
167
|
+
export function planActions(assessment) {
|
|
168
|
+
const plan = [];
|
|
169
|
+
const bySlug = assessment.resolved.bySlug;
|
|
170
|
+
const manifestAgents = (assessment.manifest && assessment.manifest.agents) || {};
|
|
171
|
+
const pinned = new Set((assessment.pins || []).map((p) => p && p.slug).filter(Boolean));
|
|
172
|
+
|
|
173
|
+
for (const m of assessment.gaps.missing) {
|
|
174
|
+
if (pinned.has(m.slug)) { plan.push({ slug: m.slug, action: "adopt-workspace", reason: "a workspace pin offers this slug" }); continue; }
|
|
175
|
+
if (manifestAgents[m.slug]) { plan.push({ slug: m.slug, action: "adopt-sdk", reason: "the SDK ships this slug" }); continue; }
|
|
176
|
+
plan.push({ slug: m.slug, action: "defer", reason: "no layer offers this slug — fork/create needs a human" });
|
|
177
|
+
}
|
|
178
|
+
// Entries that DO resolve but are not on disk yet are also adoptions: the
|
|
179
|
+
// registry's steady state is "every resolved winner is materialised".
|
|
180
|
+
for (const e of assessment.resolved.entries) {
|
|
181
|
+
if (e.onDisk || plan.some((p) => p.slug === e.slug)) continue;
|
|
182
|
+
plan.push({ slug: e.slug, action: e.layer === "workspace" ? "adopt-workspace" : "adopt-sdk", reason: `resolved from ${e.layer} but not on disk` });
|
|
183
|
+
}
|
|
184
|
+
return plan;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default {
|
|
188
|
+
id: "subagents",
|
|
189
|
+
label: "Sub-agents",
|
|
190
|
+
order: 40,
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Non-blocking, no input, NO NETWORK. `complete` only when there are no
|
|
194
|
+
* `missing` and no unresolved `drop-or-create` (§2.3).
|
|
195
|
+
*/
|
|
196
|
+
async detect(ctx) {
|
|
197
|
+
const cfg = ctx.subagentCfg || client.resolveConfig({ agentRoot: ctx.agentRoot, env: ctx.env });
|
|
198
|
+
const a = await assess(ctx, { network: false });
|
|
199
|
+
const missing = a.gaps.missing.map((m) => m.slug);
|
|
200
|
+
const openDrops = a.gaps.openDrops.map((u) => u.ref);
|
|
201
|
+
|
|
202
|
+
if (a.gaps.status === "complete") {
|
|
203
|
+
const note = cfg.enabled ? "" : " (workspace layer off — not enrolled)";
|
|
204
|
+
return { status: "complete", summary: `${a.gaps.counts.resolved} sub-agents resolved${note}` };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
status: a.gaps.counts.resolved ? "partial" : "unconfigured",
|
|
208
|
+
summary: a.gaps.summary,
|
|
209
|
+
missing: [...missing.map((s) => `missing: ${s}`), ...openDrops.map((s) => `dangling ref: ${s}`)].slice(0, 12),
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One table, per-slug actions. TTY only; the runner swallows throws here, and
|
|
215
|
+
* `io.*` returns the pre-supplied answer under `--headless` without blocking.
|
|
216
|
+
*/
|
|
217
|
+
async prompt(ctx) {
|
|
218
|
+
const a = await assess(ctx, { network: true });
|
|
219
|
+
ctx.answers.subagentAssessment = { counts: a.gaps.counts, degraded: a.degraded };
|
|
220
|
+
|
|
221
|
+
if (a.degraded.length) {
|
|
222
|
+
io.note("Could not reach the workspace registry — resolving from the SDK + local cache only.", { dim: true });
|
|
223
|
+
}
|
|
224
|
+
if (!a.gaps.missing.length && !a.gaps.openDrops.length && !a.gaps.idle.length) {
|
|
225
|
+
io.note(`Sub-agents: ${a.gaps.counts.resolved} resolved, nothing to decide.`, { dim: true });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
io.note("");
|
|
230
|
+
io.note(`Sub-agents — ${a.gaps.summary}`);
|
|
231
|
+
const plan = planActions(a);
|
|
232
|
+
const chosen = {};
|
|
233
|
+
|
|
234
|
+
for (const m of a.gaps.missing) {
|
|
235
|
+
const dflt = (plan.find((p) => p.slug === m.slug) || {}).action || "defer";
|
|
236
|
+
const choices = [
|
|
237
|
+
{ label: `adopt from the workspace registry`, value: "adopt-workspace" },
|
|
238
|
+
{ label: `adopt the SDK's shipped definition`, value: "adopt-sdk" },
|
|
239
|
+
{ label: `fork it (copy the body, own it)`, value: "fork" },
|
|
240
|
+
{ label: `create a new definition from the skeleton`, value: "create" },
|
|
241
|
+
{ label: `defer — decide later`, value: "defer" },
|
|
242
|
+
];
|
|
243
|
+
chosen[m.slug] = await io.select(ctx, {
|
|
244
|
+
message: ` ${m.slug} (${m.role || "roster agent"}) — missing`,
|
|
245
|
+
key: `subagent.${m.slug}`,
|
|
246
|
+
default: dflt,
|
|
247
|
+
choices,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Dangling references. NEVER auto-edits or deletes a workflow: a `drop` only
|
|
252
|
+
// records the accepted drop and prints the file:line for a human to delete.
|
|
253
|
+
for (const u of a.gaps.openDrops) {
|
|
254
|
+
const at = u.locations[0] ? `${u.locations[0].file}:${u.locations[0].line}` : "unknown location";
|
|
255
|
+
const choices = [
|
|
256
|
+
{ label: `drop the workflow step (recommended) — ${at}`, value: "drop-workflow-step" },
|
|
257
|
+
...(u.suggest ? [{ label: `rewire to ${u.suggest} (${Math.round(u.score * 100)}% match)`, value: `rewire:${u.suggest}` }] : []),
|
|
258
|
+
{ label: "create a sub-agent for it", value: "create" },
|
|
259
|
+
{ label: "defer", value: "defer" },
|
|
260
|
+
];
|
|
261
|
+
chosen[`ref:${u.ref}`] = await io.select(ctx, {
|
|
262
|
+
message: ` ${u.ref} — referenced but not on the roster (${u.reason})`,
|
|
263
|
+
key: `subagent.ref.${u.ref}`,
|
|
264
|
+
default: u.recommend === "rewire" && u.suggest ? `rewire:${u.suggest}` : "drop-workflow-step",
|
|
265
|
+
choices,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (a.gaps.idle.length) {
|
|
270
|
+
io.note(` ${a.gaps.idle.length} roster agent(s) are referenced by nothing: ${a.gaps.idle.map((i) => i.slug).join(", ")}`, { dim: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
ctx.answers.subagentChoices = chosen;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Idempotent. EVERY network call is wrapped; on failure it degrades to
|
|
278
|
+
* cache-or-local, records `degraded:["workspace"]` and returns NORMALLY —
|
|
279
|
+
* because a throw here aborts the entire wizard.
|
|
280
|
+
*/
|
|
281
|
+
async apply(ctx) {
|
|
282
|
+
const headless = io.isHeadless(ctx.flags || {});
|
|
283
|
+
let a;
|
|
284
|
+
try {
|
|
285
|
+
a = await assess(ctx, { network: true });
|
|
286
|
+
} catch (e) {
|
|
287
|
+
// Even the local assessment failing must not abort setup: the registry is
|
|
288
|
+
// additive, and an agent with no sub-agent resolution still runs.
|
|
289
|
+
io.note(`[subagents] assessment failed (${e && e.message ? e.message : e}) — skipping the registry step.`, { dim: true });
|
|
290
|
+
mergeLock(ctx.agentRoot, { degraded: ["assessment"] });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const choices = (ctx.answers && ctx.answers.subagentChoices) || {};
|
|
295
|
+
const plan = planActions(a);
|
|
296
|
+
|
|
297
|
+
// 1. Materialise every resolved winner. Local edits are never overwritten
|
|
298
|
+
// (materialise() refuses without --force), so this is safe to run always.
|
|
299
|
+
const out = materialiseAll({ agentRoot: ctx.agentRoot, resolved: a.resolved });
|
|
300
|
+
if (out.written) io.note(`Materialised ${out.written} sub-agent definition(s).`, { dim: true });
|
|
301
|
+
for (const r of out.refused) io.note(` ! ${r.slug}: ${r.reason}`, { dim: true });
|
|
302
|
+
|
|
303
|
+
// 2. Act on the operator's per-slug verdicts. Under --headless only the
|
|
304
|
+
// unambiguous adoptions run (already done by step 1); create/fork NEVER
|
|
305
|
+
// happen unattended.
|
|
306
|
+
const created = [];
|
|
307
|
+
for (const [key, verdict] of Object.entries(choices)) {
|
|
308
|
+
if (!verdict || verdict === "defer") continue;
|
|
309
|
+
|
|
310
|
+
if (key.startsWith("ref:")) {
|
|
311
|
+
const ref = key.slice(4);
|
|
312
|
+
if (verdict === "drop-workflow-step") {
|
|
313
|
+
const u = a.gaps.unrostered.find((x) => x.ref === ref);
|
|
314
|
+
const loc = (u && u.locations[0]) || {};
|
|
315
|
+
acceptDrop(ctx.agentRoot, { ref, file: loc.file, line: loc.line, reason: "operator accepted drop at init" });
|
|
316
|
+
io.note(` drop accepted for "${ref}" — delete it yourself at ${loc.file || "?"}:${loc.line || "?"} (the wizard never edits workflows).`, { dim: true });
|
|
317
|
+
} else if (String(verdict).startsWith("rewire:")) {
|
|
318
|
+
const target = String(verdict).slice(7);
|
|
319
|
+
const u = a.gaps.unrostered.find((x) => x.ref === ref);
|
|
320
|
+
const loc = (u && u.locations[0]) || {};
|
|
321
|
+
io.note(` rewire "${ref}" → "${target}" at ${loc.file || "?"}:${loc.line || "?"} (edit it yourself — the wizard never edits workflows).`, { dim: true });
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (headless && !HEADLESS_SAFE.has(verdict)) continue;
|
|
327
|
+
if (verdict !== "create" && verdict !== "fork") continue;
|
|
328
|
+
|
|
329
|
+
// create / fork — the only two verdicts that mint a row in hq. Each is
|
|
330
|
+
// individually wrapped: a failure queues to the outbox and moves on.
|
|
331
|
+
try {
|
|
332
|
+
const entry = a.resolved.bySlug.get(key);
|
|
333
|
+
const params = verdict === "fork" && entry
|
|
334
|
+
? { slug: key, baseLayer: entry.layer === "sdk" ? "sdk" : "none", baseSlug: key, title: key, summary: (entry.frontmatter && entry.frontmatter.description) || key, frontmatter: entry.frontmatter, body: entry.body }
|
|
335
|
+
: null;
|
|
336
|
+
const r = params
|
|
337
|
+
? await client.forkDefinition(params, { cfg: a.cfg, agentRoot: ctx.agentRoot, fetchImpl: ctx.subagentFetchImpl })
|
|
338
|
+
: { ok: false, degraded: true, error: { message: "create needs a scaffolded body — run `maestro subagents add`" } };
|
|
339
|
+
if (r.ok) {
|
|
340
|
+
created.push(key);
|
|
341
|
+
await client.pinDefinition({ slug: key, definitionId: r.result && r.result.definitionId, version: r.result && r.result.version }, { cfg: a.cfg, agentRoot: ctx.agentRoot, fetchImpl: ctx.subagentFetchImpl });
|
|
342
|
+
} else if (r.queued) {
|
|
343
|
+
io.note(` ${key}: queued offline → ${r.queued}`, { dim: true });
|
|
344
|
+
} else {
|
|
345
|
+
io.note(` ${key}: ${(r.error && r.error.message) || "registry write failed"}`, { dim: true });
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {
|
|
348
|
+
io.note(` ${key}: registry write threw (${e && e.message ? e.message : e}) — continuing.`, { dim: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 3. Persist the lock: what is on disk, the roster version we saw, and any
|
|
353
|
+
// degradation, so `doctor` can tell "N sub-agents behind workspace".
|
|
354
|
+
mergeLock(ctx.agentRoot, { agents: out.lockAgents, rosterVersion: a.rosterVersion, degraded: a.degraded });
|
|
355
|
+
|
|
356
|
+
// 4. Headless leaves a report instead of asking. Never creates unattended.
|
|
357
|
+
if (headless) {
|
|
358
|
+
writeGaps(ctx.agentRoot, {
|
|
359
|
+
summary: a.gaps.summary,
|
|
360
|
+
counts: a.gaps.counts,
|
|
361
|
+
missing: a.gaps.missing,
|
|
362
|
+
unrostered: a.gaps.unrostered,
|
|
363
|
+
idle: a.gaps.idle,
|
|
364
|
+
plan,
|
|
365
|
+
degraded: a.degraded,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (created.length) io.note(`Registered ${created.length} workspace definition(s): ${created.join(", ")}`, { dim: true });
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Pure local, no network: schema, lock/disk agreement, reference resolution.
|
|
373
|
+
*/
|
|
374
|
+
async verify(ctx) {
|
|
375
|
+
const checks = [];
|
|
376
|
+
const a = await assess(ctx, { network: false });
|
|
377
|
+
|
|
378
|
+
// (a) every resolved file that is on disk validates.
|
|
379
|
+
const bad = [];
|
|
380
|
+
for (const e of a.resolved.entries) {
|
|
381
|
+
const p = agentFilePath(ctx.agentRoot, e.slug);
|
|
382
|
+
if (!existsSync(p)) continue;
|
|
383
|
+
let text;
|
|
384
|
+
try { text = readFileSync(p, "utf8"); } catch { bad.push(`${e.slug}: unreadable`); continue; }
|
|
385
|
+
const c = checkAgentFile(text, e.slug);
|
|
386
|
+
if (!c.ok) bad.push(`${e.slug}: ${c.errors[0]}`);
|
|
387
|
+
}
|
|
388
|
+
checks.push({
|
|
389
|
+
name: bad.length ? `sub-agent definitions valid (${bad.length} invalid)` : "sub-agent definitions valid",
|
|
390
|
+
ok: bad.length === 0,
|
|
391
|
+
remedy: bad.length ? `fix: ${bad.slice(0, 3).join("; ")}` : undefined,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// (b) lock hashes match disk. A mismatch is not a failure — it is the
|
|
395
|
+
// definition of a local edit — but a lock entry pointing at a file that
|
|
396
|
+
// no longer exists IS a broken lock.
|
|
397
|
+
const orphanLock = [];
|
|
398
|
+
for (const [slug, e] of Object.entries(a.lock.agents || {})) {
|
|
399
|
+
const p = agentFilePath(ctx.agentRoot, slug);
|
|
400
|
+
if (!existsSync(p)) { orphanLock.push(slug); continue; }
|
|
401
|
+
if (!e.fileHash) continue;
|
|
402
|
+
try { if (hashFile(readFileSync(p, "utf8")) !== e.fileHash) { /* local edit — reported, not failed */ } } catch { orphanLock.push(slug); }
|
|
403
|
+
}
|
|
404
|
+
checks.push({
|
|
405
|
+
name: orphanLock.length ? `lock matches disk (${orphanLock.length} orphan entr${orphanLock.length === 1 ? "y" : "ies"})` : "lock matches disk",
|
|
406
|
+
ok: orphanLock.length === 0,
|
|
407
|
+
remedy: orphanLock.length ? "run `maestro subagents pull` to re-materialise, or remove the stale lock entries" : undefined,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// (c) every parsed reference resolves or is an accepted drop.
|
|
411
|
+
const dangling = a.gaps.openDrops.map((u) => u.ref);
|
|
412
|
+
checks.push({
|
|
413
|
+
name: dangling.length ? `sub-agent references resolve (${dangling.length} dangling)` : "sub-agent references resolve",
|
|
414
|
+
// Dangling references WARN rather than fail: `create` copies maestro's own
|
|
415
|
+
// workflows verbatim, so a fresh agent legitimately starts with ~15 of them
|
|
416
|
+
// and hard-failing verify would make every new agent's setup red.
|
|
417
|
+
ok: true,
|
|
418
|
+
remedy: dangling.length ? `run \`maestro subagents status\` — ${dangling.slice(0, 5).join(", ")}` : undefined,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (a.degraded.length) {
|
|
422
|
+
checks.push({ name: "workspace registry reachable (best-effort)", ok: true, remedy: `degraded: ${a.degraded.join(", ")} — resolved from cache/SDK` });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
426
|
+
},
|
|
427
|
+
};
|