@kinqs/brainrouter-cli 0.3.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.
Files changed (87) hide show
  1. package/.env.example +109 -0
  2. package/README.md +185 -0
  3. package/dist/agent/agent.d.ts +765 -0
  4. package/dist/agent/agent.js +1977 -0
  5. package/dist/cli/cliPrompt.d.ts +15 -0
  6. package/dist/cli/cliPrompt.js +62 -0
  7. package/dist/cli/commands/_context.d.ts +53 -0
  8. package/dist/cli/commands/_context.js +14 -0
  9. package/dist/cli/commands/_helpers.d.ts +45 -0
  10. package/dist/cli/commands/_helpers.js +140 -0
  11. package/dist/cli/commands/guard.d.ts +6 -0
  12. package/dist/cli/commands/guard.js +292 -0
  13. package/dist/cli/commands/memory.d.ts +12 -0
  14. package/dist/cli/commands/memory.js +263 -0
  15. package/dist/cli/commands/obs.d.ts +6 -0
  16. package/dist/cli/commands/obs.js +208 -0
  17. package/dist/cli/commands/orchestration.d.ts +6 -0
  18. package/dist/cli/commands/orchestration.js +218 -0
  19. package/dist/cli/commands/session.d.ts +6 -0
  20. package/dist/cli/commands/session.js +191 -0
  21. package/dist/cli/commands/ui.d.ts +6 -0
  22. package/dist/cli/commands/ui.js +477 -0
  23. package/dist/cli/commands/workflow.d.ts +6 -0
  24. package/dist/cli/commands/workflow.js +691 -0
  25. package/dist/cli/repl.d.ts +12 -0
  26. package/dist/cli/repl.js +894 -0
  27. package/dist/config/config.d.ts +22 -0
  28. package/dist/config/config.js +105 -0
  29. package/dist/config/workspace.d.ts +7 -0
  30. package/dist/config/workspace.js +62 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +610 -0
  33. package/dist/memory/briefing.d.ts +46 -0
  34. package/dist/memory/briefing.js +152 -0
  35. package/dist/memory/consolidation.d.ts +60 -0
  36. package/dist/memory/consolidation.js +208 -0
  37. package/dist/memory/formatters.d.ts +38 -0
  38. package/dist/memory/formatters.js +102 -0
  39. package/dist/memory/mentions.d.ts +10 -0
  40. package/dist/memory/mentions.js +72 -0
  41. package/dist/orchestration/orchestrator.d.ts +36 -0
  42. package/dist/orchestration/orchestrator.js +71 -0
  43. package/dist/orchestration/roles.d.ts +11 -0
  44. package/dist/orchestration/roles.js +117 -0
  45. package/dist/orchestration/tools.d.ts +244 -0
  46. package/dist/orchestration/tools.js +528 -0
  47. package/dist/prompt/breadthHint.d.ts +48 -0
  48. package/dist/prompt/breadthHint.js +93 -0
  49. package/dist/prompt/compactor.d.ts +31 -0
  50. package/dist/prompt/compactor.js +112 -0
  51. package/dist/prompt/initAgentMd.d.ts +13 -0
  52. package/dist/prompt/initAgentMd.js +194 -0
  53. package/dist/prompt/skillRunner.d.ts +34 -0
  54. package/dist/prompt/skillRunner.js +146 -0
  55. package/dist/prompt/systemPrompt.d.ts +10 -0
  56. package/dist/prompt/systemPrompt.js +171 -0
  57. package/dist/runtime/clipboard.d.ts +17 -0
  58. package/dist/runtime/clipboard.js +52 -0
  59. package/dist/runtime/llmSemaphore.d.ts +30 -0
  60. package/dist/runtime/llmSemaphore.js +67 -0
  61. package/dist/runtime/loopRunner.d.ts +25 -0
  62. package/dist/runtime/loopRunner.js +79 -0
  63. package/dist/runtime/mcpClient.d.ts +156 -0
  64. package/dist/runtime/mcpClient.js +234 -0
  65. package/dist/runtime/mcpUtils.d.ts +36 -0
  66. package/dist/runtime/mcpUtils.js +64 -0
  67. package/dist/runtime/sandbox.d.ts +48 -0
  68. package/dist/runtime/sandbox.js +156 -0
  69. package/dist/runtime/tracing.d.ts +25 -0
  70. package/dist/runtime/tracing.js +91 -0
  71. package/dist/state/cliState.d.ts +59 -0
  72. package/dist/state/cliState.js +311 -0
  73. package/dist/state/goalStore.d.ts +174 -0
  74. package/dist/state/goalStore.js +410 -0
  75. package/dist/state/hookifyStore.d.ts +80 -0
  76. package/dist/state/hookifyStore.js +237 -0
  77. package/dist/state/hooksStore.d.ts +42 -0
  78. package/dist/state/hooksStore.js +71 -0
  79. package/dist/state/preferencesStore.d.ts +41 -0
  80. package/dist/state/preferencesStore.js +25 -0
  81. package/dist/state/sessionStore.d.ts +42 -0
  82. package/dist/state/sessionStore.js +193 -0
  83. package/dist/state/taskStore.d.ts +23 -0
  84. package/dist/state/taskStore.js +80 -0
  85. package/dist/state/workflowArtifacts.d.ts +33 -0
  86. package/dist/state/workflowArtifacts.js +139 -0
  87. package/package.json +71 -0
@@ -0,0 +1,31 @@
1
+ import type { LLMConfig } from '../config/config.js';
2
+ export interface CompactionInput {
3
+ /** The chat history minus the system message. */
4
+ messages: Array<{
5
+ role: string;
6
+ content: string;
7
+ name?: string;
8
+ }>;
9
+ /** Workspace root, surfaced in the prompt so the model can be specific. */
10
+ workspaceRoot: string;
11
+ /** The last user message verbatim. */
12
+ lastUserMessage?: string;
13
+ }
14
+ export interface CompactionResult {
15
+ summary: string;
16
+ /** Approximate token estimate of the produced summary. */
17
+ estimatedTokens: number;
18
+ /** Wall clock for the compaction call (ms). */
19
+ durationMs: number;
20
+ }
21
+ /**
22
+ * Run compaction by asking the LLM for a structured summary. Returns the
23
+ * summary as a single string; the caller decides how to splice it back into
24
+ * the chat history. We don't mutate state here — that's the agent's job.
25
+ */
26
+ export declare function runCompaction(llm: LLMConfig, input: CompactionInput): Promise<CompactionResult>;
27
+ /**
28
+ * Compose the system block that replaces the verbose chat history after
29
+ * compaction. Keep it tagged so the agent loop knows it came from /compact.
30
+ */
31
+ export declare function renderCompactSystemMessage(summary: string): string;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Conversation compaction for long sessions.
3
+ *
4
+ * The REPL's old `/compact` just nuked chat history. That works in a pinch but
5
+ * loses every decision the agent made. This module asks the model to write a
6
+ * structured summary of the conversation so far, then replaces the verbose
7
+ * history with a single condensed system message.
8
+ *
9
+ * The summary lives in a stable shape so the post-compact turn can reason
10
+ * about it without missing critical state:
11
+ *
12
+ * - Goals: what the user is trying to accomplish
13
+ * - Decisions: design choices already made
14
+ * - Files touched: paths the agent has read or written
15
+ * - Open work: what remains to do
16
+ * - Last user request: verbatim so the next turn picks up cleanly
17
+ */
18
+ const COMPACT_SYSTEM_PROMPT = [
19
+ 'You are compacting a long agent conversation so it can continue in a fresh context window.',
20
+ 'Produce a structured summary the next turn can read in 1 second. Use the headings shown below.',
21
+ '',
22
+ '# Goals',
23
+ 'List the user\'s current high-level goals as bullets.',
24
+ '',
25
+ '# Decisions made',
26
+ 'List decisions, choices, or assumptions already agreed upon. Quote file paths where helpful.',
27
+ '',
28
+ '# Files touched',
29
+ 'List file paths the agent has read, written, or edited.',
30
+ '',
31
+ '# Open work',
32
+ 'List what is still pending — bug fixes, follow-ups, tests, reviews.',
33
+ '',
34
+ '# Last user request',
35
+ 'Quote the user\'s most recent message verbatim. Do not paraphrase.',
36
+ '',
37
+ 'Output Markdown only. No preamble. No code fences around the summary itself.',
38
+ ].join('\n');
39
+ /**
40
+ * Run compaction by asking the LLM for a structured summary. Returns the
41
+ * summary as a single string; the caller decides how to splice it back into
42
+ * the chat history. We don't mutate state here — that's the agent's job.
43
+ */
44
+ export async function runCompaction(llm, input) {
45
+ const startedAt = Date.now();
46
+ const flattened = input.messages
47
+ .filter((m) => m.role !== 'system')
48
+ .map((m) => `### ${m.role.toUpperCase()}${m.name ? ` (${m.name})` : ''}\n${m.content}`)
49
+ .join('\n\n');
50
+ const userMsg = [
51
+ `# Compaction request`,
52
+ `Workspace: ${input.workspaceRoot}`,
53
+ input.lastUserMessage ? `Last user message (treat as anchor): ${input.lastUserMessage}` : '',
54
+ '',
55
+ '# Conversation transcript',
56
+ flattened,
57
+ ].filter(Boolean).join('\n');
58
+ const body = {
59
+ model: llm.model,
60
+ messages: [
61
+ { role: 'system', content: COMPACT_SYSTEM_PROMPT },
62
+ { role: 'user', content: userMsg },
63
+ ],
64
+ };
65
+ const endpoint = llm.endpoint || 'https://api.openai.com/v1';
66
+ const apiKey = llm.apiKey || process.env.OPENAI_API_KEY || '';
67
+ const headers = { 'Content-Type': 'application/json' };
68
+ if (apiKey)
69
+ headers['Authorization'] = `Bearer ${apiKey}`;
70
+ const timeoutMs = Number(process.env.BRAINROUTER_LLM_TIMEOUT_MS || 60000);
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
73
+ let res;
74
+ try {
75
+ res = await fetch(`${endpoint}/chat/completions`, {
76
+ method: 'POST',
77
+ headers,
78
+ body: JSON.stringify(body),
79
+ signal: controller.signal,
80
+ });
81
+ }
82
+ finally {
83
+ clearTimeout(timer);
84
+ }
85
+ if (!res.ok) {
86
+ const errText = await res.text();
87
+ throw new Error(`compaction call failed: ${res.status} ${res.statusText} - ${errText.slice(0, 500)}`);
88
+ }
89
+ const data = await res.json();
90
+ const text = String(data?.choices?.[0]?.message?.content ?? '').trim();
91
+ if (!text)
92
+ throw new Error('compaction returned an empty summary');
93
+ return {
94
+ summary: text,
95
+ estimatedTokens: Math.ceil(text.length / 4),
96
+ durationMs: Date.now() - startedAt,
97
+ };
98
+ }
99
+ /**
100
+ * Compose the system block that replaces the verbose chat history after
101
+ * compaction. Keep it tagged so the agent loop knows it came from /compact.
102
+ */
103
+ export function renderCompactSystemMessage(summary) {
104
+ return [
105
+ '## Compacted conversation summary',
106
+ '',
107
+ 'The conversation up to this point was compacted to fit the context window.',
108
+ 'Treat the following as authoritative state and resume from "Last user request".',
109
+ '',
110
+ summary,
111
+ ].join('\n');
112
+ }
@@ -0,0 +1,13 @@
1
+ export interface InitResult {
2
+ status: 'created' | 'exists';
3
+ path: string;
4
+ }
5
+ /**
6
+ * Create AGENT.md in the workspace root if neither AGENT.md nor AGENTS.md is
7
+ * already present. Idempotent: returns { status: 'exists' } when something
8
+ * already lives there.
9
+ *
10
+ * We use AGENT.md (singular) as the canonical name — most AGENT-md aware tools
11
+ * read both spellings, so a singular file works everywhere.
12
+ */
13
+ export declare function initAgentMd(workspaceRoot: string): InitResult;
@@ -0,0 +1,194 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Repo-signal scan. We sniff for common project files and use that to populate
5
+ * the AGENT.md template with realistic guesses instead of a blank "Describe…"
6
+ * placeholder. Detected signals appear under "Detected project signals" so the
7
+ * user can verify them at a glance.
8
+ */
9
+ function detectRepoSignals(root) {
10
+ const hits = [];
11
+ const buildCmds = [];
12
+ const testCmds = [];
13
+ const has = (rel) => fs.existsSync(path.join(root, rel));
14
+ const read = (rel) => {
15
+ try {
16
+ return fs.readFileSync(path.join(root, rel), 'utf8');
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ };
22
+ if (has('package.json')) {
23
+ hits.push('Node.js / npm (`package.json`)');
24
+ try {
25
+ const pkg = JSON.parse(read('package.json') ?? '{}');
26
+ const scripts = pkg.scripts ?? {};
27
+ if (scripts.build)
28
+ buildCmds.push('npm run build');
29
+ if (scripts.dev)
30
+ buildCmds.push('npm run dev');
31
+ if (scripts.test)
32
+ testCmds.push('npm test');
33
+ if (scripts.lint)
34
+ testCmds.push('npm run lint');
35
+ if (scripts.typecheck)
36
+ testCmds.push('npm run typecheck');
37
+ if (pkg.workspaces)
38
+ hits.push('npm workspaces (monorepo)');
39
+ }
40
+ catch { /* malformed package.json — skip */ }
41
+ }
42
+ if (has('pnpm-workspace.yaml') || has('pnpm-lock.yaml'))
43
+ hits.push('pnpm');
44
+ if (has('yarn.lock'))
45
+ hits.push('yarn');
46
+ if (has('tsconfig.json'))
47
+ hits.push('TypeScript (`tsconfig.json`)');
48
+ if (has('go.mod')) {
49
+ hits.push('Go (`go.mod`)');
50
+ buildCmds.push('go build ./...');
51
+ testCmds.push('go test ./...');
52
+ }
53
+ if (has('Cargo.toml')) {
54
+ hits.push('Rust (`Cargo.toml`)');
55
+ buildCmds.push('cargo build');
56
+ testCmds.push('cargo test');
57
+ }
58
+ if (has('pyproject.toml') || has('requirements.txt') || has('setup.py')) {
59
+ hits.push('Python');
60
+ if (has('pytest.ini') || (read('pyproject.toml') ?? '').includes('pytest')) {
61
+ testCmds.push('pytest');
62
+ }
63
+ }
64
+ if (has('Gemfile'))
65
+ hits.push('Ruby (`Gemfile`)');
66
+ if (has('Dockerfile'))
67
+ hits.push('Docker (`Dockerfile`)');
68
+ if (has('docker-compose.yml') || has('docker-compose.yaml') || has('compose.yaml'))
69
+ hits.push('Docker Compose');
70
+ if (has('.github/workflows'))
71
+ hits.push('GitHub Actions CI');
72
+ if (has('.gitlab-ci.yml'))
73
+ hits.push('GitLab CI');
74
+ if (has('Makefile')) {
75
+ hits.push('Makefile');
76
+ buildCmds.push('make');
77
+ testCmds.push('make test');
78
+ }
79
+ if (has('.env.example') || has('.env.sample'))
80
+ hits.push('Env template (`.env.example`)');
81
+ if (has('CLAUDE.md') || has('AGENTS.md'))
82
+ hits.push('Existing sibling agent doc');
83
+ if (has('README.md'))
84
+ hits.push('README.md');
85
+ return { hits, buildCmds: dedupe(buildCmds), testCmds: dedupe(testCmds) };
86
+ }
87
+ function dedupe(items) {
88
+ return Array.from(new Set(items));
89
+ }
90
+ function renderTemplate(signals, projectName) {
91
+ const { hits, buildCmds, testCmds } = signals;
92
+ const buildSection = buildCmds.length > 0
93
+ ? buildCmds.map((c) => `- Build / dev: \`${c}\``).join('\n')
94
+ : '- Build / dev: _(fill in — e.g. `npm run build`, `cargo build`, `go build ./...`)_';
95
+ const testSection = testCmds.length > 0
96
+ ? testCmds.map((c) => `- Test: \`${c}\``).join('\n')
97
+ : '- Test: _(fill in)_';
98
+ const signalsSection = hits.length > 0
99
+ ? hits.map((h) => `- ${h}`).join('\n')
100
+ : '- _(no signals detected — fill in stack manually)_';
101
+ return `# AGENT.md
102
+
103
+ > Instructions for AI coding agents working in this repo. Compatible with AGENT.md / AGENTS.md aware tools.
104
+
105
+ ## Project context
106
+
107
+ **${projectName}** — describe what this project is and the high-level architecture in 2-3 sentences.
108
+
109
+ ## Detected project signals
110
+
111
+ ${signalsSection}
112
+
113
+ ## Build, test, run
114
+
115
+ ${buildSection}
116
+ ${testSection}
117
+
118
+ ## Conventions
119
+
120
+ - Code style: _(describe — formatter, lint config)_
121
+ - Testing: _(unit/integration patterns)_
122
+ - Commits: conventional commits (\`feat\`, \`fix\`, \`chore\`, ...)
123
+
124
+ ## Boundaries
125
+
126
+ - Always do: run tests before claiming work is complete; cite memory record ids when used.
127
+ - Ask first: schema migrations, dependency upgrades, anything that touches secrets.
128
+ - Never do: commit \`.env\` or anything matching \`*.key\`, modify \`vendor/\`, skip git hooks.
129
+
130
+ ## Skill hints
131
+
132
+ If you have catalogued BrainRouter skills relevant to this repo, list them here:
133
+
134
+ - ${"`code-review-and-quality`"} — use before merging.
135
+ - ${"`agentic-engineering-workflow`"} — use for /feature-dev.
136
+ `;
137
+ }
138
+ const TEMPLATE_FALLBACK = `# AGENT.md
139
+
140
+ > Instructions for AI coding agents working in this repo. Compatible with AGENT.md / AGENTS.md aware tools.
141
+
142
+ ## Project context
143
+
144
+ Describe what this project is and the high-level architecture in 2-3 sentences.
145
+
146
+ ## Build, test, run
147
+
148
+ - Install: \`npm install\`
149
+ - Build: \`npm run build\`
150
+ - Test: \`npm test\`
151
+ - Run dev: \`npm run dev\`
152
+
153
+ ## Conventions
154
+
155
+ - Code style: …
156
+ - Testing: …
157
+ - Commits: conventional commits (\`feat\`, \`fix\`, \`chore\`, …).
158
+
159
+ ## Boundaries
160
+
161
+ - Always do: run tests before claiming work is complete; cite memory record ids when used.
162
+ - Ask first: schema migrations, dependency upgrades, anything that touches secrets.
163
+ - Never do: commit \`.env\` or anything matching \`*.key\`, modify \`vendor/\`, skip git hooks.
164
+
165
+ ## Skill hints
166
+
167
+ If you have catalogued skills relevant to this repo, list them here so the
168
+ \`memory_register_skill_hints\` tool can warm them up automatically:
169
+
170
+ - ${"`code-review-and-quality`"} — use before merging.
171
+ - ${"`agentic-engineering-workflow`"} — use for /feature-dev.
172
+ `;
173
+ /**
174
+ * Create AGENT.md in the workspace root if neither AGENT.md nor AGENTS.md is
175
+ * already present. Idempotent: returns { status: 'exists' } when something
176
+ * already lives there.
177
+ *
178
+ * We use AGENT.md (singular) as the canonical name — most AGENT-md aware tools
179
+ * read both spellings, so a singular file works everywhere.
180
+ */
181
+ export function initAgentMd(workspaceRoot) {
182
+ const candidates = ['AGENT.md', 'AGENTS.md'].map((name) => path.join(workspaceRoot, name));
183
+ for (const candidate of candidates) {
184
+ if (fs.existsSync(candidate)) {
185
+ return { status: 'exists', path: candidate };
186
+ }
187
+ }
188
+ const target = candidates[0];
189
+ const projectName = path.basename(workspaceRoot);
190
+ const signals = detectRepoSignals(workspaceRoot);
191
+ const body = signals.hits.length > 0 ? renderTemplate(signals, projectName) : TEMPLATE_FALLBACK;
192
+ fs.writeFileSync(target, body, 'utf8');
193
+ return { status: 'created', path: target };
194
+ }
@@ -0,0 +1,34 @@
1
+ import type { McpClientWrapper } from '../runtime/mcpClient.js';
2
+ export interface SkillResolution {
3
+ name: string;
4
+ body: string;
5
+ source: 'mcp' | 'filesystem' | 'fallback';
6
+ }
7
+ export interface RunSkillOptions {
8
+ /** Free-text input from the user (e.g., feature description, review scope). */
9
+ input?: string;
10
+ /** Optional extra orchestration directives appended after the skill body. */
11
+ orchestration?: string;
12
+ /** Skill section to fetch from get_skill. Defaults to "workflow" — pass "full" for the entire SKILL.md. */
13
+ section?: 'description' | 'overview' | 'when_to_use' | 'workflow' | 'usage' | 'detailed_instructions' | 'phases' | 'checklist' | 'red_flags' | 'rationalizations' | 'full';
14
+ }
15
+ /**
16
+ * Slash-command → skill mapping. Each entry names the skill catalogued in
17
+ * the BrainRouter skills/ folder (and exposed by the MCP server) that the
18
+ * command delegates to. The CLI sends a thin prompt; the heavy lifting lives
19
+ * in the skill body, so authoring is centralized.
20
+ */
21
+ export declare const SLASH_TO_SKILL: Record<string, string>;
22
+ /**
23
+ * Resolve a skill by name. Prefers the MCP server (so users get whatever the
24
+ * server has loaded, including their own private skills), falls back to a
25
+ * local filesystem scan of `skills/` for when the MCP tool is unavailable.
26
+ */
27
+ export declare function resolveSkill(mcpClient: McpClientWrapper, name: string, workspaceRoot: string, section?: RunSkillOptions['section']): Promise<SkillResolution>;
28
+ /**
29
+ * Build the user prompt that asks the agent to execute a skill. The skill
30
+ * body is embedded so the agent does not need to round-trip through
31
+ * `get_skill` again. Orchestration affordances (spawn_agent, update_plan)
32
+ * are reminded explicitly so multi-agent workflows are actually triggered.
33
+ */
34
+ export declare function buildSkillPrompt(skill: SkillResolution, options?: RunSkillOptions): string;
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ const requireFromHere = createRequire(import.meta.url);
5
+ /**
6
+ * Slash-command → skill mapping. Each entry names the skill catalogued in
7
+ * the BrainRouter skills/ folder (and exposed by the MCP server) that the
8
+ * command delegates to. The CLI sends a thin prompt; the heavy lifting lives
9
+ * in the skill body, so authoring is centralized.
10
+ */
11
+ export const SLASH_TO_SKILL = {
12
+ '/feature-dev': 'agentic-engineering-workflow',
13
+ '/review': 'code-review-and-quality',
14
+ '/implement-plan': 'incremental-skill',
15
+ '/spec': 'spec-driven-skill',
16
+ '/plan-write': 'planning-skill',
17
+ '/debug': 'debug-skill',
18
+ '/handover': 'handover-skill',
19
+ '/commit-skill': 'git-workflow-skill',
20
+ '/changelog': 'changelog-generator',
21
+ '/refactor': 'code-simplification',
22
+ '/test': 'testing-skill',
23
+ };
24
+ const WORKSPACE_SKILL_ROOTS = ['skills', '.brainrouter/skills'];
25
+ /**
26
+ * Resolve a skill by name. Prefers the MCP server (so users get whatever the
27
+ * server has loaded, including their own private skills), falls back to a
28
+ * local filesystem scan of `skills/` for when the MCP tool is unavailable.
29
+ */
30
+ export async function resolveSkill(mcpClient, name, workspaceRoot, section = 'full') {
31
+ try {
32
+ const res = await mcpClient.callTool('get_skill', { name, section });
33
+ if (!res.isError && Array.isArray(res.content) && res.content[0]?.text) {
34
+ return { name, body: res.content[0].text, source: 'mcp' };
35
+ }
36
+ }
37
+ catch {
38
+ // Fall through to filesystem lookup.
39
+ }
40
+ const body = readSkillFromFilesystem(workspaceRoot, name);
41
+ if (body) {
42
+ return { name, body, source: 'filesystem' };
43
+ }
44
+ return {
45
+ name,
46
+ body: `(No SKILL.md found for "${name}". Use your general judgement and the agentic-engineering-workflow defaults.)`,
47
+ source: 'fallback',
48
+ };
49
+ }
50
+ function readSkillFromFilesystem(workspaceRoot, name) {
51
+ for (const root of skillSearchRoots(workspaceRoot)) {
52
+ if (!fs.existsSync(root))
53
+ continue;
54
+ const match = findSkillDir(root, name);
55
+ if (match) {
56
+ try {
57
+ return fs.readFileSync(path.join(match, 'SKILL.md'), 'utf8');
58
+ }
59
+ catch { /* ignore */ }
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Roots to search for SKILL.md when the MCP server is unavailable. Includes:
66
+ * - the user's workspace (so per-project skills win locally)
67
+ * - the installed @kinqs/brainrouter-mcp-server package directory (so the canonical
68
+ * BrainRouter catalogue is found even when MCP is down, because prepack
69
+ * bundles `skills/` into the published package)
70
+ * - the monorepo root (when running from source during development)
71
+ */
72
+ function skillSearchRoots(workspaceRoot) {
73
+ const roots = [];
74
+ for (const sub of WORKSPACE_SKILL_ROOTS) {
75
+ roots.push(path.join(workspaceRoot, sub));
76
+ }
77
+ const mcpPkgDir = resolveInstalledMcpPackageDir();
78
+ if (mcpPkgDir)
79
+ roots.push(path.join(mcpPkgDir, 'skills'));
80
+ return roots;
81
+ }
82
+ function resolveInstalledMcpPackageDir() {
83
+ try {
84
+ const pkgJsonPath = requireFromHere.resolve('@kinqs/brainrouter-mcp-server/package.json');
85
+ return path.dirname(pkgJsonPath);
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ function findSkillDir(rootDir, skillName, depth = 3) {
92
+ if (depth < 0)
93
+ return undefined;
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory())
103
+ continue;
104
+ const child = path.join(rootDir, entry.name);
105
+ if (entry.name === skillName && fs.existsSync(path.join(child, 'SKILL.md'))) {
106
+ return child;
107
+ }
108
+ const nested = findSkillDir(child, skillName, depth - 1);
109
+ if (nested)
110
+ return nested;
111
+ }
112
+ return undefined;
113
+ }
114
+ /**
115
+ * Build the user prompt that asks the agent to execute a skill. The skill
116
+ * body is embedded so the agent does not need to round-trip through
117
+ * `get_skill` again. Orchestration affordances (spawn_agent, update_plan)
118
+ * are reminded explicitly so multi-agent workflows are actually triggered.
119
+ */
120
+ export function buildSkillPrompt(skill, options = {}) {
121
+ const sections = [];
122
+ sections.push(`# Executing skill: ${skill.name}`);
123
+ sections.push(`Source: ${skill.source}`);
124
+ sections.push('');
125
+ sections.push('## Skill instructions');
126
+ sections.push(skill.body.trim());
127
+ if (options.input?.trim()) {
128
+ sections.push('');
129
+ sections.push('## User input');
130
+ sections.push(options.input.trim());
131
+ }
132
+ sections.push('');
133
+ sections.push('## Execution affordances');
134
+ sections.push([
135
+ '- You may delegate bounded parallel work with `spawn_agent` (roles: explorer, architect, reviewer, worker, verifier).',
136
+ '- Keep the durable plan current with `update_plan`. At most one item should be `in_progress`.',
137
+ '- Persist meaningful outputs through BrainRouter memory tools (`memory_capture_turn`, `memory_working_offload`) as the skill dictates.',
138
+ '- Always synthesize child outputs in your own words before claiming work is done.',
139
+ ].join('\n'));
140
+ if (options.orchestration?.trim()) {
141
+ sections.push('');
142
+ sections.push('## CLI orchestration hints');
143
+ sections.push(options.orchestration.trim());
144
+ }
145
+ return sections.join('\n');
146
+ }
@@ -0,0 +1,10 @@
1
+ export interface SystemPromptContext {
2
+ workspaceRoot: string;
3
+ launchCwd: string;
4
+ sessionKey: string;
5
+ instructionSummary?: string;
6
+ /** Communication style overlay set by /personality. */
7
+ personality?: 'concise' | 'standard' | 'detailed' | 'pair-programmer';
8
+ }
9
+ export declare function buildSystemPrompt(context: SystemPromptContext): string;
10
+ export declare function loadWorkspaceInstructionSummary(workspaceRoot: string): string | undefined;