codemini-cli 0.5.9 → 0.5.10

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 @@
1
+ import{i as e}from"./index-BK75hMb2.js";export{e as Mermaid};
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.palette = palette;
15
15
  })();
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-C4tKT3v4.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-BK75hMb2.js"></script>
18
18
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-S-ySWqyJ.js">
19
19
  <link rel="stylesheet" crossorigin href="/assets/index-BSdIdn3L.css">
20
20
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: brainstorm
3
3
  description: Lightweight brainstorming skill. Use when a feature or behavior request has multiple reasonable approaches and the missing piece is user preference, tradeoff choice, or key constraint.
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  ---
6
6
 
7
7
  Use this skill only when the task needs clarification or option comparison before coding.
@@ -18,6 +18,7 @@ Do NOT skip this skill for tasks that appear straightforward but lack clear requ
18
18
  2. **Give 2-3 short options** only when the blocking constraint is already clear. Keep options concrete and focused on the main tradeoff.
19
19
  3. **Present conclusions as suggested decisions**, not final choices.
20
20
  4. **Do NOT write code, pseudo-code, file edits, or broad repo exploration** while direction is still being chosen.
21
+ 5. **Stop after your brainstorm response.** Do not say "I will start", "starting now", "I'll edit", or otherwise transition into implementation in the same turn.
21
22
 
22
23
  ## Output Formats
23
24
 
@@ -54,6 +55,8 @@ Suggested decision:
54
55
  - reason: <why>
55
56
  ```
56
57
 
58
+ After Mode B, STOP. Wait for the user to approve, reject, or revise the suggested decision.
59
+
57
60
  ## Self-Review
58
61
 
59
62
  Before presenting options or a suggested decision, quickly check:
@@ -64,9 +67,11 @@ Before presenting options or a suggested decision, quickly check:
64
67
 
65
68
  ## Exit
66
69
 
67
- After the user approves a direction:
70
+ Brainstorm ends only when the user sends a later message that clearly approves a direction, for example "use option 2", "按这个做", "确认,开始实现", or "直接写代码".
71
+
72
+ After that later user approval:
68
73
 
69
74
  - If the task is small and clear enough to implement directly → proceed to code.
70
75
  - If the task is non-trivial or touches multiple areas → YOU MUST invoke `writing-plans` to create an implementation plan before coding.
71
76
 
72
- Do NOT stop at the brainstorm conclusion when the natural next step is planning.
77
+ Do NOT treat your own suggested decision as approval. Do NOT continue from the brainstorm conclusion into planning or implementation until the user has explicitly approved a direction in a separate message.
package/src/core/ast.js CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
4
4
  import { Parser, Language, Query } from 'web-tree-sitter';
5
5
  import { LANGUAGE_ALIASES, EXTENSION_LANGUAGE_MAP } from './constants.js';
6
6
  import { sha256Prefixed as sha256 } from './crypto-utils.js';
7
+ import { BoundedCache } from './bounded-cache.js';
7
8
 
8
9
  const require = createRequire(import.meta.url);
9
10
 
@@ -43,7 +44,7 @@ const parserInitPromise = Parser.init({
43
44
  return scriptName === 'web-tree-sitter.wasm' ? TREE_SITTER_WASM_PATH : scriptName;
44
45
  }
45
46
  });
46
- const languageCache = new Map();
47
+ const languageCache = new BoundedCache({ maxSize: 16, ttlMs: 60 * 60 * 1000 });
47
48
 
48
49
  function clipText(text, maxLen = 220) {
49
50
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
@@ -113,15 +114,34 @@ async function loadLanguage(language) {
113
114
  if (languageCache.has(language)) return languageCache.get(language);
114
115
  const wasmPath = LANGUAGE_WASM_PATHS[language];
115
116
  if (!wasmPath) throw new Error(`Unsupported Tree-sitter language: ${language}`);
116
- const loaded = await Language.load(wasmPath);
117
- languageCache.set(language, loaded);
118
- return loaded;
117
+ const loadPromise = Language.load(wasmPath);
118
+ languageCache.set(language, loadPromise);
119
+ try {
120
+ return await loadPromise;
121
+ } catch (error) {
122
+ languageCache.delete(language);
123
+ throw error;
124
+ }
119
125
  }
120
126
 
121
- async function parseContent(content, language) {
127
+ async function getParser(language) {
122
128
  const loadedLanguage = await loadLanguage(language);
123
129
  const parser = new Parser();
124
130
  parser.setLanguage(loadedLanguage);
131
+ return { parser, loadedLanguage };
132
+ }
133
+
134
+ function deleteParsed(parsed) {
135
+ try {
136
+ parsed?.tree?.delete?.();
137
+ } catch {}
138
+ try {
139
+ parsed?.parser?.delete?.();
140
+ } catch {}
141
+ }
142
+
143
+ async function parseContent(content, language) {
144
+ const { parser, loadedLanguage } = await getParser(language);
125
145
  const tree = parser.parse(content);
126
146
  return { parser, tree, loadedLanguage };
127
147
  }
@@ -188,8 +208,7 @@ export async function findEnclosingSymbol(content, filePath, line) {
188
208
  } catch {
189
209
  return null;
190
210
  } finally {
191
- if (tree) tree.delete();
192
- if (parser) parser.delete();
211
+ deleteParsed({ tree, parser });
193
212
  }
194
213
  }
195
214
 
@@ -223,8 +242,7 @@ export async function queryAst(root, args) {
223
242
  }
224
243
 
225
244
  query.delete();
226
- parsed.tree.delete();
227
- parsed.parser.delete();
245
+ deleteParsed(parsed);
228
246
 
229
247
  return {
230
248
  path: relativePath,
@@ -261,8 +279,7 @@ export async function readAstNode(root, args) {
261
279
  child_summaries: node.namedChildren.slice(0, 8).map((child) => summarizeNode(child))
262
280
  };
263
281
 
264
- parsed.tree.delete();
265
- parsed.parser.delete();
282
+ deleteParsed(parsed);
266
283
  return result;
267
284
  }
268
285
 
@@ -277,15 +294,13 @@ export async function resolveAstTarget(root, relativePath, astTarget) {
277
294
  const parsed = await parseFile(root, relativePath, astTarget.language);
278
295
  const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
279
296
  if (!node) {
280
- parsed.tree.delete();
281
- parsed.parser.delete();
297
+ deleteParsed(parsed);
282
298
  throw new Error('AST target no longer matches the current file');
283
299
  }
284
300
 
285
301
  const currentHash = sha256(node.text);
286
302
  if (String(astTarget.range_hash || '') !== currentHash) {
287
- parsed.tree.delete();
288
- parsed.parser.delete();
303
+ deleteParsed(parsed);
289
304
  throw new Error('ast_target range_hash mismatch; the selected node changed and is now stale');
290
305
  }
291
306
 
@@ -128,6 +128,8 @@ function getCompletionCopy(language = 'zh') {
128
128
  'context.prompt_budget_audit': 'Prompt 预算审计开关',
129
129
  'context.microcompact_enabled': '微压缩(micro-compact)开关',
130
130
  'context.microcompact_keep_recent': '微压缩保留最近工具结果数',
131
+ 'context.project_instructions_enabled': '项目 AGENTS.md 注入开关',
132
+ 'context.project_instructions_max_chars': '项目 AGENTS.md 字符上限',
131
133
  'sessions.max_sessions': '会话保留上限',
132
134
  'sessions.retention_days': '会话保留天数',
133
135
  'shell.default': '默认 shell',
@@ -148,7 +150,9 @@ function getCompletionCopy(language = 'zh') {
148
150
  'policy.safe_mode': '可选:true | false',
149
151
  'policy.allowed_paths': 'JSON 数组,例如 ["D:\\\\shared"]',
150
152
  'policy.allow_dangerous_commands': '可选:true | false',
151
- 'context.prompt_budget_audit': '可选:true | false'
153
+ 'context.prompt_budget_audit': '可选:true | false',
154
+ 'context.project_instructions_enabled': '可选:true | false',
155
+ 'context.project_instructions_max_chars': '建议:8000-12000'
152
156
  },
153
157
  describeSet: (label, hint) => `设置${label}${hint ? `(${hint})` : ''}`,
154
158
  describeGet: (label, hint) => `查看${label}${hint ? `(${hint})` : ''}`,
@@ -237,6 +241,8 @@ function getCompletionCopy(language = 'zh') {
237
241
  'context.prompt_budget_audit': 'prompt budget audit switch',
238
242
  'context.microcompact_enabled': 'micro-compact enabled',
239
243
  'context.microcompact_keep_recent': 'micro-compact keep recent tool results',
244
+ 'context.project_instructions_enabled': 'project AGENTS.md injection switch',
245
+ 'context.project_instructions_max_chars': 'project AGENTS.md character limit',
240
246
  'sessions.max_sessions': 'stored session limit',
241
247
  'sessions.retention_days': 'session retention days',
242
248
  'shell.default': 'default shell',
@@ -257,7 +263,9 @@ function getCompletionCopy(language = 'zh') {
257
263
  'policy.safe_mode': 'options: true | false',
258
264
  'policy.allowed_paths': 'JSON array, for example ["D:\\\\shared"]',
259
265
  'policy.allow_dangerous_commands': 'options: true | false',
260
- 'context.prompt_budget_audit': 'options: true | false'
266
+ 'context.prompt_budget_audit': 'options: true | false',
267
+ 'context.project_instructions_enabled': 'options: true | false',
268
+ 'context.project_instructions_max_chars': 'recommended: 8000-12000'
261
269
  },
262
270
  describeSet: (label, hint) => `set the ${label}${hint ? ` (${hint})` : ''}`,
263
271
  describeGet: (label, hint) => `show the ${label}${hint ? ` (${hint})` : ''}`,
@@ -2287,7 +2295,10 @@ function summarizePromptBudgetAudit(audit) {
2287
2295
  }
2288
2296
 
2289
2297
  function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode, extraSession }) {
2290
- const parentTokens = estimateMessagesTokens(currentSession?.messages || []);
2298
+ const activeParentMessages = Array.isArray(currentSession?.compact?.view) && currentSession.compact.view.length > 0
2299
+ ? currentSession.compact.view
2300
+ : currentSession?.messages || [];
2301
+ const parentTokens = estimateMessagesTokens(activeParentMessages);
2291
2302
  const subTokens = extraSession ? estimateMessagesTokens(extraSession.messages || []) : 0;
2292
2303
  const currentContextTokens = parentTokens + subTokens;
2293
2304
  const maxContextTokens = effectiveMaxContextTokens(config);
@@ -4388,6 +4399,8 @@ export async function createChatRuntime({
4388
4399
  'context.read_file_max_chars',
4389
4400
  'context.microcompact_enabled',
4390
4401
  'context.microcompact_keep_recent',
4402
+ 'context.project_instructions_enabled',
4403
+ 'context.project_instructions_max_chars',
4391
4404
  'sessions.max_sessions',
4392
4405
  'sessions.retention_days',
4393
4406
  'shell.timeout_ms',
@@ -36,7 +36,9 @@ const DEFAULT_CONFIG = {
36
36
  read_file_max_chars: 24000,
37
37
  prompt_budget_audit: false,
38
38
  microcompact_enabled: true,
39
- microcompact_keep_recent: 5
39
+ microcompact_keep_recent: 5,
40
+ project_instructions_enabled: true,
41
+ project_instructions_max_chars: 12000
40
42
  },
41
43
  execution: {
42
44
  mode: 'normal',
@@ -25,6 +25,7 @@ const PROJECT_MARKER_FILES = new Set([
25
25
  const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
26
26
 
27
27
  const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
28
+ const ignoreRulesCache = new BoundedCache({ maxSize: 128, ttlMs: 60 * 1000 });
28
29
  const PROJECT_CONTEXT_MAX_FILES = 6;
29
30
 
30
31
  function clipList(values, max = 32) {
@@ -92,9 +93,24 @@ function gitignorePatternToRegex(pattern) {
92
93
  }
93
94
 
94
95
  async function readIgnoreFileRules(cwd, fileName) {
96
+ const filePath = path.join(cwd, fileName);
97
+ const stat = await safeStat(filePath);
98
+ const cacheKey = `${filePath}:${Number(stat?.mtimeMs || 0)}:${Number(stat?.size || 0)}`;
99
+ if (ignoreRulesCache.has(cacheKey)) return ignoreRulesCache.get(cacheKey);
100
+
101
+ for (const key of ignoreRulesCache.keys()) {
102
+ if (String(key).startsWith(`${filePath}:`) && key !== cacheKey) {
103
+ ignoreRulesCache.delete(key);
104
+ }
105
+ }
106
+
95
107
  try {
96
- const raw = await fs.readFile(path.join(cwd, fileName), 'utf8');
97
- return raw
108
+ if (!stat?.isFile()) {
109
+ ignoreRulesCache.set(cacheKey, []);
110
+ return [];
111
+ }
112
+ const raw = await fs.readFile(filePath, 'utf8');
113
+ const rules = raw
98
114
  .split(/\r?\n/)
99
115
  .map((line) => line.trim())
100
116
  .filter((line) => line && !line.startsWith('#'))
@@ -114,7 +130,10 @@ async function readIgnoreFileRules(cwd, fileName) {
114
130
  };
115
131
  })
116
132
  .filter((rule) => rule.normalized);
133
+ ignoreRulesCache.set(cacheKey, rules);
134
+ return rules;
117
135
  } catch {
136
+ ignoreRulesCache.set(cacheKey, []);
118
137
  return [];
119
138
  }
120
139
  }
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_MAX_CHARS = 12000;
5
+ const CANDIDATE_FILES = [
6
+ 'AGENTS.md',
7
+ path.join('.agents', 'AGENTS.md'),
8
+ path.join('.agents', 'agents.md'),
9
+ path.join('.codemini', 'AGENTS.md'),
10
+ 'CLAUDE.md'
11
+ ];
12
+
13
+ function trimProjectInstructions(value, maxChars = DEFAULT_MAX_CHARS) {
14
+ const text = String(value || '').trim();
15
+ if (!text) return '';
16
+ const limit = Math.max(1000, Number(maxChars) || DEFAULT_MAX_CHARS);
17
+ if (text.length <= limit) return text;
18
+ return `${text.slice(0, limit - 120).trimEnd()}\n\n[Project instructions truncated: keep AGENTS.md concise or move details into linked docs.]`;
19
+ }
20
+
21
+ async function readFirstExistingFile(cwd, candidates = CANDIDATE_FILES) {
22
+ let current = path.resolve(cwd);
23
+ while (true) {
24
+ for (const candidate of candidates) {
25
+ const absolutePath = path.resolve(current, candidate);
26
+ let stat;
27
+ try {
28
+ stat = await fs.stat(absolutePath);
29
+ } catch {
30
+ continue;
31
+ }
32
+ if (!stat?.isFile()) continue;
33
+ const content = await fs.readFile(absolutePath, 'utf8');
34
+ const relativePath = path.relative(cwd, absolutePath) || candidate;
35
+ return {
36
+ path: absolutePath,
37
+ relativePath: relativePath.split(path.sep).join('/'),
38
+ content
39
+ };
40
+ }
41
+ const parent = path.dirname(current);
42
+ if (parent === current) break;
43
+ current = parent;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export async function loadProjectInstructions({
49
+ cwd = process.cwd(),
50
+ config = {},
51
+ maxChars = config?.context?.project_instructions_max_chars
52
+ } = {}) {
53
+ const enabled = config?.context?.project_instructions_enabled !== false;
54
+ if (!enabled) return '';
55
+
56
+ const found = await readFirstExistingFile(cwd);
57
+ if (!found) return '';
58
+
59
+ const body = trimProjectInstructions(found.content, maxChars);
60
+ if (!body) return '';
61
+
62
+ return [
63
+ 'Project Instructions:',
64
+ `Source: ${found.relativePath}`,
65
+ body
66
+ ].join('\n');
67
+ }
68
+
69
+ export function buildDefaultAgentsMd() {
70
+ return `# AGENTS.md
71
+
72
+ This file gives coding agents stable project instructions. Keep it short and use it as a map, not as full documentation.
73
+
74
+ ## Project
75
+
76
+ - Describe what this repository is and the main runtime or product surface.
77
+ - Note required runtime versions and package managers.
78
+
79
+ ## Commands
80
+
81
+ - Install: \`npm install\`
82
+ - Test: \`npm test\`
83
+ - Build: add the project build command here.
84
+
85
+ ## Task Routing
86
+
87
+ - CLI or command behavior: list the entry files here.
88
+ - Runtime behavior: list the core runtime files here.
89
+ - Web UI behavior: list the server, state, and component roots here.
90
+ - Tests: list focused test files for common changes.
91
+
92
+ ## Rules
93
+
94
+ - Use project/file indexes for orientation, then inspect real source files before editing.
95
+ - Keep generated output and build artifacts out of manual edits.
96
+ - Put reusable workflows in skills; put always-needed project facts and routing rules here.
97
+ `;
98
+ }
package/src/core/shell.js CHANGED
@@ -23,6 +23,74 @@ const READY_OUTPUT_PATTERNS = [
23
23
  /\bhttp:\/\/127\.0\.0\.1\b/i,
24
24
  /\bhttp:\/\/localhost\b/i
25
25
  ];
26
+ const INSTALL_COMMAND_PATTERNS = [
27
+ /\b(?:npm|pnpm|yarn|bun)\s+install\b/i,
28
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:ci|i|add)\b/i,
29
+ /\buv\s+pip\s+install\b/i,
30
+ /\bpip\s+install\b/i,
31
+ /\bcargo\s+install\b/i,
32
+ /\bbundle\s+install\b/i,
33
+ /\bcomposer\s+install\b/i
34
+ ];
35
+ const BUILD_COMMAND_RE = /\b(?:build|compile|bundle|pack|transpile)\b/i;
36
+ const TEST_COMMAND_PATTERNS = [
37
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|check|typecheck)\b/i,
38
+ /\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test)\b/i
39
+ ];
40
+ const FRONTEND_SERVICE_PATTERNS = [
41
+ /\bvite\b/i,
42
+ /\bnext\s+dev\b/i,
43
+ /\bnuxt\s+dev\b/i,
44
+ /\bastro\s+dev\b/i,
45
+ /\bremix\s+dev\b/i,
46
+ /\bsvelte-kit\s+dev\b/i,
47
+ /\bwebpack\s+serve\b/i,
48
+ /\bvue-cli-service\s+serve\b/i,
49
+ /\breact-scripts\s+start\b/i,
50
+ /\bstorybook\b/i,
51
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:client|frontend|front-end|web|ui)\b/i,
52
+ /\b(?:client|frontend|front-end|web|ui)\b.*\b(?:dev|start|serve|preview)\b/i
53
+ ];
54
+ const BACKEND_SERVICE_PATTERNS = [
55
+ /\bpython\s+-m\s+http\.server\b/i,
56
+ /\buvicorn\b/i,
57
+ /\bgunicorn\b/i,
58
+ /\bflask\s+run\b/i,
59
+ /\bdjango\s+runserver\b/i,
60
+ /\brails\s+(?:s|server)\b/i,
61
+ /\bmvn(?:w)?\s+spring-boot:run\b/i,
62
+ /\bgradle(?:w)?\s+bootRun\b/i,
63
+ /\bgradle(?:w)?\s+run\b/i,
64
+ /\bjava\b.*\bserver\b/i,
65
+ /\bdotnet\s+run\b/i,
66
+ /\bgo\s+run\b.*\b(server|cmd\/server|main\.go)\b/i,
67
+ /\bnest\s+start\b/i,
68
+ /\bnodemon\b/i,
69
+ /\bts-node-dev\b/i,
70
+ /\bair\b/i,
71
+ /\bphp\s+artisan\s+serve\b/i,
72
+ /\bsymfony\s+server:start\b/i,
73
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:server|api|backend)\b/i,
74
+ /\b(?:server|api|backend)\b.*\b(?:dev|start|serve|preview)\b/i
75
+ ];
76
+ const DATABASE_SERVICE_PATTERNS = [
77
+ /\bpostgres(?:ql)?\b/i,
78
+ /\bmysql\b/i,
79
+ /\bmariadb\b/i,
80
+ /\bmongod\b/i,
81
+ /\bredis-server\b/i,
82
+ /\b(?:docker|docker-compose|docker compose)\s+.*\b(?:db|database|postgres|mysql|mongo|redis)\b/i,
83
+ /\b(?:db|database|postgres|mysql|mongo|redis)\b.*\b(?:start|up|serve|run)\b/i
84
+ ];
85
+ const DOCKER_SERVICE_PATTERNS = [
86
+ /\bdocker\s+compose\s+up\b/i,
87
+ /\bdocker-compose\s+up\b/i,
88
+ /\bdocker\s+run\b/i,
89
+ /\bdocker\s+start\b/i
90
+ ];
91
+ const PACKAGE_SERVICE_RE = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview|watch)\b/i;
92
+ const VITE_OR_SERVE_RE = /\b(?:vite|serve)\b/i;
93
+ const SERVICE_HINT_RE = /\b(?:watch|serve|server|dev|preview)\b/i;
26
94
  const AUTO_STOP_GRACE_MS = 150;
27
95
  const LONG_RUNNING_STARTUP_WINDOW_MS = 1500;
28
96
 
@@ -41,105 +109,43 @@ export function classifyCommandIntent(command) {
41
109
  return { kind: 'generic', longRunning: false };
42
110
  }
43
111
 
44
- if (
45
- /\b(?:npm|pnpm|yarn|bun)\s+install\b/i.test(value) ||
46
- /\b(?:npm|pnpm|yarn|bun)\s+(?:ci|i|add)\b/i.test(value) ||
47
- /\buv\s+pip\s+install\b/i.test(value) ||
48
- /\bpip\s+install\b/i.test(value) ||
49
- /\bcargo\s+install\b/i.test(value) ||
50
- /\bbundle\s+install\b/i.test(value) ||
51
- /\bcomposer\s+install\b/i.test(value)
52
- ) {
112
+ if (matchesAny(value, INSTALL_COMMAND_PATTERNS)) {
53
113
  return { kind: 'install', longRunning: false };
54
114
  }
55
115
 
56
- if (/\b(?:build|compile|bundle|pack|transpile)\b/i.test(value)) {
116
+ if (BUILD_COMMAND_RE.test(value)) {
57
117
  return { kind: 'build', longRunning: false };
58
118
  }
59
119
 
60
- if (
61
- /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|check|typecheck)\b/i.test(value) ||
62
- /\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test)\b/i.test(value)
63
- ) {
120
+ if (matchesAny(value, TEST_COMMAND_PATTERNS)) {
64
121
  return { kind: 'test', longRunning: false };
65
122
  }
66
123
 
67
- const frontendServicePatterns = [
68
- /\bvite\b/i,
69
- /\bnext\s+dev\b/i,
70
- /\bnuxt\s+dev\b/i,
71
- /\bastro\s+dev\b/i,
72
- /\bremix\s+dev\b/i,
73
- /\bsvelte-kit\s+dev\b/i,
74
- /\bwebpack\s+serve\b/i,
75
- /\bvue-cli-service\s+serve\b/i,
76
- /\breact-scripts\s+start\b/i,
77
- /\bstorybook\b/i,
78
- /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:client|frontend|front-end|web|ui)\b/i,
79
- /\b(?:client|frontend|front-end|web|ui)\b.*\b(?:dev|start|serve|preview)\b/i
80
- ];
81
- if (matchesAny(value, frontendServicePatterns)) {
124
+ if (matchesAny(value, FRONTEND_SERVICE_PATTERNS)) {
82
125
  return { kind: 'frontend-service', longRunning: true };
83
126
  }
84
127
 
85
- const backendServicePatterns = [
86
- /\bpython\s+-m\s+http\.server\b/i,
87
- /\buvicorn\b/i,
88
- /\bgunicorn\b/i,
89
- /\bflask\s+run\b/i,
90
- /\bdjango\s+runserver\b/i,
91
- /\brails\s+(?:s|server)\b/i,
92
- /\bmvn(?:w)?\s+spring-boot:run\b/i,
93
- /\bgradle(?:w)?\s+bootRun\b/i,
94
- /\bgradle(?:w)?\s+run\b/i,
95
- /\bjava\b.*\bserver\b/i,
96
- /\bdotnet\s+run\b/i,
97
- /\bgo\s+run\b.*\b(server|cmd\/server|main\.go)\b/i,
98
- /\bnest\s+start\b/i,
99
- /\bnodemon\b/i,
100
- /\bts-node-dev\b/i,
101
- /\bair\b/i,
102
- /\bphp\s+artisan\s+serve\b/i,
103
- /\bsymfony\s+server:start\b/i,
104
- /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:server|api|backend)\b/i,
105
- /\b(?:server|api|backend)\b.*\b(?:dev|start|serve|preview)\b/i
106
- ];
107
- if (matchesAny(value, backendServicePatterns)) {
128
+ if (matchesAny(value, BACKEND_SERVICE_PATTERNS)) {
108
129
  return { kind: 'backend-service', longRunning: true };
109
130
  }
110
131
 
111
- const databaseServicePatterns = [
112
- /\bpostgres(?:ql)?\b/i,
113
- /\bmysql\b/i,
114
- /\bmariadb\b/i,
115
- /\bmongod\b/i,
116
- /\bredis-server\b/i,
117
- /\b(?:docker|docker-compose|docker compose)\s+.*\b(?:db|database|postgres|mysql|mongo|redis)\b/i,
118
- /\b(?:db|database|postgres|mysql|mongo|redis)\b.*\b(?:start|up|serve|run)\b/i
119
- ];
120
- if (matchesAny(value, databaseServicePatterns)) {
132
+ if (matchesAny(value, DATABASE_SERVICE_PATTERNS)) {
121
133
  return { kind: 'database-service', longRunning: true };
122
134
  }
123
135
 
124
- const dockerServicePatterns = [
125
- /\bdocker\s+compose\s+up\b/i,
126
- /\bdocker-compose\s+up\b/i,
127
- /\bdocker\s+run\b/i,
128
- /\bdocker\s+start\b/i
129
- ];
130
- if (matchesAny(value, dockerServicePatterns)) {
136
+ if (matchesAny(value, DOCKER_SERVICE_PATTERNS)) {
131
137
  return { kind: 'docker-service', longRunning: true };
132
138
  }
133
139
 
134
140
  if (
135
- /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview|watch)\b/i.test(value) ||
136
- /\b(?:vite|serve)\b/i.test(value) ||
137
- /\b(?:watch|serve|server|dev|preview)\b/i.test(value)
141
+ PACKAGE_SERVICE_RE.test(value) ||
142
+ VITE_OR_SERVE_RE.test(value) ||
143
+ SERVICE_HINT_RE.test(value)
138
144
  ) {
139
145
  return { kind: 'service', longRunning: true };
140
146
  }
141
147
 
142
- if (/\b(?:watch|serve|server|dev|preview)\b/i.test(value)) {
148
+ if (SERVICE_HINT_RE.test(value)) {
143
149
  return { kind: 'service', longRunning: true };
144
150
  }
145
151
 
@@ -1,4 +1,5 @@
1
1
  import { buildMemorySnapshot } from './memory-prompt.js';
2
+ import { loadProjectInstructions } from './project-instructions.js';
2
3
  import { buildSystemPromptWithReplyLanguage, stripReplyLanguageDirective } from './reply-language.js';
3
4
  import { buildSystemPromptWithSoul } from './soul.js';
4
5
 
@@ -17,6 +18,8 @@ export async function composeSystemPrompt({
17
18
  skillsPrompt = '',
18
19
  memorySnapshot,
19
20
  includeMemory = true,
21
+ projectInstructionsSnippet,
22
+ includeProjectInstructions = true,
20
23
  projectContextSnippet = '',
21
24
  projectContextGuidance = '',
22
25
  extraPrompts = [],
@@ -30,8 +33,15 @@ export async function composeSystemPrompt({
30
33
  : includeMemory
31
34
  ? await buildMemorySnapshot({ config, workspaceRoot }).catch(() => '')
32
35
  : '';
36
+ const projectInstructionsPrompt = projectInstructionsSnippet !== undefined
37
+ ? projectInstructionsSnippet
38
+ : includeProjectInstructions
39
+ ? await loadProjectInstructions({ cwd: workspaceRoot, config }).catch(() => '')
40
+ : '';
41
+ const hasProjectInstructions = /\bProject Instructions:\s*\n/i.test(shellAndSoul);
33
42
  const body = joinPromptParts([
34
43
  shellAndSoul,
44
+ hasProjectInstructions ? '' : projectInstructionsPrompt,
35
45
  skillsPrompt,
36
46
  memoryPrompt,
37
47
  projectContextSnippet,
@@ -1 +0,0 @@
1
- import{i as e}from"./index-C4tKT3v4.js";export{e as Mermaid};