aegiscode 6.1.0 → 6.2.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.
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Real enforcement for the /permissions rules (src/config.js) in the
5
+ * direct-provider tool loop. The `claude` CLI path already has its own real
6
+ * approval UX from the real binary, so this only applies to
7
+ * Bash/Read/Write/Edit/Glob/Grep run by a provider model.
8
+ *
9
+ * Scope decision (Phase 12): the multi-directory-change Bash heuristic,
10
+ * explicit /permissions allow|deny|ask rules, and `defaultMode: 'ask'` when
11
+ * the permissions file *explicitly* writes it (rules.explicitAsk) are
12
+ * enforced here. The file-free default ('ask' in DEFAULT_PERMISSIONS) is
13
+ * NOT consulted — honoring the implicit default would prompt on every tool
14
+ * call out of the box and make the agentic loop unusable by default.
15
+ * /confirm and /yolo still toggle defaultMode for the explicit case.
16
+ *
17
+ * Not tools.js's Glob-tool globToRegex: that one is filesystem-glob (`*`
18
+ * stops at `/`, matching one path segment) because it walks a directory
19
+ * tree. Rule subjects here are just as often a Bash command string, where
20
+ * `/` is ordinary text ("rm -rf /tmp/x") — a `Bash(npm run *)` rule (see
21
+ * DEFAULT_PERMISSIONS's own example) needs `*` to match the rest of the
22
+ * line, slashes included. So `*` and `?` here are plain "match anything"
23
+ * wildcards, not path-segment-bounded ones; reimplemented rather than
24
+ * imported so this module has no import-cycle with tools.js (executeTool's
25
+ * home, which calls into this one).
26
+ *
27
+ * Ported from aegiscodex-dev/src/permissions.js (ESM → CommonJS).
28
+ */
29
+
30
+ const RULE_RE = /^(\w+)(?:\((.*)\))?$/;
31
+
32
+ function globToRegex(pattern) {
33
+ let re = '^';
34
+ for (const c of pattern) {
35
+ if (c === '*') re += '.*';
36
+ else if (c === '?') re += '.';
37
+ else if (/[.+^${}()|[\]\\]/.test(c)) re += `\\${c}`;
38
+ else re += c;
39
+ }
40
+ return new RegExp(re + '$');
41
+ }
42
+
43
+ /** The subject a rule's pattern matches against, per tool. */
44
+ function subjectFor(toolName, args = {}) {
45
+ if (toolName === 'Bash') return String(args.command || '');
46
+ if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') return String(args.file_path || '');
47
+ if (toolName === 'Glob' || toolName === 'Grep') return String(args.pattern || '');
48
+ return '';
49
+ }
50
+
51
+ function matchRule(rule, toolName, subject) {
52
+ const m = RULE_RE.exec(String(rule || '').trim());
53
+ if (!m) return false;
54
+ const [, tool, pattern] = m;
55
+ if (tool !== toolName) return false;
56
+ if (pattern === undefined) return true; // bare "Bash" matches every call
57
+ try { return globToRegex(pattern).test(subject); } catch { return false; }
58
+ }
59
+
60
+ const matchesAny = (list, toolName, subject) =>
61
+ Array.isArray(list) && list.some((r) => matchRule(r, toolName, subject));
62
+
63
+ /**
64
+ * A Bash command that changes directory more than once needs a second look:
65
+ * each `&&`/`;`/`|`/newline-separated segment starting with `cd` counts.
66
+ * Mirrors the captured Claude Code 2.1.228 heuristic ("Multiple directory
67
+ * changes in one command require approval for clarity") — a hardcoded UX
68
+ * safety rail, not a configurable rule.
69
+ */
70
+ function isMultiDirCommand(command) {
71
+ const segments = String(command || '').split(/&&|\|\||;|\n/).map((s) => s.trim());
72
+ const cdCount = segments.filter((s) => /^cd(\s|$)/.test(s)).length;
73
+ return cdCount > 1;
74
+ }
75
+
76
+ /**
77
+ * Evaluate a tool call against the persisted /permissions rules.
78
+ * Returns 'allow' | 'deny' | 'ask'. Precedence: an explicit deny rule always
79
+ * wins (a security floor). An explicit allow rule is the real "always
80
+ * allow" escape hatch, so it's checked next and suppresses the multi-cd
81
+ * heuristic below it — same as upstream, where an always-allow rule for
82
+ * Bash stops it asking again. With no allow rule, the multi-cd heuristic
83
+ * forces 'ask' on its own; then explicit ask rules. Anything left
84
+ * unmatched falls through to rules.explicitAsk (an explicitly written
85
+ * `defaultMode: 'ask'` prompts on the residual); otherwise 'allow' — see
86
+ * the scope note above, the implicit defaultMode is intentionally not
87
+ * consulted.
88
+ */
89
+ function evalPermission(toolName, args, rules = {}) {
90
+ const subject = subjectFor(toolName, args);
91
+ if (matchesAny(rules.deny, toolName, subject)) return 'deny';
92
+ if (matchesAny(rules.allow, toolName, subject)) return 'allow';
93
+ if (toolName === 'Bash' && isMultiDirCommand(args && args.command)) return 'ask';
94
+ if (matchesAny(rules.ask, toolName, subject)) return 'ask';
95
+ if (rules.explicitAsk && rules.defaultMode === 'ask') return 'ask';
96
+ return 'allow';
97
+ }
98
+
99
+ module.exports = {
100
+ isMultiDirCommand,
101
+ evalPermission,
102
+ };
package/src/render.js CHANGED
@@ -245,7 +245,11 @@ function renderTurn(ctx, turn, width = 80) {
245
245
  if (turn.streaming) body[body.length - 1] += `${t.white}${GLYPH.block}${RESET}`;
246
246
  lines.push(...body);
247
247
  } else if (role === 'tool') {
248
- lines.push(`${t.white}${GLYPH.block}${RESET} ${t.gray}${turn.label || 'tool'}${RESET}`);
248
+ // turn.ok is only known once the call has actually run (the agent loop's
249
+ // tool-activity event fires after execution); undefined means "no result
250
+ // yet to report" (the plain registry-tool path, which never set it).
251
+ const status = turn.ok === undefined ? '' : turn.ok ? ` ${t.green}✓${RESET}` : ` ${t.red}${ERR}${RESET}`;
252
+ lines.push(`${t.white}${GLYPH.block}${RESET} ${t.gray}${turn.label || 'tool'}${RESET}${status}`);
249
253
  const args =
250
254
  turn.args == null
251
255
  ? ''
@@ -357,6 +361,33 @@ function renderToolResult(ctx, name, text, width = 80) {
357
361
  return lines;
358
362
  }
359
363
 
364
+ /**
365
+ * The tool-approval card: the mutating call the model wants to run (a diff
366
+ * for writeFile/editFile, the command for exec) and the three answers. One
367
+ * inline question, matching aegiscodex-dev's Bash/edit approval dialog.
368
+ */
369
+ function renderApproval(ctx, info = {}, width = 80) {
370
+ const t = themeOf(ctx);
371
+ const lines = [renderHeading(ctx, `confirm ${info.tool || 'tool'}`, width)];
372
+ const body = info.diff
373
+ ? info.diff
374
+ : info.args == null
375
+ ? ''
376
+ : typeof info.args === 'string'
377
+ ? info.args
378
+ : JSON.stringify(info.args);
379
+ for (const l of wrapBlock(body, Math.max(8, width - 2))) {
380
+ lines.push(l ? ` ${t.gray}${l}${RESET}` : '');
381
+ }
382
+ lines.push('');
383
+ lines.push(
384
+ ` ${t.coral}${GLYPH.bullet}${RESET} ${t.white}y${RESET}es once` +
385
+ ` ${t.dim}${GLYPH.bullet}${RESET} ${t.white}s${RESET}ession` +
386
+ ` ${t.dim}${GLYPH.bullet}${RESET} ${t.white}n${RESET}o ${t.dim}(default)${RESET}`
387
+ );
388
+ return lines;
389
+ }
390
+
360
391
  /** A transient notice. Kinds map gray / coral / red / green with the matching
361
392
  * glyph: info `·`, warn `⚠`, error `✗`, ok `✔`. */
362
393
  const NOTICE = {
@@ -381,6 +412,7 @@ module.exports = {
381
412
  renderWorking,
382
413
  renderHeading,
383
414
  renderToolResult,
415
+ renderApproval,
384
416
  renderNotice,
385
417
  mdLines,
386
418
  inline,
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Transcript summarization for /compact and /recap.
5
+ *
6
+ * Dependency-free port of aegiscodex-dev/src/summarize.js. The reference
7
+ * spawns `claude -p "Summarize…"` (via platform.resolveExecutable); here the
8
+ * backend is *injected* instead, so this module pulls in nothing but tokens.js:
9
+ *
10
+ * async summarizeTranscript(transcript, { callModel, model, signal } = {})
11
+ * async recapLine(transcript, { callModel, model, signal } = {})
12
+ *
13
+ * `callModel(prompt) -> string` (async) is the caller's own model seam. When
14
+ * `callModel` is not a function the module falls back to a purely local
15
+ * extraction — the first user text, the last assistant text and the turn count
16
+ * — and never throws. On abort (signal.aborted) it resolves null so handlers
17
+ * can report "cancelled"; on any other backend failure it resolves the local
18
+ * extraction.
19
+ */
20
+
21
+ const { estimateTokens } = require('./tokens.js');
22
+
23
+ // Guard: don't push more than this many chars into a prompt.
24
+ const MAX_INPUT_CHARS = 120_000;
25
+ const MAX_REPLY_CHARS = 2_000;
26
+
27
+ function extractiveSummary(transcript) {
28
+ const users = transcript.filter((m) => m.role === 'user').map((m) => m.text || '');
29
+ const lastAsst = [...transcript].reverse().find((m) => m.role === 'assistant');
30
+ const topics = users.slice(0, 3).map((t) => `“${t.length > 48 ? t.slice(0, 45) + '…' : t}”`);
31
+ const parts = [];
32
+ if (users.length) parts.push(`Covered ${users.length} prompt${users.length > 1 ? 's' : ''}: ${topics.join('; ')}.`);
33
+ if (lastAsst) {
34
+ const tail = (lastAsst.text || '').split(/\s+/).slice(0, 30).join(' ');
35
+ parts.push(`Last reply began: ${tail.length > 100 ? tail.slice(0, 97) + '…' : tail}`);
36
+ }
37
+ if (!parts.length) return 'Session with no exchanges yet.';
38
+ return parts.join(' ');
39
+ }
40
+
41
+ function demoSummary(transcript) {
42
+ return `[demo summary] ${extractiveSummary(transcript)}`;
43
+ }
44
+
45
+ /** Summarize the whole transcript in a few sentences. Resolves null on abort. */
46
+ async function summarizeTranscript(transcript, { callModel, model, signal } = {}) {
47
+ if (!transcript || !transcript.length) return 'Session with no exchanges yet.';
48
+ if (typeof callModel !== 'function') return demoSummary(transcript);
49
+ return callBackend('summarize', transcript, model, signal, callModel, () => demoSummary(transcript));
50
+ }
51
+
52
+ /** A single-line recap. Resolves null on abort. */
53
+ async function recapLine(transcript, { callModel, model, signal } = {}) {
54
+ if (!transcript || !transcript.length) return 'No exchanges yet.';
55
+ if (typeof callModel !== 'function') return `[demo recap] ${extractiveSummary(transcript)}`;
56
+ return callBackend('recap', transcript, model, signal, callModel, () => `[demo recap] ${extractiveSummary(transcript)}`);
57
+ }
58
+
59
+ async function callBackend(kind, transcript, model, signal, callModel, fallback) {
60
+ const text = transcriptToText(transcript).slice(0, MAX_INPUT_CHARS);
61
+ // NB: the ternary binds exactly as in the reference — the recap branch does
62
+ // not append the transcript text. Ported verbatim.
63
+ const prompt = kind === 'recap'
64
+ ? 'Recap the following conversation in one sentence. Be factual and specific.'
65
+ : 'Summarize the following conversation. Preserve every requirement, decision, and open question, in 2-4 sentences.'
66
+ + '\n\n' + text;
67
+
68
+ try {
69
+ if (signal && signal.aborted) return null;
70
+ const out = await callModel(prompt, { model, signal });
71
+ if (signal && signal.aborted) return null;
72
+ if (typeof out === 'string' && out.trim()) return out.trim().slice(0, MAX_REPLY_CHARS);
73
+ return fallback();
74
+ } catch {
75
+ return fallback();
76
+ }
77
+ }
78
+
79
+ function transcriptToText(transcript) {
80
+ return transcript
81
+ .filter((m) => m.role === 'user' || m.role === 'assistant' || m.role === 'note')
82
+ .map((m) => `${m.role}: ${m.text || ''}`)
83
+ .join('\n\n');
84
+ }
85
+
86
+ module.exports = {
87
+ summarizeTranscript,
88
+ recapLine,
89
+ estimateTokens,
90
+ };
package/src/system.js ADDED
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Small system helpers: best-effort URL opening (never blocks or errors) and
5
+ * a few constants shared by the support/easter-egg commands.
6
+ *
7
+ * Ported from aegiscodex-dev/src/system.js (ESM → CommonJS). The URLS table
8
+ * pointed at Anthropic/Claude support endpoints; those are re-homed to the
9
+ * AEGIS base (https://aegiscloud.org) and the project repo. Deep paths that
10
+ * had no AEGIS equivalent collapse to the base rather than inventing a route.
11
+ */
12
+
13
+ const { spawn } = require('node:child_process');
14
+
15
+ /** Best-effort open of a URL in the system browser. Returns true if launched. */
16
+ function openUrl(url) {
17
+ try {
18
+ const platform = process.platform;
19
+ const cmd = platform === 'darwin' ? ['open'] : platform === 'win32' ? ['cmd', '/c', 'start', ''] : ['xdg-open'];
20
+ const child = spawn(cmd[0], [...cmd.slice(1), url], { stdio: 'ignore', detached: true });
21
+ child.unref();
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ // AEGIS support URLs (used by /troubleshooting, /feedback…).
29
+ const URLS = {
30
+ troubleshooting: 'https://aegiscloud.org',
31
+ feedback: 'https://aegiscloud.org',
32
+ issues: 'https://github.com/aegisinfo/aegiscode-plugin/issues/new',
33
+ docs: 'https://aegiscloud.org',
34
+ radio: 'https://www.youtube.com/watch?v=cP8jB6YQWbQ',
35
+ };
36
+
37
+ module.exports = { openUrl, URLS };
package/src/tokens.js ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Token + cost estimation. Tokens are estimated from text length (~4
5
+ * chars/token, the usual rule of thumb) and priced against the per-model rates
6
+ * below. Everything is labeled approximate in the UI.
7
+ *
8
+ * Ported from aegiscodex-dev/src/tokens.js (ESM → CommonJS).
9
+ */
10
+
11
+ // Per-million-token USD rates. Cache-read/write matter for long sessions.
12
+ const RATES = {
13
+ sonnet: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
14
+ default: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
15
+ fable: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
16
+ opus: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
17
+ };
18
+
19
+ // System prompt + tool definitions overhead, roughly, in tokens.
20
+ const SYSTEM_TOKENS = 18000;
21
+ const TOOL_TOKENS = 12000;
22
+ // Default context window we budget against (Sonnet 5 class).
23
+ const CONTEXT_WINDOW = 200000;
24
+
25
+ // Real per-provider context budgets for the /context meter. The default above
26
+ // is the Claude-class window the UI was built around; providers with a
27
+ // different window map here so the meter shows the truth instead of a
28
+ // Sonnet-flavored estimate.
29
+ //
30
+ // DeepSeek V4 — verified live 2026-09-11 against api.deepseek.com:
31
+ // - a 942,031-token prompt was ACCEPTED (HTTP 200)
32
+ // - a 1,122,032-token prompt was REJECTED, verbatim: "This model's maximum
33
+ // context length is 1048576 tokens."
34
+ // so the window is 1M. The old 256k figure here understated it 4x and made the
35
+ // /context meter lie. Max output is 393216 — the API rejects anything larger
36
+ // with "the valid range of max_tokens is [1, 393216]".
37
+ // Keyed by provider name or model-id prefix (longest prefix wins via the
38
+ // iteration order below — exact id matches take priority).
39
+ const CONTEXT_WINDOWS = {
40
+ deepseek: 1_048_576,
41
+ openrouter: 256_000,
42
+ together: 256_000,
43
+ xai: 256_000,
44
+ groq: 128_000,
45
+ ollama: 128_000,
46
+ openai: 200_000,
47
+ anthropic: 200_000,
48
+ google: 1_000_000,
49
+ gemini: 1_000_000,
50
+ };
51
+
52
+ /** The context budget to show/guard against for a model id, provider, or raw model string. */
53
+ function contextWindowFor(model) {
54
+ const id = String(model || '').toLowerCase();
55
+ if (!id) return CONTEXT_WINDOW;
56
+ if (CONTEXT_WINDOWS[id]) return CONTEXT_WINDOWS[id];
57
+ for (const [prefix, win] of Object.entries(CONTEXT_WINDOWS)) {
58
+ if (id.startsWith(prefix)) return win;
59
+ }
60
+ return CONTEXT_WINDOW;
61
+ }
62
+
63
+ /** Rough token estimate: ~4 chars per token (heuristic, labeled approximate). */
64
+ function estimateTokens(text) {
65
+ if (!text) return 0;
66
+ return Math.max(1, Math.ceil([...String(text)].length / 4));
67
+ }
68
+
69
+ /**
70
+ * Bucket the transcript's token usage.
71
+ * input — user prompts + system + tools (per exchange)
72
+ * output — assistant replies
73
+ * cacheRead — what a resumed session reads back (prior context, approximated
74
+ * as all prior user+assistant text)
75
+ * cacheWrite — the newest user+assistant chunk written to cache
76
+ */
77
+ function transcriptUsage(transcript, model = 'sonnet') {
78
+ const userMsgs = transcript.filter((m) => m.role === 'user');
79
+ const asstMsgs = transcript.filter((m) => m.role === 'assistant');
80
+ const input = userMsgs.reduce((a, m) => a + estimateTokens(m.text || ''), 0);
81
+ const output = asstMsgs.reduce((a, m) => a + estimateTokens(m.text || ''), 0);
82
+ const cacheRead = Math.max(0, input - (userMsgs.length ? estimateTokens(userMsgs[userMsgs.length - 1].text || '') : 0));
83
+ const cacheWrite = userMsgs.length ? estimateTokens(userMsgs[userMsgs.length - 1].text || '') : 0;
84
+ return { input, output, cacheRead, cacheWrite };
85
+ }
86
+
87
+ /** Dollar cost of a usage record at the given model's rates. */
88
+ function usageCost(usage, model = 'sonnet') {
89
+ const r = RATES[model] || RATES.sonnet;
90
+ const toD = (n, rate) => (n / 1_000_000) * rate;
91
+ return toD(usage.input, r.input)
92
+ + toD(usage.output, r.output)
93
+ + toD(usage.cacheRead, r.cacheRead)
94
+ + toD(usage.cacheWrite, r.cacheWrite);
95
+ }
96
+
97
+ /** Full session accounting: per-bucket tokens, cost, and context used %. */
98
+ function sessionAccounting(transcript, model = 'sonnet') {
99
+ const usage = transcriptUsage(transcript, model);
100
+ const system = SYSTEM_TOKENS;
101
+ const tools = TOOL_TOKENS;
102
+ const history = usage.input + usage.output;
103
+ const used = system + tools + history;
104
+ const contextWindow = contextWindowFor(model);
105
+ return {
106
+ usage,
107
+ system,
108
+ tools,
109
+ history,
110
+ used,
111
+ contextWindow,
112
+ pct: Math.min(100, Math.round((used / contextWindow) * 100)),
113
+ cost: usageCost(usage, model),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Phase 4: build the same accounting shape from summed history.jsonl token
119
+ * records (live usage numbers when `real`, estimated otherwise). Real usage
120
+ * already includes system prompt + tools in cacheRead, so `used` counts
121
+ * input + output + cache instead of re-adding the constants.
122
+ */
123
+ function accountingFromUsage(usage, model = 'sonnet', { exchanges = 0, real = false, costUsd } = {}) {
124
+ const cost = typeof costUsd === 'number' ? costUsd : usageCost(usage, model);
125
+ const used = real
126
+ ? usage.input + usage.output + usage.cacheRead + usage.cacheWrite
127
+ : SYSTEM_TOKENS + TOOL_TOKENS + usage.input + usage.output;
128
+ const contextWindow = contextWindowFor(model);
129
+ return {
130
+ usage,
131
+ system: SYSTEM_TOKENS,
132
+ tools: TOOL_TOKENS,
133
+ history: usage.input + usage.output,
134
+ used,
135
+ contextWindow,
136
+ pct: Math.min(100, Math.round((used / contextWindow) * 100)),
137
+ cost,
138
+ exchanges,
139
+ real,
140
+ };
141
+ }
142
+
143
+ function fmtTokens(n) {
144
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
145
+ return String(n);
146
+ }
147
+
148
+ function fmtCost(d) {
149
+ return `$${d.toFixed(4)}`;
150
+ }
151
+
152
+ module.exports = {
153
+ RATES,
154
+ SYSTEM_TOKENS,
155
+ TOOL_TOKENS,
156
+ CONTEXT_WINDOW,
157
+ CONTEXT_WINDOWS,
158
+ contextWindowFor,
159
+ estimateTokens,
160
+ transcriptUsage,
161
+ usageCost,
162
+ sessionAccounting,
163
+ accountingFromUsage,
164
+ fmtTokens,
165
+ fmtCost,
166
+ };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * agents.js — subagent prompt presets for the desktop `task` tool, ported
5
+ * from aegiscodex-dev's src/agents.js (same roles, same synthesis presets).
6
+ * A preset is a system prompt: `task` runs it through the normal engine.chat
7
+ * loop as a nested turn (its own tool rounds, same model class), not a
8
+ * separate runtime, so the presets are pure prompt composition.
9
+ *
10
+ * Text is adapted to the desktop's own tool vocabulary (readFile/writeFile/
11
+ * editFile/listDir/glob/grep/exec/task — see tools.js) so a subagent's
12
+ * instructions never name a tool the desktop doesn't actually advertise.
13
+ */
14
+
15
+ /** Role → system prompt. */
16
+ const AGENT_PRESETS = {
17
+ synthesizer: `You are a senior technical lead. Given analysis from multiple specialist agents, synthesize their findings into a clear, actionable summary.
18
+ Structure your response as: key findings, recommended approach, top action items.
19
+ Be direct, concrete, and avoid repeating everything the agents said.
20
+ Focus on delivering a decision-ready synthesis.`,
21
+
22
+ architect: `You are a System Architect. Design the new application architecture.
23
+ Define: project structure, tech stack, directory layout, key modules, data flow, API design.
24
+ Consider: scalability, maintainability, testing strategy, deployment.
25
+ Output a concrete file tree and architecture decisions log. Be specific.`,
26
+
27
+ scaffolder: `You are a Project Scaffolder. Build the complete application from scratch.
28
+
29
+ YOUR JOB IS TO CREATE ALL PROJECT FILES - not just describe them.
30
+
31
+ Use writeFile to create: package.json, tsconfig.json, source files, configs, tests.
32
+ Generate COMPLETE, WORKING code - not stubs or placeholders.
33
+ Set up build scripts, lint config, and any necessary tooling.
34
+
35
+ After creating files, use exec to run: npm/pnpm install, then build/compile.
36
+ Fix any errors until the project builds successfully.
37
+
38
+ Be thorough - a real, runnable project is the goal.`,
39
+
40
+ planner: `You are a Refactoring Planner. Given the analyzer findings, create a step-by-step plan.
41
+ Each step: file path, what to change, why, risk level (LOW/MEDIUM/HIGH).
42
+ Include before/after snippets. Order by impact. Be concrete.`,
43
+
44
+ implementer: `You are an Implementation Engineer. Execute the refactoring plan.
45
+ Use editFile and writeFile to make actual code changes.
46
+ After each change, use readFile to verify correctness. Keep existing code style.
47
+ Run build commands with exec to ensure nothing is broken.`,
48
+
49
+ reviewer: `You are a Code Reviewer. Review the approach and code.
50
+ Check: logic errors, type safety, error handling, performance, security.
51
+ Be critical but constructive. Report specific issues with file paths.`,
52
+
53
+ debugger: `You are a Debugging Specialist. Analyze potential issues and edge cases.
54
+ Identify: failure modes, error handling gaps, testing considerations.
55
+ Think about what could go wrong and how to prevent it.`,
56
+
57
+ scanner: `You are a Security Vulnerability Scanner.
58
+ Scan for: hardcoded API keys/secrets, SQL injection, XSS, unsafe eval/exec, path traversal.
59
+ Use grep with targeted patterns. Report every finding with: file path, severity (CRITICAL/HIGH/MEDIUM/LOW), line number.`,
60
+
61
+ analyzer: `You are a Code Analyzer. Find refactoring opportunities.
62
+ Look for: duplicated code, long functions (>20 lines), complex conditionals, unused imports,
63
+ circular dependencies, inconsistent patterns. Report with file paths and line numbers.`,
64
+ };
65
+
66
+ /** System prompt for a `general` (or unrecognized) subagent type. */
67
+ const GENERAL_AGENT_PROMPT =
68
+ 'You are a capable autonomous coding subagent. Complete the assigned task end to end using ' +
69
+ 'the available tools (exec, readFile, writeFile, editFile, listDir, glob, grep). Work in the ' +
70
+ 'current repository, verify your work, and finish with a concise report of what you did and ' +
71
+ 'what you found. You cannot ask follow-up questions — make reasonable assumptions and proceed. ' +
72
+ 'When a large sub-task is better handled by a focused specialist, delegate it with the task tool.';
73
+
74
+ /** Human-readable role label for a preset id (falls back to the id). */
75
+ function agentRoleLabel(role) {
76
+ const labels = {
77
+ synthesizer: 'Technical Lead', architect: 'System Architect',
78
+ scaffolder: 'Project Scaffolder', planner: 'Refactoring Planner',
79
+ implementer: 'Implementation Engineer', reviewer: 'Code Reviewer',
80
+ debugger: 'Debugging Specialist', scanner: 'Vulnerability Scanner',
81
+ analyzer: 'Code Analyzer', general: 'General',
82
+ };
83
+ return labels[role] || role;
84
+ }
85
+
86
+ /** Every preset id, in palette order (excludes the 'general' fallback). */
87
+ function agentRoles() {
88
+ return Object.keys(AGENT_PRESETS);
89
+ }
90
+
91
+ /** Resolve a subagent_type to its system prompt (unknown/absent → general). */
92
+ function agentSystemPrompt(role) {
93
+ return AGENT_PRESETS[role] || GENERAL_AGENT_PROMPT;
94
+ }
95
+
96
+ module.exports = {
97
+ AGENT_PRESETS,
98
+ GENERAL_AGENT_PROMPT,
99
+ agentRoleLabel,
100
+ agentRoles,
101
+ agentSystemPrompt,
102
+ };