@agentplate/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/agents/architect.md +108 -0
- package/agents/builder.md +97 -0
- package/agents/coordinator.md +113 -0
- package/agents/deployer.md +117 -0
- package/agents/devops.md +114 -0
- package/agents/lead.md +107 -0
- package/agents/merger.md +103 -0
- package/agents/reviewer.md +90 -0
- package/agents/scout.md +95 -0
- package/agents/verifier.md +106 -0
- package/package.json +64 -0
- package/src/agents/guard-rules.ts +55 -0
- package/src/agents/identity.test.ts +161 -0
- package/src/agents/identity.ts +229 -0
- package/src/agents/manifest.test.ts +260 -0
- package/src/agents/manifest.ts +286 -0
- package/src/agents/overlay.test.ts +190 -0
- package/src/agents/overlay.ts +212 -0
- package/src/agents/system-prompt.test.ts +53 -0
- package/src/agents/system-prompt.ts +95 -0
- package/src/agents/turn-runner.ts +79 -0
- package/src/commands/coordinator.test.ts +75 -0
- package/src/commands/coordinator.ts +259 -0
- package/src/commands/deploy.test.ts +504 -0
- package/src/commands/deploy.ts +874 -0
- package/src/commands/doctor.test.ts +106 -0
- package/src/commands/doctor.ts +208 -0
- package/src/commands/init.ts +71 -0
- package/src/commands/log.ts +51 -0
- package/src/commands/mail.ts +197 -0
- package/src/commands/merge.ts +127 -0
- package/src/commands/model.ts +58 -0
- package/src/commands/prime.ts +61 -0
- package/src/commands/reap.ts +87 -0
- package/src/commands/serve.ts +61 -0
- package/src/commands/setup.ts +48 -0
- package/src/commands/ship.test.ts +106 -0
- package/src/commands/ship.ts +202 -0
- package/src/commands/skill.test.ts +458 -0
- package/src/commands/skill.ts +730 -0
- package/src/commands/sling.ts +365 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/stop.ts +56 -0
- package/src/commands/tui.ts +199 -0
- package/src/commands/worktree.ts +77 -0
- package/src/config.test.ts +92 -0
- package/src/config.ts +202 -0
- package/src/db/sqlite.test.ts +77 -0
- package/src/db/sqlite.ts +102 -0
- package/src/deploy/audit.test.ts +233 -0
- package/src/deploy/audit.ts +245 -0
- package/src/deploy/context.test.ts +243 -0
- package/src/deploy/context.ts +72 -0
- package/src/deploy/registry.test.ts +101 -0
- package/src/deploy/registry.ts +86 -0
- package/src/deploy/secrets.test.ts +129 -0
- package/src/deploy/secrets.ts +69 -0
- package/src/deploy/targets/docker-gha.test.ts +323 -0
- package/src/deploy/targets/docker-gha.ts +841 -0
- package/src/deploy/types.ts +153 -0
- package/src/errors.test.ts +42 -0
- package/src/errors.ts +69 -0
- package/src/events/store.test.ts +183 -0
- package/src/events/store.ts +201 -0
- package/src/index.ts +137 -0
- package/src/insights/quality-gates.ts +73 -0
- package/src/json.test.ts +28 -0
- package/src/json.ts +50 -0
- package/src/logging/color.ts +62 -0
- package/src/logging/logger.ts +60 -0
- package/src/logging/sanitizer.test.ts +36 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/client.test.ts +192 -0
- package/src/mail/client.ts +188 -0
- package/src/mail/store.test.ts +279 -0
- package/src/mail/store.ts +311 -0
- package/src/merge/lock.test.ts +88 -0
- package/src/merge/lock.ts +84 -0
- package/src/merge/queue.test.ts +136 -0
- package/src/merge/queue.ts +177 -0
- package/src/merge/resolver.test.ts +219 -0
- package/src/merge/resolver.ts +274 -0
- package/src/paths.ts +36 -0
- package/src/providers/apply.test.ts +90 -0
- package/src/providers/apply.ts +66 -0
- package/src/providers/registry.test.ts +74 -0
- package/src/providers/registry.ts +254 -0
- package/src/runtimes/claude.ts +313 -0
- package/src/runtimes/codex.ts +280 -0
- package/src/runtimes/cursor.ts +247 -0
- package/src/runtimes/gemini.ts +173 -0
- package/src/runtimes/mock.ts +71 -0
- package/src/runtimes/opencode.ts +259 -0
- package/src/runtimes/registry.test.ts +924 -0
- package/src/runtimes/registry.ts +63 -0
- package/src/runtimes/resolve.ts +45 -0
- package/src/runtimes/types.ts +97 -0
- package/src/scaffold.ts +68 -0
- package/src/secrets.test.ts +51 -0
- package/src/secrets.ts +78 -0
- package/src/serve/api.ts +667 -0
- package/src/serve/server.test.ts +433 -0
- package/src/serve/server.ts +271 -0
- package/src/serve/system.ts +90 -0
- package/src/serve/weather.ts +140 -0
- package/src/sessions/reaper.test.ts +162 -0
- package/src/sessions/reaper.ts +149 -0
- package/src/sessions/store.test.ts +351 -0
- package/src/sessions/store.ts +350 -0
- package/src/skills/distiller.test.ts +498 -0
- package/src/skills/distiller.ts +426 -0
- package/src/skills/feedback.test.ts +300 -0
- package/src/skills/feedback.ts +168 -0
- package/src/skills/lifecycle.ts +169 -0
- package/src/skills/retrieval.test.ts +421 -0
- package/src/skills/retrieval.ts +365 -0
- package/src/skills/safety.test.ts +335 -0
- package/src/skills/safety.ts +216 -0
- package/src/skills/store.test.ts +425 -0
- package/src/skills/store.ts +684 -0
- package/src/skills/types.ts +107 -0
- package/src/types.ts +442 -0
- package/src/utils/detect.test.ts +35 -0
- package/src/utils/detect.ts +82 -0
- package/src/version.test.ts +19 -0
- package/src/version.ts +7 -0
- package/src/wizard/setup.ts +254 -0
- package/src/worktree/manager.test.ts +181 -0
- package/src/worktree/manager.ts +229 -0
- package/templates/overlay.md.tmpl +102 -0
- package/ui/dist/assets/index-C7rXIMER.css +1 -0
- package/ui/dist/assets/index-W4kbr4by.js +4526 -0
- package/ui/dist/favicon.svg +21 -0
- package/ui/dist/index.html +16 -0
- package/ui/dist/logo-clay.svg +21 -0
- package/ui/dist/logo.svg +18 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the agent manifest.
|
|
3
|
+
*
|
|
4
|
+
* Uses a real temp directory and the real JSON round-trip on disk — no mocks,
|
|
5
|
+
* per the project testing philosophy. Each test that touches the filesystem
|
|
6
|
+
* gets its own project root so files are fully isolated.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { ConfigError, NotFoundError } from "../errors.ts";
|
|
14
|
+
import type { Capability } from "../types.ts";
|
|
15
|
+
import {
|
|
16
|
+
buildDefaultManifest,
|
|
17
|
+
getDefinition,
|
|
18
|
+
loadManifest,
|
|
19
|
+
MANIFEST_VERSION,
|
|
20
|
+
writeManifest,
|
|
21
|
+
} from "./manifest.ts";
|
|
22
|
+
|
|
23
|
+
/** The six orchestration-core capabilities the default manifest must define. */
|
|
24
|
+
const CORE_CAPABILITIES: readonly Capability[] = [
|
|
25
|
+
"scout",
|
|
26
|
+
"builder",
|
|
27
|
+
"reviewer",
|
|
28
|
+
"lead",
|
|
29
|
+
"merger",
|
|
30
|
+
"coordinator",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** The four delivery-pipeline capabilities (Phase 4) added to the manifest. */
|
|
34
|
+
const PIPELINE_CAPABILITIES: readonly Capability[] = [
|
|
35
|
+
"architect",
|
|
36
|
+
"devops",
|
|
37
|
+
"deployer",
|
|
38
|
+
"verifier",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Every capability the default manifest must define (core + pipeline = ten). */
|
|
42
|
+
const ALL_CAPABILITIES: readonly Capability[] = [...CORE_CAPABILITIES, ...PIPELINE_CAPABILITIES];
|
|
43
|
+
|
|
44
|
+
describe("buildDefaultManifest", () => {
|
|
45
|
+
test("defines exactly the ten capabilities (six core + four pipeline)", () => {
|
|
46
|
+
const manifest = buildDefaultManifest();
|
|
47
|
+
const keys = Object.keys(manifest.agents).sort();
|
|
48
|
+
expect(keys.length).toBe(10);
|
|
49
|
+
expect(keys).toEqual([...ALL_CAPABILITIES].sort());
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("stamps the current manifest version", () => {
|
|
53
|
+
expect(buildDefaultManifest().version).toBe(MANIFEST_VERSION);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("every definition declares the capability it is keyed under", () => {
|
|
57
|
+
const manifest = buildDefaultManifest();
|
|
58
|
+
for (const cap of ALL_CAPABILITIES) {
|
|
59
|
+
const def = manifest.agents[cap];
|
|
60
|
+
expect(def).toBeDefined();
|
|
61
|
+
expect(def?.capabilities).toEqual([cap]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("only lead and coordinator can spawn children", () => {
|
|
66
|
+
const manifest = buildDefaultManifest();
|
|
67
|
+
expect(manifest.agents.lead?.canSpawn).toBe(true);
|
|
68
|
+
expect(manifest.agents.coordinator?.canSpawn).toBe(true);
|
|
69
|
+
expect(manifest.agents.scout?.canSpawn).toBe(false);
|
|
70
|
+
expect(manifest.agents.builder?.canSpawn).toBe(false);
|
|
71
|
+
expect(manifest.agents.reviewer?.canSpawn).toBe(false);
|
|
72
|
+
expect(manifest.agents.merger?.canSpawn).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("the four pipeline roles are all leaves (canSpawn=false)", () => {
|
|
76
|
+
const manifest = buildDefaultManifest();
|
|
77
|
+
for (const cap of PIPELINE_CAPABILITIES) {
|
|
78
|
+
expect(manifest.agents[cap]?.canSpawn).toBe(false);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("pipeline roles use their <capability>.md base file", () => {
|
|
83
|
+
const manifest = buildDefaultManifest();
|
|
84
|
+
expect(manifest.agents.architect?.file).toBe("architect.md");
|
|
85
|
+
expect(manifest.agents.devops?.file).toBe("devops.md");
|
|
86
|
+
expect(manifest.agents.deployer?.file).toBe("deployer.md");
|
|
87
|
+
expect(manifest.agents.verifier?.file).toBe("verifier.md");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("pipeline roles carry the specified model tiers", () => {
|
|
91
|
+
const manifest = buildDefaultManifest();
|
|
92
|
+
// architect/deployer reason about high-stakes plans/deploys → opus.
|
|
93
|
+
expect(manifest.agents.architect?.model).toBe("opus");
|
|
94
|
+
expect(manifest.agents.deployer?.model).toBe("opus");
|
|
95
|
+
// devops/verifier are mechanical/probing → a cheaper tier (sonnet or haiku).
|
|
96
|
+
expect(["sonnet", "haiku"]).toContain(manifest.agents.devops?.model ?? "");
|
|
97
|
+
expect(["sonnet", "haiku"]).toContain(manifest.agents.verifier?.model ?? "");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("architect and verifier are read-only (no Edit/Write); devops and deployer are write-capable", () => {
|
|
101
|
+
const manifest = buildDefaultManifest();
|
|
102
|
+
for (const cap of ["architect", "verifier"] as const) {
|
|
103
|
+
const tools = manifest.agents[cap]?.tools ?? [];
|
|
104
|
+
expect(tools).toContain("Read");
|
|
105
|
+
expect(tools).not.toContain("Edit");
|
|
106
|
+
expect(tools).not.toContain("Write");
|
|
107
|
+
}
|
|
108
|
+
for (const cap of ["devops", "deployer"] as const) {
|
|
109
|
+
const tools = manifest.agents[cap]?.tools ?? [];
|
|
110
|
+
expect(tools).toContain("Edit");
|
|
111
|
+
expect(tools).toContain("Write");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("assigns the specified model tiers", () => {
|
|
116
|
+
const manifest = buildDefaultManifest();
|
|
117
|
+
expect(manifest.agents.scout?.model).toBe("sonnet");
|
|
118
|
+
expect(manifest.agents.builder?.model).toBe("sonnet");
|
|
119
|
+
expect(manifest.agents.reviewer?.model).toBe("haiku");
|
|
120
|
+
expect(manifest.agents.lead?.model).toBe("opus");
|
|
121
|
+
expect(manifest.agents.merger?.model).toBe("opus");
|
|
122
|
+
expect(manifest.agents.coordinator?.model).toBe("opus");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("read-only roles lack Edit and Write", () => {
|
|
126
|
+
const manifest = buildDefaultManifest();
|
|
127
|
+
for (const cap of ["scout", "reviewer"] as const) {
|
|
128
|
+
const tools = manifest.agents[cap]?.tools ?? [];
|
|
129
|
+
expect(tools).toContain("Read");
|
|
130
|
+
expect(tools).toContain("Bash");
|
|
131
|
+
expect(tools).not.toContain("Edit");
|
|
132
|
+
expect(tools).not.toContain("Write");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("write-capable roles include Edit and Write", () => {
|
|
137
|
+
const manifest = buildDefaultManifest();
|
|
138
|
+
for (const cap of ["builder", "lead", "merger", "coordinator"] as const) {
|
|
139
|
+
const tools = manifest.agents[cap]?.tools ?? [];
|
|
140
|
+
expect(tools).toContain("Edit");
|
|
141
|
+
expect(tools).toContain("Write");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("merger includes Bash and Edit", () => {
|
|
146
|
+
const manifest = buildDefaultManifest();
|
|
147
|
+
const tools = manifest.agents.merger?.tools ?? [];
|
|
148
|
+
expect(tools).toContain("Bash");
|
|
149
|
+
expect(tools).toContain("Edit");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("each definition has a <capability>.md file and non-empty constraints", () => {
|
|
153
|
+
const manifest = buildDefaultManifest();
|
|
154
|
+
for (const cap of ALL_CAPABILITIES) {
|
|
155
|
+
const def = manifest.agents[cap];
|
|
156
|
+
expect(def?.file).toBe(`${cap}.md`);
|
|
157
|
+
expect(def?.constraints.length ?? 0).toBeGreaterThan(0);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("capabilityIndex maps each capability to a list containing itself", () => {
|
|
162
|
+
const manifest = buildDefaultManifest();
|
|
163
|
+
for (const cap of ALL_CAPABILITIES) {
|
|
164
|
+
expect(manifest.capabilityIndex[cap]).toEqual([cap]);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("returns a fresh object each call (no shared mutable state)", () => {
|
|
169
|
+
const a = buildDefaultManifest();
|
|
170
|
+
const b = buildDefaultManifest();
|
|
171
|
+
expect(a).not.toBe(b);
|
|
172
|
+
expect(a.agents).not.toBe(b.agents);
|
|
173
|
+
// Mutating one must not affect the other.
|
|
174
|
+
a.agents.scout?.tools.push("Injected");
|
|
175
|
+
expect(b.agents.scout?.tools).not.toContain("Injected");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("getDefinition", () => {
|
|
180
|
+
test("returns the definition for a present capability", () => {
|
|
181
|
+
const manifest = buildDefaultManifest();
|
|
182
|
+
const def = getDefinition(manifest, "builder");
|
|
183
|
+
expect(def.file).toBe("builder.md");
|
|
184
|
+
expect(def.canSpawn).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("throws NotFoundError for an absent capability", () => {
|
|
188
|
+
// All ten Capability members are defined by default, so delete one first
|
|
189
|
+
// to exercise the lookup-miss branch without fabricating an off-union value.
|
|
190
|
+
const manifest = buildDefaultManifest();
|
|
191
|
+
delete manifest.agents.architect;
|
|
192
|
+
expect(() => getDefinition(manifest, "architect")).toThrow(NotFoundError);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("writeManifest + loadManifest", () => {
|
|
197
|
+
let root: string;
|
|
198
|
+
let path: string;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
root = mkdtempSync(join(tmpdir(), "agentplate-manifest-"));
|
|
202
|
+
path = join(root, "agent-manifest.json");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
rmSync(root, { recursive: true, force: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("round-trips the default manifest unchanged", () => {
|
|
210
|
+
const original = buildDefaultManifest();
|
|
211
|
+
writeManifest(path, original);
|
|
212
|
+
const loaded = loadManifest(path);
|
|
213
|
+
expect(loaded).toEqual(original);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("writes pretty 2-space JSON with a trailing newline", () => {
|
|
217
|
+
writeManifest(path, buildDefaultManifest());
|
|
218
|
+
const raw = readFileSync(path, "utf8");
|
|
219
|
+
expect(raw.endsWith("\n")).toBe(true);
|
|
220
|
+
// 2-space indent: the top-level "version" key sits two spaces in.
|
|
221
|
+
expect(raw).toContain('\n "version"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("loadManifest throws NotFoundError when the file is absent", () => {
|
|
225
|
+
expect(() => loadManifest(join(root, "nope.json"))).toThrow(NotFoundError);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("loadManifest throws ConfigError on invalid JSON", () => {
|
|
229
|
+
writeFileSync(path, "{ not valid json ");
|
|
230
|
+
expect(() => loadManifest(path)).toThrow(ConfigError);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("loadManifest throws ConfigError on a non-object JSON value", () => {
|
|
234
|
+
writeFileSync(path, "[]");
|
|
235
|
+
expect(() => loadManifest(path)).toThrow(ConfigError);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("loadManifest throws ConfigError when version is missing", () => {
|
|
239
|
+
writeFileSync(path, JSON.stringify({ agents: {}, capabilityIndex: {} }));
|
|
240
|
+
expect(() => loadManifest(path)).toThrow(ConfigError);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("loadManifest throws ConfigError when agents is missing", () => {
|
|
244
|
+
writeFileSync(path, JSON.stringify({ version: "1", capabilityIndex: {} }));
|
|
245
|
+
expect(() => loadManifest(path)).toThrow(ConfigError);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("loadManifest throws ConfigError when capabilityIndex is missing", () => {
|
|
249
|
+
writeFileSync(path, JSON.stringify({ version: "1", agents: {} }));
|
|
250
|
+
expect(() => loadManifest(path)).toThrow(ConfigError);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a definition loaded from disk is queryable via getDefinition", () => {
|
|
254
|
+
writeManifest(path, buildDefaultManifest());
|
|
255
|
+
const loaded = loadManifest(path);
|
|
256
|
+
const lead = getDefinition(loaded, "lead");
|
|
257
|
+
expect(lead.canSpawn).toBe(true);
|
|
258
|
+
expect(lead.model).toBe("opus");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent manifest: the static registry of capabilities.
|
|
3
|
+
*
|
|
4
|
+
* The manifest answers "what kinds of agents can Agentplate spawn, and with what
|
|
5
|
+
* powers?" — the *HOW* of each role (model tier, allowed tools, spawn rights,
|
|
6
|
+
* hard constraints). It is the Layer-1 base; the per-task overlay (see
|
|
7
|
+
* {@link OverlayConfig}) supplies the Layer-2 *WHAT*.
|
|
8
|
+
*
|
|
9
|
+
* Each {@link AgentDefinition} is keyed by the single {@link Capability} it
|
|
10
|
+
* provides, so the manifest is a direct `Capability -> definition` map. The
|
|
11
|
+
* {@link AgentManifest.capabilityIndex} therefore maps every capability to the
|
|
12
|
+
* (one-element) list of capabilities that satisfy it — trivial today, but the
|
|
13
|
+
* indirection keeps the lookup shape stable if a future definition ever
|
|
14
|
+
* advertises more than one capability.
|
|
15
|
+
*
|
|
16
|
+
* Models are stored as aliases ("opus"/"sonnet"/"haiku") rather than concrete
|
|
17
|
+
* ids; the runtime/provider bridge resolves an alias to a real model at spawn
|
|
18
|
+
* time, so the manifest never goes stale when model names change.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { ConfigError, NotFoundError } from "../errors.ts";
|
|
23
|
+
import type { AgentDefinition, AgentManifest, Capability } from "../types.ts";
|
|
24
|
+
|
|
25
|
+
/** Manifest schema version. Bump when the on-disk shape changes incompatibly. */
|
|
26
|
+
export const MANIFEST_VERSION = "1";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read-only tool set: inspect the repo without mutating it. Used by scout and
|
|
30
|
+
* reviewer, whose contract is "look, report, never change files".
|
|
31
|
+
*/
|
|
32
|
+
const READ_ONLY_TOOLS: readonly string[] = ["Read", "Glob", "Grep", "Bash"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Full write-capable tool set for roles that author code or resolve conflicts.
|
|
36
|
+
* Includes the read-only tools plus Edit/Write so a worker can both understand
|
|
37
|
+
* and modify the tree.
|
|
38
|
+
*/
|
|
39
|
+
const FULL_TOOLS: readonly string[] = ["Read", "Glob", "Grep", "Edit", "Write", "Bash"];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the default manifest shipped with a freshly initialized project.
|
|
43
|
+
*
|
|
44
|
+
* Ten capabilities are defined: the six orchestration-core roles plus the four
|
|
45
|
+
* delivery-pipeline roles (`architect`/`devops`/`deployer`/`verifier`). Only
|
|
46
|
+
* `lead` and `coordinator` may spawn children — every other role, including all
|
|
47
|
+
* four pipeline roles, is a leaf, which is what bounds delegation depth. A fresh
|
|
48
|
+
* array/object is allocated on each call so callers can mutate the result (e.g.
|
|
49
|
+
* before {@link writeManifest}) without aliasing module state.
|
|
50
|
+
*/
|
|
51
|
+
export function buildDefaultManifest(): AgentManifest {
|
|
52
|
+
// One definition per capability. Constraints are injected verbatim into the
|
|
53
|
+
// overlay, so they are phrased as direct instructions to the agent.
|
|
54
|
+
const definitions: Partial<Record<Capability, AgentDefinition>> = {
|
|
55
|
+
scout: {
|
|
56
|
+
file: "scout.md",
|
|
57
|
+
// Sonnet: exploration needs solid reasoning but not the priciest tier.
|
|
58
|
+
model: "sonnet",
|
|
59
|
+
tools: [...READ_ONLY_TOOLS],
|
|
60
|
+
capabilities: ["scout"],
|
|
61
|
+
canSpawn: false,
|
|
62
|
+
constraints: [
|
|
63
|
+
"Read-only: never edit, write, or delete files.",
|
|
64
|
+
"Produce a findings report; do not attempt to implement changes.",
|
|
65
|
+
"Stay within the assigned task scope; do not explore unrelated areas.",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
builder: {
|
|
69
|
+
file: "builder.md",
|
|
70
|
+
// Sonnet: the implementation workhorse — capable and cost-effective.
|
|
71
|
+
model: "sonnet",
|
|
72
|
+
tools: [...FULL_TOOLS],
|
|
73
|
+
capabilities: ["builder"],
|
|
74
|
+
canSpawn: false,
|
|
75
|
+
constraints: [
|
|
76
|
+
"Modify only files within your assigned FILE_SCOPE.",
|
|
77
|
+
"Run the project's quality gates before reporting done.",
|
|
78
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
reviewer: {
|
|
82
|
+
file: "reviewer.md",
|
|
83
|
+
// Haiku: review is high-volume pattern-matching; the cheap tier suffices.
|
|
84
|
+
model: "haiku",
|
|
85
|
+
tools: [...READ_ONLY_TOOLS],
|
|
86
|
+
capabilities: ["reviewer"],
|
|
87
|
+
canSpawn: false,
|
|
88
|
+
constraints: [
|
|
89
|
+
"Read-only: never edit, write, or delete files.",
|
|
90
|
+
"Report findings as pass/fail with specific, actionable feedback.",
|
|
91
|
+
"Do not implement fixes; flag issues for the builder to address.",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
lead: {
|
|
95
|
+
file: "lead.md",
|
|
96
|
+
// Opus: a lead plans and decomposes work, which rewards stronger reasoning.
|
|
97
|
+
model: "opus",
|
|
98
|
+
tools: [...FULL_TOOLS],
|
|
99
|
+
capabilities: ["lead"],
|
|
100
|
+
canSpawn: true,
|
|
101
|
+
constraints: [
|
|
102
|
+
"Decompose the task and delegate to scout/builder/reviewer workers.",
|
|
103
|
+
"Respect the configured maxAgentsPerLead and maxDepth limits.",
|
|
104
|
+
"Aggregate worker results before reporting completion upward.",
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
merger: {
|
|
108
|
+
file: "merger.md",
|
|
109
|
+
// Opus: conflict resolution is delicate and benefits from the top tier.
|
|
110
|
+
model: "opus",
|
|
111
|
+
tools: [...FULL_TOOLS],
|
|
112
|
+
capabilities: ["merger"],
|
|
113
|
+
canSpawn: false,
|
|
114
|
+
constraints: [
|
|
115
|
+
"Resolve conflicts by preserving the intent of every contributing branch.",
|
|
116
|
+
"Run quality gates after merging; never report a merge that breaks them.",
|
|
117
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
coordinator: {
|
|
121
|
+
file: "coordinator.md",
|
|
122
|
+
// Opus: the top of the tree owns the whole plan and orchestration loop.
|
|
123
|
+
model: "opus",
|
|
124
|
+
tools: [...FULL_TOOLS],
|
|
125
|
+
capabilities: ["coordinator"],
|
|
126
|
+
canSpawn: true,
|
|
127
|
+
constraints: [
|
|
128
|
+
"Spawn leads (and workers) to drive the project to completion.",
|
|
129
|
+
"Coordinate via the mail bus; track progress and unblock stalled agents.",
|
|
130
|
+
"Respect the configured maxConcurrent and maxDepth limits.",
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
architect: {
|
|
134
|
+
file: "architect.md",
|
|
135
|
+
// Opus: deployment planning is high-stakes recon that rewards top-tier reasoning.
|
|
136
|
+
model: "opus",
|
|
137
|
+
tools: [...READ_ONLY_TOOLS],
|
|
138
|
+
capabilities: ["architect"],
|
|
139
|
+
canSpawn: false,
|
|
140
|
+
constraints: [
|
|
141
|
+
"Read-only: never edit, write, or delete files; this is reconnaissance.",
|
|
142
|
+
"Emit a deploy-plan describing target, environments, gates, and required infra.",
|
|
143
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
devops: {
|
|
147
|
+
file: "devops.md",
|
|
148
|
+
// Sonnet: authoring infra config is solid mechanical work, not top-tier reasoning.
|
|
149
|
+
model: "sonnet",
|
|
150
|
+
tools: [...FULL_TOOLS],
|
|
151
|
+
capabilities: ["devops"],
|
|
152
|
+
canSpawn: false,
|
|
153
|
+
constraints: [
|
|
154
|
+
"Author only infrastructure files (CI/CD, manifests, Dockerfiles); touch nothing else.",
|
|
155
|
+
"Never apply or push changes to any environment; you author config only.",
|
|
156
|
+
"Never inline secrets; reference them by env-var binding instead.",
|
|
157
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
deployer: {
|
|
161
|
+
file: "deployer.md",
|
|
162
|
+
// Opus: executing a deploy is delicate and irreversible — use the top tier.
|
|
163
|
+
model: "opus",
|
|
164
|
+
tools: [...FULL_TOOLS],
|
|
165
|
+
capabilities: ["deployer"],
|
|
166
|
+
canSpawn: false,
|
|
167
|
+
constraints: [
|
|
168
|
+
"Deploy only when the environment's gate is satisfied; otherwise wait or escalate.",
|
|
169
|
+
"Never log, echo, or persist secrets; reference them by binding only.",
|
|
170
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
verifier: {
|
|
174
|
+
file: "verifier.md",
|
|
175
|
+
// Sonnet: probing a live deployment needs reliable reasoning at moderate cost.
|
|
176
|
+
model: "sonnet",
|
|
177
|
+
tools: [...READ_ONLY_TOOLS],
|
|
178
|
+
capabilities: ["verifier"],
|
|
179
|
+
canSpawn: false,
|
|
180
|
+
constraints: [
|
|
181
|
+
"Read-only: never edit, write, or delete files.",
|
|
182
|
+
"Actually probe the deployment (health checks, endpoints); never report a false green.",
|
|
183
|
+
"Report pass/fail with the concrete evidence that backs the verdict.",
|
|
184
|
+
"Do not spawn other agents; you are a leaf worker.",
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Each capability is provided by exactly the definition keyed under it, so the
|
|
190
|
+
// index maps every capability to a single-element list containing itself.
|
|
191
|
+
const capabilityIndex: Partial<Record<Capability, Capability[]>> = {};
|
|
192
|
+
const agents: Partial<Record<Capability, AgentDefinition>> = {};
|
|
193
|
+
for (const [cap, def] of Object.entries(definitions) as [Capability, AgentDefinition][]) {
|
|
194
|
+
agents[cap] = def;
|
|
195
|
+
for (const provided of def.capabilities) {
|
|
196
|
+
const existing = capabilityIndex[provided];
|
|
197
|
+
if (existing) {
|
|
198
|
+
existing.push(cap);
|
|
199
|
+
} else {
|
|
200
|
+
capabilityIndex[provided] = [cap];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { version: MANIFEST_VERSION, agents, capabilityIndex };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Write a manifest to `path` as pretty-printed JSON (2-space indent, trailing
|
|
210
|
+
* newline) so the file is diff-friendly and matches editor-on-save formatting.
|
|
211
|
+
*
|
|
212
|
+
* Uses a synchronous write (matching `setSecret` in secrets.ts) so the file is
|
|
213
|
+
* on disk before this function returns — a synchronous {@link loadManifest}
|
|
214
|
+
* immediately afterward is guaranteed to see it, with no Promise to await.
|
|
215
|
+
*/
|
|
216
|
+
export function writeManifest(path: string, manifest: AgentManifest): void {
|
|
217
|
+
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Load and parse a manifest from disk.
|
|
222
|
+
*
|
|
223
|
+
* Validates only the top-level shape (a non-empty `version` plus `agents` and
|
|
224
|
+
* `capabilityIndex` objects). Deep per-field validation of each definition is
|
|
225
|
+
* intentionally omitted: Agentplate keeps load lean and trusts {@link writeManifest}
|
|
226
|
+
* to emit well-formed files; a stricter validator, if ever needed, belongs here.
|
|
227
|
+
*
|
|
228
|
+
* @throws {NotFoundError} if no file exists at `path`.
|
|
229
|
+
* @throws {ConfigError} if the file is unreadable, not valid JSON, or not a
|
|
230
|
+
* well-formed manifest object.
|
|
231
|
+
*/
|
|
232
|
+
export function loadManifest(path: string): AgentManifest {
|
|
233
|
+
// Distinguish "missing" (a NotFoundError the caller can handle by writing a
|
|
234
|
+
// default) from "present but broken" (a ConfigError the caller must surface).
|
|
235
|
+
if (!existsSync(path)) {
|
|
236
|
+
throw new NotFoundError(`Agent manifest not found: ${path}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let text: string;
|
|
240
|
+
try {
|
|
241
|
+
text = readFileSync(path, "utf8");
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new ConfigError(`Failed to read agent manifest ${path}: ${(error as Error).message}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let parsed: unknown;
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(text);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new ConfigError(`Invalid JSON in agent manifest ${path}: ${(error as Error).message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
254
|
+
throw new ConfigError(`Agent manifest ${path} must be a JSON object`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const obj = parsed as Record<string, unknown>;
|
|
258
|
+
if (typeof obj.version !== "string" || obj.version.length === 0) {
|
|
259
|
+
throw new ConfigError(`Agent manifest ${path} is missing a non-empty "version"`);
|
|
260
|
+
}
|
|
261
|
+
if (obj.agents === null || typeof obj.agents !== "object" || Array.isArray(obj.agents)) {
|
|
262
|
+
throw new ConfigError(`Agent manifest ${path} is missing an "agents" object`);
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
obj.capabilityIndex === null ||
|
|
266
|
+
typeof obj.capabilityIndex !== "object" ||
|
|
267
|
+
Array.isArray(obj.capabilityIndex)
|
|
268
|
+
) {
|
|
269
|
+
throw new ConfigError(`Agent manifest ${path} is missing a "capabilityIndex" object`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return parsed as AgentManifest;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Look up the definition for a capability.
|
|
277
|
+
*
|
|
278
|
+
* @throws {NotFoundError} if the manifest has no definition for `capability`.
|
|
279
|
+
*/
|
|
280
|
+
export function getDefinition(manifest: AgentManifest, capability: Capability): AgentDefinition {
|
|
281
|
+
const def = manifest.agents[capability];
|
|
282
|
+
if (!def) {
|
|
283
|
+
throw new NotFoundError(`No agent definition for capability "${capability}" in manifest`);
|
|
284
|
+
}
|
|
285
|
+
return def;
|
|
286
|
+
}
|