@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -0
  4. package/agents/architect.md +108 -0
  5. package/agents/builder.md +97 -0
  6. package/agents/coordinator.md +113 -0
  7. package/agents/deployer.md +117 -0
  8. package/agents/devops.md +114 -0
  9. package/agents/lead.md +107 -0
  10. package/agents/merger.md +103 -0
  11. package/agents/reviewer.md +90 -0
  12. package/agents/scout.md +95 -0
  13. package/agents/verifier.md +106 -0
  14. package/package.json +64 -0
  15. package/src/agents/guard-rules.ts +55 -0
  16. package/src/agents/identity.test.ts +161 -0
  17. package/src/agents/identity.ts +229 -0
  18. package/src/agents/manifest.test.ts +260 -0
  19. package/src/agents/manifest.ts +286 -0
  20. package/src/agents/overlay.test.ts +190 -0
  21. package/src/agents/overlay.ts +212 -0
  22. package/src/agents/system-prompt.test.ts +53 -0
  23. package/src/agents/system-prompt.ts +95 -0
  24. package/src/agents/turn-runner.ts +79 -0
  25. package/src/commands/coordinator.test.ts +75 -0
  26. package/src/commands/coordinator.ts +259 -0
  27. package/src/commands/deploy.test.ts +504 -0
  28. package/src/commands/deploy.ts +874 -0
  29. package/src/commands/doctor.test.ts +106 -0
  30. package/src/commands/doctor.ts +208 -0
  31. package/src/commands/init.ts +71 -0
  32. package/src/commands/log.ts +51 -0
  33. package/src/commands/mail.ts +197 -0
  34. package/src/commands/merge.ts +127 -0
  35. package/src/commands/model.ts +58 -0
  36. package/src/commands/prime.ts +61 -0
  37. package/src/commands/reap.ts +87 -0
  38. package/src/commands/serve.ts +61 -0
  39. package/src/commands/setup.ts +48 -0
  40. package/src/commands/ship.test.ts +106 -0
  41. package/src/commands/ship.ts +202 -0
  42. package/src/commands/skill.test.ts +458 -0
  43. package/src/commands/skill.ts +730 -0
  44. package/src/commands/sling.ts +365 -0
  45. package/src/commands/status.ts +60 -0
  46. package/src/commands/stop.ts +56 -0
  47. package/src/commands/tui.ts +199 -0
  48. package/src/commands/worktree.ts +77 -0
  49. package/src/config.test.ts +92 -0
  50. package/src/config.ts +202 -0
  51. package/src/db/sqlite.test.ts +77 -0
  52. package/src/db/sqlite.ts +102 -0
  53. package/src/deploy/audit.test.ts +233 -0
  54. package/src/deploy/audit.ts +245 -0
  55. package/src/deploy/context.test.ts +243 -0
  56. package/src/deploy/context.ts +72 -0
  57. package/src/deploy/registry.test.ts +101 -0
  58. package/src/deploy/registry.ts +86 -0
  59. package/src/deploy/secrets.test.ts +129 -0
  60. package/src/deploy/secrets.ts +69 -0
  61. package/src/deploy/targets/docker-gha.test.ts +323 -0
  62. package/src/deploy/targets/docker-gha.ts +841 -0
  63. package/src/deploy/types.ts +153 -0
  64. package/src/errors.test.ts +42 -0
  65. package/src/errors.ts +69 -0
  66. package/src/events/store.test.ts +183 -0
  67. package/src/events/store.ts +201 -0
  68. package/src/index.ts +137 -0
  69. package/src/insights/quality-gates.ts +73 -0
  70. package/src/json.test.ts +28 -0
  71. package/src/json.ts +50 -0
  72. package/src/logging/color.ts +62 -0
  73. package/src/logging/logger.ts +60 -0
  74. package/src/logging/sanitizer.test.ts +36 -0
  75. package/src/logging/sanitizer.ts +57 -0
  76. package/src/mail/client.test.ts +192 -0
  77. package/src/mail/client.ts +188 -0
  78. package/src/mail/store.test.ts +279 -0
  79. package/src/mail/store.ts +311 -0
  80. package/src/merge/lock.test.ts +88 -0
  81. package/src/merge/lock.ts +84 -0
  82. package/src/merge/queue.test.ts +136 -0
  83. package/src/merge/queue.ts +177 -0
  84. package/src/merge/resolver.test.ts +219 -0
  85. package/src/merge/resolver.ts +274 -0
  86. package/src/paths.ts +36 -0
  87. package/src/providers/apply.test.ts +90 -0
  88. package/src/providers/apply.ts +66 -0
  89. package/src/providers/registry.test.ts +74 -0
  90. package/src/providers/registry.ts +254 -0
  91. package/src/runtimes/claude.ts +313 -0
  92. package/src/runtimes/codex.ts +280 -0
  93. package/src/runtimes/cursor.ts +247 -0
  94. package/src/runtimes/gemini.ts +173 -0
  95. package/src/runtimes/mock.ts +71 -0
  96. package/src/runtimes/opencode.ts +259 -0
  97. package/src/runtimes/registry.test.ts +924 -0
  98. package/src/runtimes/registry.ts +63 -0
  99. package/src/runtimes/resolve.ts +45 -0
  100. package/src/runtimes/types.ts +97 -0
  101. package/src/scaffold.ts +68 -0
  102. package/src/secrets.test.ts +51 -0
  103. package/src/secrets.ts +78 -0
  104. package/src/serve/api.ts +667 -0
  105. package/src/serve/server.test.ts +433 -0
  106. package/src/serve/server.ts +271 -0
  107. package/src/serve/system.ts +90 -0
  108. package/src/serve/weather.ts +140 -0
  109. package/src/sessions/reaper.test.ts +162 -0
  110. package/src/sessions/reaper.ts +149 -0
  111. package/src/sessions/store.test.ts +351 -0
  112. package/src/sessions/store.ts +350 -0
  113. package/src/skills/distiller.test.ts +498 -0
  114. package/src/skills/distiller.ts +426 -0
  115. package/src/skills/feedback.test.ts +300 -0
  116. package/src/skills/feedback.ts +168 -0
  117. package/src/skills/lifecycle.ts +169 -0
  118. package/src/skills/retrieval.test.ts +421 -0
  119. package/src/skills/retrieval.ts +365 -0
  120. package/src/skills/safety.test.ts +335 -0
  121. package/src/skills/safety.ts +216 -0
  122. package/src/skills/store.test.ts +425 -0
  123. package/src/skills/store.ts +684 -0
  124. package/src/skills/types.ts +107 -0
  125. package/src/types.ts +442 -0
  126. package/src/utils/detect.test.ts +35 -0
  127. package/src/utils/detect.ts +82 -0
  128. package/src/version.test.ts +19 -0
  129. package/src/version.ts +7 -0
  130. package/src/wizard/setup.ts +254 -0
  131. package/src/worktree/manager.test.ts +181 -0
  132. package/src/worktree/manager.ts +229 -0
  133. package/templates/overlay.md.tmpl +102 -0
  134. package/ui/dist/assets/index-C7rXIMER.css +1 -0
  135. package/ui/dist/assets/index-W4kbr4by.js +4526 -0
  136. package/ui/dist/favicon.svg +21 -0
  137. package/ui/dist/index.html +16 -0
  138. package/ui/dist/logo-clay.svg +21 -0
  139. package/ui/dist/logo.svg +18 -0
@@ -0,0 +1,75 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { DEFAULT_CONFIG, serializeConfig, setProjectRootOverride } from "../config.ts";
6
+ import { sessionsDbPath } from "../paths.ts";
7
+ import { createSessionStore } from "../sessions/store.ts";
8
+ import { createCoordinatorCommand } from "./coordinator.ts";
9
+
10
+ let root: string;
11
+
12
+ function initProject(runtime: string): void {
13
+ mkdirSync(join(root, ".agentplate"), { recursive: true });
14
+ const config = structuredClone(DEFAULT_CONFIG);
15
+ config.project.name = "coord-test";
16
+ config.project.root = root;
17
+ config.runtime.default = runtime;
18
+ writeFileSync(join(root, ".agentplate", "config.yaml"), serializeConfig(config), "utf8");
19
+ }
20
+
21
+ beforeEach(() => {
22
+ root = mkdtempSync(join(tmpdir(), "agentplate-coord-"));
23
+ setProjectRootOverride(root);
24
+ });
25
+
26
+ afterEach(() => {
27
+ setProjectRootOverride(null);
28
+ rmSync(root, { recursive: true, force: true });
29
+ delete process.env.AGENTPLATE_MOCK_INTERACTIVE;
30
+ });
31
+
32
+ async function runCoordinator(args: string[]): Promise<void> {
33
+ const program = createCoordinatorCommand();
34
+ program.exitOverride();
35
+ await program.parseAsync(["node", "coordinator", ...args]);
36
+ }
37
+
38
+ describe("coordinator start", () => {
39
+ test("--no-attach registers the run + coordinator session without spawning", async () => {
40
+ initProject("mock");
41
+ await runCoordinator(["start", "--no-attach"]);
42
+ const store = createSessionStore(sessionsDbPath(root));
43
+ try {
44
+ const session = store.getSessionByAgent("coordinator");
45
+ expect(session?.state).toBe("working");
46
+ expect(store.listRuns(1).length).toBe(1);
47
+ } finally {
48
+ store.close();
49
+ }
50
+ });
51
+
52
+ test("interactive (mock runtime) spawns then marks the coordinator stopped", async () => {
53
+ initProject("mock");
54
+ // Mock interactive session exits immediately (a no-op), so the spawn path
55
+ // runs end to end without forking a real claude.
56
+ process.env.AGENTPLATE_MOCK_INTERACTIVE = "true";
57
+ await runCoordinator(["start"]);
58
+ const store = createSessionStore(sessionsDbPath(root));
59
+ try {
60
+ const session = store.getSessionByAgent("coordinator");
61
+ // After the (instant) mock session exits, the coordinator is stopped.
62
+ expect(session?.state).toBe("stopped");
63
+ } finally {
64
+ store.close();
65
+ }
66
+ });
67
+
68
+ test("status reports the coordinator after start", async () => {
69
+ initProject("mock");
70
+ await runCoordinator(["start", "--no-attach"]);
71
+ // Should not throw; the session exists.
72
+ await runCoordinator(["status", "--json"]);
73
+ expect(true).toBe(true);
74
+ });
75
+ });
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `agentplate coordinator` — the top-level orchestration session.
3
+ *
4
+ * Basic core: `start` opens a run and registers a coordinator session that
5
+ * worker agents attach to (via `agentplate sling`, which inherits the current run).
6
+ * `send` queues a message for the coordinator, `status` shows its state, `stop`
7
+ * ends it. Driving a fully headless coordinator turn-loop (where the coordinator
8
+ * itself spawns leads via real AI) builds on this in a later phase.
9
+ */
10
+
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { Command } from "commander";
14
+ import { writeCoordinatorSystemPrompt } from "../agents/system-prompt.ts";
15
+ import { findProjectRoot, isInitialized, loadConfig } from "../config.ts";
16
+ import { NotFoundError, ValidationError } from "../errors.ts";
17
+ import { jsonOutput } from "../json.ts";
18
+ import {
19
+ brand,
20
+ muted,
21
+ printHint,
22
+ printInfo,
23
+ printSuccess,
24
+ printWarning,
25
+ } from "../logging/color.ts";
26
+ import { createMailClient } from "../mail/client.ts";
27
+ import { currentRunPath, sessionsDbPath } from "../paths.ts";
28
+ import { getRuntime } from "../runtimes/registry.ts";
29
+ import { resolveModel } from "../runtimes/resolve.ts";
30
+ import { createSessionStore } from "../sessions/store.ts";
31
+ import type { AgentSession } from "../types.ts";
32
+ import { resolveArgv } from "../utils/detect.ts";
33
+
34
+ const COORDINATOR_NAME = "coordinator";
35
+
36
+ function requireInit(): string {
37
+ const root = findProjectRoot();
38
+ if (!isInitialized(root)) {
39
+ throw new ValidationError("Not initialized. Run `agentplate setup` first.");
40
+ }
41
+ return root;
42
+ }
43
+
44
+ function startCommand(): Command {
45
+ return new Command("start")
46
+ .description("Open a run and launch the interactive coordinator chat")
47
+ .option("--label <text>", "label for the run")
48
+ .option("--runtime <name>", "runtime: claude | opencode | codex (default: config)")
49
+ .option("--no-attach", "register the run only; don't launch the chat")
50
+ .option("--print <message>", "seed the coordinator's first message")
51
+ .option(
52
+ "--safe",
53
+ "prompt for permission on each action (default: auto/bypass — runs unattended)",
54
+ )
55
+ .option("--json", "output JSON (implies --no-attach)")
56
+ .action(
57
+ async (
58
+ opts: {
59
+ label?: string;
60
+ runtime?: string;
61
+ attach?: boolean;
62
+ print?: string;
63
+ safe?: boolean;
64
+ json?: boolean;
65
+ },
66
+ command: Command,
67
+ ) => {
68
+ const useJson = command.optsWithGlobals().json === true;
69
+ const root = requireInit();
70
+ const config = loadConfig(root);
71
+ const store = createSessionStore(sessionsDbPath(root));
72
+
73
+ // 1. Register the run + coordinator session.
74
+ const run = store.createRun(opts.label);
75
+ writeFileSync(currentRunPath(root), `${run.id}\n`, "utf8");
76
+ const now = new Date().toISOString();
77
+ const session: AgentSession = {
78
+ id: `session-${COORDINATOR_NAME}-${Date.now()}`,
79
+ agentName: COORDINATOR_NAME,
80
+ capability: "coordinator",
81
+ taskId: "coordination",
82
+ runId: run.id,
83
+ worktreePath: root,
84
+ branchName: config.project.canonicalBranch,
85
+ state: "working",
86
+ parentAgent: null,
87
+ depth: 0,
88
+ pid: null,
89
+ runtimeSessionId: null,
90
+ startedAt: now,
91
+ lastActivity: now,
92
+ };
93
+ store.upsertSession(session);
94
+
95
+ // JSON / --no-attach: register only, mirror the prior behavior.
96
+ if (useJson || opts.attach === false) {
97
+ if (useJson) jsonOutput({ run, coordinator: session });
98
+ else {
99
+ printSuccess(`${brand("coordinator")} registered — run ${run.id}`);
100
+ printHint("Run `agentplate coordinator start` (without --no-attach) to chat.");
101
+ }
102
+ store.close();
103
+ return;
104
+ }
105
+
106
+ // 2. Resolve the runtime + model and build the interactive session.
107
+ const runtime = getRuntime(opts.runtime ?? config.runtime.default, config.runtime.default);
108
+ if (!runtime.buildInteractiveSpawn) {
109
+ store.close();
110
+ throw new ValidationError(
111
+ `Runtime "${runtime.id}" has no interactive mode. Use --no-attach, or set a runtime that supports interactive chat (e.g. claude).`,
112
+ );
113
+ }
114
+ // The coordinator reasons about the whole run, so use the strongest
115
+ // tier available; resolveModel falls back to the provider's configured
116
+ // model when this alias isn't overridden.
117
+ const resolved = resolveModel(config, root, "opus");
118
+ const { text: systemPrompt } = writeCoordinatorSystemPrompt(root, {
119
+ projectName: config.project.name,
120
+ runId: run.id,
121
+ agentName: COORDINATOR_NAME,
122
+ canonicalBranch: config.project.canonicalBranch,
123
+ instructionPath: runtime.instructionPath,
124
+ });
125
+ // Also write the role to the runtime's own instruction file (e.g.
126
+ // AGENTS.md for opencode/codex, .claude/CLAUDE.md for claude) so the
127
+ // coordinator is primed even on runtimes without a system-prompt flag.
128
+ try {
129
+ const instr = join(root, runtime.instructionPath);
130
+ mkdirSync(dirname(instr), { recursive: true });
131
+ writeFileSync(instr, systemPrompt, "utf8");
132
+ } catch {
133
+ // Non-fatal: the agent still works; --append-system-prompt covers
134
+ // runtimes that support it (claude).
135
+ }
136
+ // Default to auto/bypass mode so the coordinator runs unattended (no
137
+ // per-action permission prompts); `--safe` restores prompting.
138
+ const permissionMode = opts.safe ? "default" : "bypass";
139
+ const argv = runtime.buildInteractiveSpawn({
140
+ model: resolved.model,
141
+ systemPrompt,
142
+ permissionMode,
143
+ initialMessage: opts.print,
144
+ });
145
+
146
+ printSuccess(`${brand("coordinator")} started — run ${run.id}`);
147
+ if (permissionMode === "bypass") {
148
+ printWarning(
149
+ "auto mode: the coordinator runs WITHOUT permission prompts (bypassPermissions). Use --safe to prompt.",
150
+ );
151
+ }
152
+ printInfo(muted(` launching ${runtime.id} … (Ctrl+C to exit the chat)`));
153
+ store.close();
154
+
155
+ // 3. Hand the terminal to the interactive agent (inherited stdio).
156
+ // In auto/bypass mode, let the runtime add its bypass env (e.g. OpenCode's
157
+ // OPENCODE_PERMISSION) so the coordinator runs without prompts; under
158
+ // `--safe` keep just the resolved provider env so the runtime's own
159
+ // approval prompts stay in effect.
160
+ const interactiveEnv =
161
+ permissionMode === "bypass"
162
+ ? runtime.buildEnv({ model: resolved.model, env: resolved.env })
163
+ : { ...resolved.env };
164
+ const proc = Bun.spawn(resolveArgv(argv), {
165
+ cwd: root,
166
+ env: { ...process.env, ...interactiveEnv },
167
+ stdin: "inherit",
168
+ stdout: "inherit",
169
+ stderr: "inherit",
170
+ });
171
+ const exitCode = await proc.exited;
172
+
173
+ // 4. On exit, mark the coordinator stopped.
174
+ const closeStore = createSessionStore(sessionsDbPath(root));
175
+ try {
176
+ const current = closeStore.getSessionByAgent(COORDINATOR_NAME);
177
+ if (current) closeStore.updateSessionState(current.id, "stopped");
178
+ } finally {
179
+ closeStore.close();
180
+ }
181
+ if (exitCode !== 0) {
182
+ printWarning(`coordinator session exited with code ${exitCode}`);
183
+ }
184
+ },
185
+ );
186
+ }
187
+
188
+ function statusCommand(): Command {
189
+ return new Command("status")
190
+ .description("Show coordinator state")
191
+ .option("--json", "output JSON")
192
+ .action((_opts: { json?: boolean }, command: Command) => {
193
+ const useJson = command.optsWithGlobals().json === true;
194
+ const root = requireInit();
195
+ const store = createSessionStore(sessionsDbPath(root));
196
+ try {
197
+ const session = store.getSessionByAgent(COORDINATOR_NAME);
198
+ if (useJson) {
199
+ jsonOutput({ coordinator: session });
200
+ return;
201
+ }
202
+ if (!session) {
203
+ printInfo("No coordinator. Run `agentplate coordinator start`.");
204
+ return;
205
+ }
206
+ printInfo(`${brand("coordinator")} ${session.state} — run ${session.runId}`);
207
+ } finally {
208
+ store.close();
209
+ }
210
+ });
211
+ }
212
+
213
+ function sendCommand(): Command {
214
+ return new Command("send")
215
+ .description("Send a message to the coordinator")
216
+ .argument("<body>", "message body")
217
+ .requiredOption("--subject <text>", "subject")
218
+ .option("--from <name>", "sender", "operator")
219
+ .action((body: string, opts: { subject: string; from: string }) => {
220
+ const root = requireInit();
221
+ const mail = createMailClient(root);
222
+ try {
223
+ mail.send({
224
+ from: opts.from,
225
+ to: COORDINATOR_NAME,
226
+ subject: opts.subject,
227
+ body,
228
+ type: "status",
229
+ });
230
+ printSuccess("Message queued for coordinator.");
231
+ } finally {
232
+ mail.close();
233
+ }
234
+ });
235
+ }
236
+
237
+ function stopCommand(): Command {
238
+ return new Command("stop").description("Stop the coordinator session").action(() => {
239
+ const root = requireInit();
240
+ const store = createSessionStore(sessionsDbPath(root));
241
+ try {
242
+ const session = store.getSessionByAgent(COORDINATOR_NAME);
243
+ if (!session) throw new NotFoundError("No coordinator session to stop.");
244
+ store.updateSessionState(session.id, "stopped");
245
+ printSuccess("Coordinator stopped.");
246
+ } finally {
247
+ store.close();
248
+ }
249
+ });
250
+ }
251
+
252
+ export function createCoordinatorCommand(): Command {
253
+ return new Command("coordinator")
254
+ .description("Top-level orchestration session")
255
+ .addCommand(startCommand())
256
+ .addCommand(statusCommand())
257
+ .addCommand(sendCommand())
258
+ .addCommand(stopCommand());
259
+ }