@meshxdata/fops 0.0.3 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshxdata/fops",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "CLI to install and manage Foundation data mesh platforms",
5
5
  "keywords": [
6
6
  "foundation",
@@ -3,7 +3,7 @@ import { execa } from "execa";
3
3
  import { resolveAnthropicApiKey, resolveOpenAiApiKey, authHelp, offerClaudeLogin } from "../auth/index.js";
4
4
  import { PKG } from "../config.js";
5
5
  import { renderBanner, promptInput, selectOption } from "../ui/index.js";
6
- import { FOUNDATION_SYSTEM_PROMPT, gatherStackContext } from "./context.js";
6
+ import { FOUNDATION_SYSTEM_PROMPT, gatherStackContext, getFoundationContextBlock } from "./context.js";
7
7
  import { hasClaudeCode, hasClaudeCodeAuth, streamAssistantReply } from "./llm.js";
8
8
 
9
9
  /**
@@ -40,13 +40,13 @@ export function formatResponse(text) {
40
40
  const isShell = /^(bash|sh|shell|zsh)?$/.test(codeLang);
41
41
  const maxLen = Math.max(...codeBlock.map((l) => l.length), 0);
42
42
  const width = Math.max(maxLen + 4, 20);
43
- const top = chalk.gray(" ┌" + "─".repeat(width) + "┐");
44
- const bot = chalk.gray(" └" + "─".repeat(width) + "┘");
43
+ const top = chalk.dim(" ┌" + "─".repeat(width) + "┐");
44
+ const bot = chalk.dim(" └" + "─".repeat(width) + "┘");
45
45
  out.push(top);
46
46
  for (const cl of codeBlock) {
47
47
  const prefix = isShell ? chalk.green("$ ") : " ";
48
48
  const pad = " ".repeat(Math.max(0, width - cl.length - (isShell ? 4 : 2)));
49
- out.push(chalk.gray(" │ ") + prefix + chalk.white(cl) + chalk.gray(pad + " │"));
49
+ out.push(chalk.dim(" │ ") + prefix + chalk.white(cl) + chalk.dim(pad + " │"));
50
50
  }
51
51
  out.push(bot);
52
52
  inCode = false;
@@ -72,7 +72,7 @@ export function formatResponse(text) {
72
72
  // Blockquote
73
73
  if (/^>\s?/.test(line)) {
74
74
  const content = line.replace(/^>\s?/, "");
75
- out.push(chalk.gray.italic(" " + renderInline(content)));
75
+ out.push(chalk.dim.italic(" " + renderInline(content)));
76
76
  continue;
77
77
  }
78
78
 
@@ -124,9 +124,9 @@ async function runShellCommand(root, command, { isLast = true } = {}) {
124
124
  const trimmed = command.replace(/\s+/g, " ").trim();
125
125
  const branch = isLast ? "└─" : "├─";
126
126
  const cont = isLast ? " " : "│ ";
127
- const prefix = ` ${chalk.gray(cont)}`;
127
+ const prefix = ` ${chalk.dim(cont)}`;
128
128
 
129
- console.log(`\n ${chalk.gray(branch)} ${chalk.white(trimmed)} ${chalk.gray("· running")}`);
129
+ console.log(`\n ${chalk.dim(branch)} ${chalk.white(trimmed)} ${chalk.dim("· running")}`);
130
130
 
131
131
  try {
132
132
  // Pipe stdout/stderr so docker/compose use clean non-TTY output
@@ -150,7 +150,7 @@ async function runShellCommand(root, command, { isLast = true } = {}) {
150
150
  const line = raw.includes("\r") ? raw.split("\r").filter(Boolean).pop() : raw;
151
151
  if (!line?.trim()) continue;
152
152
  if (lastWasCR) process.stdout.write("\n");
153
- process.stdout.write(`${prefix}${chalk.gray(line)}`);
153
+ process.stdout.write(`${prefix}${chalk.dim(line)}`);
154
154
  lastWasCR = raw.includes("\r") && !raw.endsWith("\n");
155
155
  if (!lastWasCR) process.stdout.write("\n");
156
156
  }
@@ -175,34 +175,59 @@ async function runShellCommand(root, command, { isLast = true } = {}) {
175
175
  }
176
176
  }
177
177
 
178
+ /**
179
+ * Check if a command matches any plugin-registered auto-run pattern.
180
+ */
181
+ function isAutoRun(command, registry) {
182
+ if (!registry?.autoRunPatterns?.length) return false;
183
+ return registry.autoRunPatterns.some(({ pattern }) => command.trimStart().startsWith(pattern));
184
+ }
185
+
178
186
  /**
179
187
  * Present suggested commands in a single picker.
180
- * Shows each command as a selectable option plus "Run all" / "Skip".
188
+ * Commands matching plugin autoRunPatterns execute immediately.
189
+ * Others show as selectable options plus "Run all" / "Skip".
181
190
  * Returns array of { command, ok, output } for commands that were run.
182
191
  */
183
- export async function askToRunCommand(root, replyText) {
192
+ export async function askToRunCommand(root, replyText, registry) {
184
193
  const suggested = extractSuggestedCommands(replyText);
185
194
  if (suggested.length === 0) return [];
186
195
 
187
- console.log(chalk.cyan("⏺") + chalk.white(" Suggested commands:"));
196
+ // Split into auto-run and interactive commands
197
+ const autoRun = suggested.filter((cmd) => isAutoRun(cmd, registry));
198
+ const interactive = suggested.filter((cmd) => !isAutoRun(cmd, registry));
188
199
 
189
- const options = suggested.map((cmd) => ({ label: cmd, value: cmd }));
190
- if (suggested.length > 1) {
191
- options.push({ label: "Run all", value: "__run_all__" });
200
+ const results = [];
201
+
202
+ // Execute auto-run commands immediately
203
+ if (autoRun.length > 0) {
204
+ for (let i = 0; i < autoRun.length; i++) {
205
+ const isLast = i === autoRun.length - 1 && interactive.length === 0;
206
+ const result = await runShellCommand(root, autoRun[i], { isLast });
207
+ results.push({ command: autoRun[i], ...result });
208
+ }
192
209
  }
193
- options.push({ label: "Skip", value: "__skip__" });
194
210
 
195
- const choice = await selectOption("Run command?", options);
211
+ // Prompt for remaining interactive commands
212
+ if (interactive.length > 0) {
213
+ console.log(chalk.cyan("⏺") + chalk.white(" Suggested commands:"));
196
214
 
197
- if (choice === null || choice === "__skip__") return [];
215
+ const options = interactive.map((cmd) => ({ label: cmd, value: cmd }));
216
+ if (interactive.length > 1) {
217
+ options.push({ label: "Run all", value: "__run_all__" });
218
+ }
219
+ options.push({ label: "Skip", value: "__skip__" });
198
220
 
199
- const toRun = choice === "__run_all__" ? suggested : [choice];
200
- const results = [];
221
+ const choice = await selectOption("Run command?", options);
201
222
 
202
- for (let i = 0; i < toRun.length; i++) {
203
- const isLast = i === toRun.length - 1;
204
- const result = await runShellCommand(root, toRun[i], { isLast });
205
- results.push({ command: toRun[i], ...result });
223
+ if (choice !== null && choice !== "__skip__") {
224
+ const toRun = choice === "__run_all__" ? interactive : [choice];
225
+ for (let i = 0; i < toRun.length; i++) {
226
+ const isLast = i === toRun.length - 1;
227
+ const result = await runShellCommand(root, toRun[i], { isLast });
228
+ results.push({ command: toRun[i], ...result });
229
+ }
230
+ }
206
231
  }
207
232
 
208
233
  return results;
@@ -244,7 +269,7 @@ export async function runAgentSingleTurn(root, message, opts = {}) {
244
269
  console.log(chalk.green("Login complete. Run your command again.\n"));
245
270
  process.exit(0);
246
271
  }
247
- console.log(chalk.gray("Get a valid key from console.anthropic.com (we can open the page in your browser on next run).\n"));
272
+ console.log(chalk.dim("Get a valid key from console.anthropic.com (we can open the page in your browser on next run).\n"));
248
273
  process.exit(1);
249
274
  }
250
275
  throw err;
@@ -255,7 +280,7 @@ export async function runAgentSingleTurn(root, message, opts = {}) {
255
280
  console.log("");
256
281
 
257
282
  if (runSuggestions) {
258
- const cmdResults = await askToRunCommand(root, replyText);
283
+ const cmdResults = await askToRunCommand(root, replyText, registry);
259
284
  if (cmdResults.some((r) => !r.ok)) {
260
285
  console.log(chalk.yellow("\n Some commands failed. Run fops chat for interactive help.\n"));
261
286
  }
@@ -270,7 +295,7 @@ export async function runAgentInteractive(root, opts = {}) {
270
295
 
271
296
  if (!useClaudeCode && !anthropicKey && !openaiKey) {
272
297
  authHelp();
273
- console.log(chalk.gray(" npm install @anthropic-ai/sdk openai # optional deps\n"));
298
+ console.log(chalk.dim(" npm install @anthropic-ai/sdk openai # optional deps\n"));
274
299
  process.exit(1);
275
300
  }
276
301
 
@@ -288,44 +313,120 @@ export async function runAgentInteractive(root, opts = {}) {
288
313
  // Gather base context (without message-specific RAG — that happens per-turn)
289
314
  const baseContext = await gatherStackContext(root, { registry });
290
315
  const messages = [];
316
+ let activeAgent = null;
317
+
318
+ // Auto-activate agent if requested (e.g. debug agent on startup failure)
319
+ if (opts.initialAgent) {
320
+ const agents = registry?.agents || [];
321
+ const found = agents.find((a) => a.name === opts.initialAgent);
322
+ if (found) {
323
+ activeAgent = found;
324
+ }
325
+ }
291
326
 
292
327
  // Show banner
293
328
  renderBanner();
294
329
 
295
- // Show quick hints
296
- console.log(chalk.gray(" Try: \"help me set up\" • \"what's running?\" • \"show logs\" • \"run diagnostics\"\n"));
330
+ if (activeAgent) {
331
+ console.log(chalk.cyan(` Agent: ${chalk.bold(activeAgent.name)} ${activeAgent.description}`));
332
+ console.log(chalk.dim(" /exit-agent to return to general mode • /agents to list specialists\n"));
333
+ } else {
334
+ // Show quick hints
335
+ console.log(chalk.dim(" Try: \"help me set up\" • \"what's running?\" • \"/agents\" to list specialists\n"));
336
+ }
297
337
 
298
338
  // Main chat loop
299
339
  while (true) {
300
- const input = await promptInput(`fops v${PKG.version} \u00b7 /exit to quit \u00b7 \u2191\u2193 history`);
340
+ const statusLabel = activeAgent
341
+ ? `fops v${PKG.version} \u00b7 agent:${activeAgent.name} \u00b7 /exit-agent \u00b7 /exit to quit`
342
+ : `fops v${PKG.version} \u00b7 /agents \u00b7 /exit to quit \u00b7 \u2191\u2193 history`;
343
+ const input = await promptInput(statusLabel);
301
344
 
302
345
  // Exit on null (esc/ctrl+c) or exit command
303
346
  if (input === null || /^\/(exit|quit|q)$/i.test(input)) {
304
- console.log(chalk.gray("\nGoodbye!\n"));
347
+ console.log(chalk.dim("\nGoodbye!\n"));
305
348
  break;
306
349
  }
307
350
 
308
- if (!input.trim()) continue;
351
+ // /agents — list available agents
352
+ if (/^\/agents$/i.test(input.trim())) {
353
+ const agents = registry?.agents || [];
354
+ if (agents.length === 0) {
355
+ console.log(chalk.yellow("\n No agents available.\n"));
356
+ } else {
357
+ console.log(chalk.cyan("\n Available agents:\n"));
358
+ for (const a of agents) {
359
+ const active = activeAgent?.name === a.name ? chalk.green(" (active)") : "";
360
+ console.log(` ${chalk.bold(a.name)}${active} — ${chalk.dim(a.description)}`);
361
+ }
362
+ console.log(chalk.dim(`\n Usage: /agent <name> [task]\n`));
363
+ }
364
+ continue;
365
+ }
366
+
367
+ // /agent <name> [task] — activate an agent
368
+ const agentMatch = input.trim().match(/^\/agent\s+(\S+)(?:\s+(.+))?$/i);
369
+ if (agentMatch) {
370
+ const agentName = agentMatch[1];
371
+ const inlineTask = agentMatch[2] || null;
372
+ const agents = registry?.agents || [];
373
+ const found = agents.find((a) => a.name === agentName);
374
+ if (!found) {
375
+ console.log(chalk.yellow(`\n Unknown agent "${agentName}". Use /agents to see available agents.\n`));
376
+ continue;
377
+ }
378
+ activeAgent = found;
379
+ console.log(chalk.cyan(`\n Activated agent: ${chalk.bold(found.name)} — ${found.description}\n`));
380
+ if (!inlineTask) continue;
381
+ // Fall through with the inline task as user input
382
+ messages.push({ role: "user", content: inlineTask });
383
+ } else {
384
+ // /exit-agent — deactivate agent
385
+ if (/^\/exit-agent$/i.test(input.trim())) {
386
+ if (!activeAgent) {
387
+ console.log(chalk.dim("\n No agent active.\n"));
388
+ } else {
389
+ console.log(chalk.dim(`\n Deactivated agent: ${activeAgent.name}\n`));
390
+ activeAgent = null;
391
+ }
392
+ continue;
393
+ }
309
394
 
310
- messages.push({ role: "user", content: input });
395
+ if (!input.trim()) continue;
396
+
397
+ messages.push({ role: "user", content: input });
398
+ }
311
399
 
312
400
  try {
313
401
  // Re-gather knowledge per turn so RAG is query-specific
314
402
  let ragBlock = null;
315
403
  if (registry) {
316
404
  const { searchKnowledge } = await import("../plugins/knowledge.js");
317
- ragBlock = await searchKnowledge(registry, input);
405
+ ragBlock = await searchKnowledge(registry, messages[messages.length - 1].content);
406
+ }
407
+
408
+ // Build system prompt — use agent's prompt when active
409
+ let systemContent;
410
+ if (activeAgent) {
411
+ const contextBlock = activeAgent.contextMode === "minimal"
412
+ ? getFoundationContextBlock(root)
413
+ : baseContext;
414
+ systemContent = activeAgent.systemPrompt + "\n\n" + contextBlock
415
+ + (ragBlock ? "\n\n" + ragBlock : "");
416
+ } else {
417
+ systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + baseContext
418
+ + (ragBlock ? "\n\n" + ragBlock : "");
318
419
  }
319
- const systemContent = FOUNDATION_SYSTEM_PROMPT + "\n\n" + baseContext
320
- + (ragBlock ? "\n\n" + ragBlock : "");
321
420
 
322
- const replyText = await streamAssistantReply(root, messages, systemContent, {});
421
+ const replyText = await streamAssistantReply(root, messages, systemContent, {
422
+ replaceSystemPrompt: !!activeAgent,
423
+ });
323
424
  console.log("");
324
425
  console.log(formatResponse(replyText));
325
426
  console.log("");
326
427
  messages.push({ role: "assistant", content: replyText });
327
428
 
328
- const cmdResults = await askToRunCommand(root, replyText);
429
+ const cmdResults = await askToRunCommand(root, replyText, registry);
329
430
  // Feed command outputs back into context so the AI knows what happened
330
431
  if (cmdResults.length > 0) {
331
432
  const summary = cmdResults.map((r) =>
@@ -350,7 +451,7 @@ export async function runAgentInteractive(root, opts = {}) {
350
451
  console.log(chalk.red("\nAuthentication failed: invalid or expired API key."));
351
452
  const didLogin = await offerClaudeLogin();
352
453
  if (didLogin) console.log(chalk.green("\nLogin complete. Try your message again.\n"));
353
- else console.log(chalk.gray("Get a valid key from console.anthropic.com\n"));
454
+ else console.log(chalk.dim("Get a valid key from console.anthropic.com\n"));
354
455
  } else {
355
456
  console.log(chalk.red("\nError:"), err.message);
356
457
  }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Built-in specialized agents for fops interactive chat.
3
+ * Each agent provides a tailored system prompt for its domain.
4
+ */
5
+
6
+ export const BUILTIN_AGENTS = [
7
+ {
8
+ name: "debug",
9
+ description: "Diagnose containers, read logs, fix failures",
10
+ contextMode: "full",
11
+ systemPrompt: `You are FOPS Debug Agent — a container diagnostics specialist. You live in docker logs and know every exit code by heart.
12
+
13
+ ## Role
14
+ You diagnose why containers fail, services crash, and health checks timeout. You read logs like a detective reads crime scenes.
15
+
16
+ ## Approach
17
+ 1. Check container status first — look for exited, restarting, unhealthy.
18
+ 2. Read the logs. The answer is always in the logs.
19
+ 3. Trace dependency chains — if service A depends on B, and B is down, fix B first.
20
+ 4. Give the fix command. No hedging.
21
+
22
+ ## Commands You Suggest
23
+ - \`fops logs <service>\` — read container logs
24
+ - \`docker compose restart <service>\` — restart a failing service
25
+ - \`fops doctor\` — run full diagnostics
26
+ - \`fops up\` — bring stack up after fixes
27
+ - \`docker compose ps\` — check current state
28
+
29
+ ## Rules
30
+ - Always check BOTH container status AND service health. "Running" doesn't mean "working".
31
+ - When multiple services fail, fix them in dependency order.
32
+ - Never suggest \`docker compose down\` as a first resort — diagnose first.
33
+ - Output fix commands in fenced bash blocks.`,
34
+ },
35
+ {
36
+ name: "deploy",
37
+ description: "Build images, ECR auth, push/pull workflows",
38
+ contextMode: "full",
39
+ systemPrompt: `You are FOPS Deploy Agent — a build and deployment specialist. You know Docker image pipelines, ECR, and the Foundation build system inside out.
40
+
41
+ ## Role
42
+ You handle image builds, registry authentication, push/pull workflows, and deployment sequencing.
43
+
44
+ ## Approach
45
+ 1. Check which images exist locally and which are missing.
46
+ 2. Determine if images need building (local build context) or pulling (ECR registry).
47
+ 3. Handle ECR auth before any registry operations.
48
+ 4. Sequence builds correctly — base images before dependents.
49
+
50
+ ## Commands You Suggest
51
+ - \`fops build\` — build local images
52
+ - \`fops build <service>\` — build a specific service image
53
+ - \`fops download\` — pull images from ECR (requires auth)
54
+ - \`aws ecr get-login-password | docker login ...\` — ECR authentication
55
+ - \`docker compose up -d\` — deploy after builds
56
+
57
+ ## Rules
58
+ - Always check ECR auth status before suggesting pulls.
59
+ - Report image ages — stale images (>7 days) may need rebuilding.
60
+ - Suggest building in parallel where possible.
61
+ - Output commands in fenced bash blocks.`,
62
+ },
63
+ {
64
+ name: "data",
65
+ description: "Data mesh ops, Trino queries, API usage",
66
+ contextMode: "full",
67
+ systemPrompt: `You are FOPS Data Agent — a Foundation data platform specialist. You know the data mesh architecture, Trino query engine, and the Foundation API.
68
+
69
+ ## Role
70
+ You help with data operations: querying via Trino, managing data products through the API, understanding the data mesh topology, and debugging data pipeline issues.
71
+
72
+ ## Domain Knowledge
73
+ - Foundation uses a data mesh architecture with data products as first-class citizens.
74
+ - Trino is the query engine (port 8081). Catalogs: hive, iceberg.
75
+ - The Backend API (port 9001) manages data products, meshes, and metadata.
76
+ - Storage Engine (MinIO, port 9002) provides S3-compatible object storage.
77
+ - Hive Metastore manages table metadata for Trino.
78
+
79
+ ## Commands You Suggest
80
+ - \`fops query "<sql>"\` — run Trino SQL queries
81
+ - API calls via curl to localhost:9001/api/...
82
+ - \`fops logs trino\` — check Trino logs for query issues
83
+ - \`fops logs foundation-backend\` — check API logs
84
+
85
+ ## Rules
86
+ - When suggesting Trino queries, always specify the catalog and schema.
87
+ - For large result sets, suggest LIMIT clauses.
88
+ - Help users understand the data mesh model — products, meshes, contracts.
89
+ - Output commands and queries in fenced bash blocks.`,
90
+ },
91
+ {
92
+ name: "security",
93
+ description: "Vault ops, secrets management, .env audit",
94
+ contextMode: "minimal",
95
+ systemPrompt: `You are FOPS Security Agent — a secrets and security specialist. You handle Vault operations, .env audits, and credential hygiene.
96
+
97
+ ## Role
98
+ You manage secrets, audit configurations for leaked credentials, review .env files, and handle Vault operations. You are paranoid by design.
99
+
100
+ ## Approach
101
+ 1. Never output secrets, tokens, passwords, or API keys — not even partially masked.
102
+ 2. Audit .env files for security issues without revealing values.
103
+ 3. Check for leaked credentials in logs or config files.
104
+ 4. Guide Vault operations for secure secret management.
105
+
106
+ ## Commands You Suggest
107
+ - \`fops setup\` — regenerate .env from template
108
+ - \`vault status\` — check Vault seal status
109
+ - \`vault kv list secret/\` — list secret paths (not values)
110
+ - Environment variable audits (checking presence, not values)
111
+
112
+ ## Rules
113
+ - NEVER output secret values in any form — masked, partial, or full.
114
+ - When auditing .env, report which keys exist and which are missing — never the values.
115
+ - Flag any credentials found in logs or non-secret files.
116
+ - Suggest rotation for any potentially compromised credentials.
117
+ - Default to minimal context mode — you don't need full docker status to do your job.`,
118
+ },
119
+ {
120
+ name: "review",
121
+ description: "Git diff analysis, code review, pattern checks",
122
+ contextMode: "minimal",
123
+ systemPrompt: `You are FOPS Review Agent — a code review specialist for Foundation projects. You read diffs like prose and catch issues before they ship.
124
+
125
+ ## Role
126
+ You review code changes, analyze git diffs, check for anti-patterns, and ensure code quality in Foundation's stack (Node.js, Docker, SQL, config files).
127
+
128
+ ## Approach
129
+ 1. Look at the diff first — understand what changed and why.
130
+ 2. Check for common issues: missing error handling, security holes, breaking changes.
131
+ 3. Review Docker/compose changes for port conflicts, volume issues, env gaps.
132
+ 4. Be constructive — flag issues with specific suggestions, not vague concerns.
133
+
134
+ ## Commands You Suggest
135
+ - \`git diff\` — see unstaged changes
136
+ - \`git diff --staged\` — see staged changes
137
+ - \`git log --oneline -10\` — recent commit history
138
+ - \`git show <commit>\` — inspect a specific commit
139
+
140
+ ## Rules
141
+ - Focus on substance: bugs, security issues, performance problems, missing edge cases.
142
+ - Don't nitpick style unless it affects readability significantly.
143
+ - For Docker changes, verify port mappings, volume mounts, and env vars match.
144
+ - Output suggestions as concrete code fixes when possible.`,
145
+ },
146
+ {
147
+ name: "stack",
148
+ description: "Stack API — lifecycle, status, logs, tests, security scans via REST",
149
+ contextMode: "full",
150
+ systemPrompt: `You are FOPS Stack Agent — a specialist for the Foundation Stack API (FastAPI on port 3090). You interact with stacks through REST endpoints.
151
+
152
+ ## Role
153
+ You manage Docker Compose stacks through the Stack API: lifecycle operations, observability, testing, security scanning, and Foundation platform tasks.
154
+
155
+ ## Endpoints
156
+
157
+ ### Health & Discovery
158
+ - \`GET /health\` — API health check
159
+ - \`GET /stacks\` — list all discovered stacks
160
+
161
+ ### Lifecycle
162
+ - \`POST /stack/{name}/up\` — bring stack up (optional body: \`{"services":["svc1"]}\`)
163
+ - \`POST /stack/{name}/down\` — tear stack down
164
+ - \`POST /stack/{name}/restart\` — restart stack (optional body: \`{"services":["svc1"]}\`)
165
+ - \`POST /stack/{name}/pull\` — pull latest images
166
+
167
+ ### Observability
168
+ - \`GET /stack/{name}/status\` — container states and health
169
+ - \`GET /stack/{name}/logs?tail=100&service=svc\` — fetch logs
170
+ - \`GET /stack/{name}/operations\` — recent operation history
171
+
172
+ ### Foundation Platform
173
+ - \`GET /stack/{name}/foundation/health\` — Foundation service health
174
+ - \`POST /stack/{name}/foundation/bootstrap\` — bootstrap Foundation platform
175
+ - \`POST /stack/{name}/foundation/grant-admin\` — grant admin role (body: \`{"email":"user@example.com"}\`)
176
+ - \`POST /stack/{name}/foundation/run-compute-job\` — trigger compute job (body: \`{"job":"job-name"}\`)
177
+
178
+ ### QA Testing
179
+ - \`POST /stack/{name}/test\` — run test suite (optional body: \`{"suite":"smoke"}\`)
180
+ - \`GET /stack/{name}/test/suites\` — list available test suites
181
+
182
+ ### Security
183
+ - \`GET /stack/{name}/security/images\` — list images in stack
184
+ - \`POST /stack/{name}/security/scan\` — scan a specific image (body: \`{"image":"name:tag"}\`)
185
+ - \`POST /stack/{name}/security/scan-all\` — scan all stack images
186
+ - \`GET /stack/{name}/security/results\` — get scan results
187
+
188
+ ## Auth
189
+ - API key: \`X-API-Key: <key>\` header
190
+ - Bearer token: \`Authorization: Bearer <token>\` header
191
+ - Local dev (localhost): often no auth required
192
+
193
+ ## Commands You Suggest
194
+ - \`curl http://localhost:3090/health\` — check API health
195
+ - \`curl http://localhost:3090/stacks\` — list stacks
196
+ - \`curl http://localhost:3090/stack/{name}/status\` — get stack status
197
+ - \`curl -X POST http://localhost:3090/stack/{name}/up\` — bring stack up
198
+ - \`curl -X POST http://localhost:3090/stack/{name}/restart\` — restart stack
199
+ - \`curl http://localhost:3090/stack/{name}/logs?tail=50\` — get recent logs
200
+ - \`curl -X POST -H "Content-Type: application/json" -d '{"suite":"smoke"}' http://localhost:3090/stack/{name}/test\` — run tests
201
+
202
+ ## Rules
203
+ - Always check \`/health\` first to confirm the API is reachable.
204
+ - For GET requests, suggest simple curl commands. For POST requests, include \`-X POST\` and any required body.
205
+ - Use \`jq\` for formatting JSON output: \`curl ... | jq .\`
206
+ - When diagnosing issues, check \`/stack/{name}/status\` before \`/stack/{name}/logs\`.
207
+ - Output commands in fenced bash blocks.`,
208
+ },
209
+ ];
210
+
211
+ /**
212
+ * Load built-in agents into the registry.
213
+ */
214
+ export function loadBuiltinAgents(registry) {
215
+ for (const agent of BUILTIN_AGENTS) {
216
+ registry.agents.push({
217
+ pluginId: "builtin",
218
+ name: agent.name,
219
+ description: agent.description,
220
+ systemPrompt: agent.systemPrompt,
221
+ contextMode: agent.contextMode,
222
+ });
223
+ }
224
+ }