codemini-cli 0.5.8 → 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.
package/package.json CHANGED
@@ -1,67 +1,67 @@
1
- {
2
- "name": "codemini-cli",
3
- "version": "0.5.8",
4
- "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
- "keywords": [
6
- "cli",
7
- "ai",
8
- "coding-assistant",
9
- "developer-tools",
10
- "terminal",
11
- "windows",
12
- "powershell"
13
- ],
14
- "type": "module",
15
- "homepage": "https://github.com/havingautism/Codemini-CLI#readme",
16
- "bugs": {
17
- "url": "https://github.com/havingautism/Codemini-CLI/issues"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/havingautism/Codemini-CLI.git"
22
- },
23
- "bin": {
24
- "codemini": "bin/coder.js",
25
- "coder": "bin/coder.js"
26
- },
27
- "scripts": {
28
- "start": "node bin/coder.js",
29
- "test": "node --test tests/*.test.js",
30
- "build:web": "npm install --prefix codemini-web && npm run build --prefix codemini-web",
31
- "prepack": "npm run build:web",
32
- "pack:offline": "npm pack",
33
- "bump:patch": "npm version patch --no-git-tag-version",
34
- "bump:minor": "npm version minor --no-git-tag-version",
35
- "bump:major": "npm version major --no-git-tag-version"
36
- },
37
- "files": [
38
- "bin",
39
- "src",
40
- "codemini-web/server.js",
41
- "codemini-web/lib",
42
- "codemini-web/dist",
43
- "codemini-web/codemini_logo.png",
44
- "souls",
45
- "templates",
46
- "skills",
47
- "README.md",
48
- "OPERATIONS.md",
49
- "deployment.md"
50
- ],
51
- "engines": {
52
- "node": ">=22"
53
- },
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "dependencies": {
58
- "@cursorless/tree-sitter-wasms": "^0.8.1",
59
- "cheerio": "^1.1.2",
60
- "cli-truncate": "^6.0.0",
61
- "ink": "^7.0.0",
62
- "react": "^19.2.5",
63
- "strip-ansi": "^7.2.0",
64
- "web-tree-sitter": "^0.26.8"
65
- },
66
- "license": "MIT"
67
- }
1
+ {
2
+ "name": "codemini-cli",
3
+ "version": "0.5.10",
4
+ "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
+ "keywords": [
6
+ "cli",
7
+ "ai",
8
+ "coding-assistant",
9
+ "developer-tools",
10
+ "terminal",
11
+ "windows",
12
+ "powershell"
13
+ ],
14
+ "type": "module",
15
+ "homepage": "https://github.com/havingautism/Codemini-CLI#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/havingautism/Codemini-CLI/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/havingautism/Codemini-CLI.git"
22
+ },
23
+ "bin": {
24
+ "codemini": "bin/coder.js",
25
+ "coder": "bin/coder.js"
26
+ },
27
+ "scripts": {
28
+ "start": "node bin/coder.js",
29
+ "test": "node --test tests/*.test.js",
30
+ "build:web": "npm install --prefix codemini-web && npm run build --prefix codemini-web",
31
+ "prepack": "npm run build:web",
32
+ "pack:offline": "npm pack",
33
+ "bump:patch": "npm version patch --no-git-tag-version",
34
+ "bump:minor": "npm version minor --no-git-tag-version",
35
+ "bump:major": "npm version major --no-git-tag-version"
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "src",
40
+ "codemini-web/server.js",
41
+ "codemini-web/lib",
42
+ "codemini-web/dist",
43
+ "codemini-web/codemini_logo.png",
44
+ "souls",
45
+ "templates",
46
+ "skills",
47
+ "README.md",
48
+ "OPERATIONS.md",
49
+ "deployment.md"
50
+ ],
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "dependencies": {
58
+ "@cursorless/tree-sitter-wasms": "^0.8.1",
59
+ "cheerio": "^1.1.2",
60
+ "cli-truncate": "^6.0.0",
61
+ "ink": "^7.0.0",
62
+ "react": "^19.2.5",
63
+ "strip-ansi": "^7.2.0",
64
+ "web-tree-sitter": "^0.26.8"
65
+ },
66
+ "license": "MIT"
67
+ }
@@ -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.
@@ -0,0 +1,40 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "superpowers-lite": {
5
+ "description": "Default concise workflow for coding tasks; keep context tight, choose the right route, and verify before claiming success.",
6
+ "mode": "always",
7
+ "triggers": [],
8
+ "enabled": true,
9
+ "priority": 100
10
+ },
11
+ "brainstorm": {
12
+ "description": "Use when a feature or behavior request has multiple reasonable approaches and the missing piece is user preference, tradeoff choice, or key constraint.",
13
+ "mode": "agent_requested",
14
+ "triggers": [],
15
+ "enabled": true,
16
+ "priority": 70
17
+ },
18
+ "writing-plans": {
19
+ "description": "Use when you have a clear goal or approved design for a non-trivial task, before touching code.",
20
+ "mode": "agent_requested",
21
+ "triggers": [],
22
+ "enabled": true,
23
+ "priority": 75
24
+ },
25
+ "grill-me": {
26
+ "description": "Use when the user explicitly asks to pressure-test a plan, architecture choice, PR, launch, or product idea.",
27
+ "mode": "agent_requested",
28
+ "triggers": [],
29
+ "enabled": true,
30
+ "priority": 65
31
+ },
32
+ "project-requirements": {
33
+ "description": "Create a project requirements report with repository inspection and structured output artifacts.",
34
+ "mode": "manual",
35
+ "triggers": [],
36
+ "enabled": true,
37
+ "priority": 50
38
+ }
39
+ }
40
+ }
@@ -77,9 +77,15 @@ export async function listSkillEntries({ scope = 'all', cwd = process.cwd() } =
77
77
  name: command.name,
78
78
  version: command.metadata?.version || '0.0.0',
79
79
  description: command.metadata?.description || '',
80
+ mode: command.metadata?.mode || '',
81
+ triggers: Array.isArray(command.metadata?.triggers) ? command.metadata.triggers : [],
80
82
  scope: itemScope,
81
83
  path: command.path,
82
- enabled: itemScope === 'builtin' ? true : config.skills?.enabled?.[command.name] !== false
84
+ enabled: command.metadata?.enabled === false
85
+ ? false
86
+ : itemScope === 'builtin'
87
+ ? true
88
+ : config.skills?.enabled?.[command.name] !== false
83
89
  });
84
90
  }
85
91
  return entries.sort((a, b) => `${a.scope}:${a.name}`.localeCompare(`${b.scope}:${b.name}`));
@@ -93,14 +99,19 @@ async function readSkillMeta(name, { scope = 'all', cwd = process.cwd() } = {})
93
99
  }
94
100
  const dir = path.dirname(found.path);
95
101
  const manifestPath = path.join(dir, 'manifest.json');
102
+ const catalogPath = path.join(path.dirname(dir), 'codemini.skills.json');
96
103
  let manifest = null;
97
104
  try {
98
- manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
105
+ const catalog = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
106
+ manifest = catalog?.skills?.[found.name] || null;
99
107
  } catch {
100
- manifest = null;
108
+ try {
109
+ manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
110
+ } catch {
111
+ manifest = null;
112
+ }
101
113
  }
102
- const entryFile = manifest?.entry || 'SKILL.md';
103
- const skillPath = found.path || path.join(dir, entryFile);
114
+ const skillPath = found.path || path.join(dir, 'SKILL.md');
104
115
  try {
105
116
  const content = await fs.readFile(skillPath, 'utf8');
106
117
  const firstLines = content.split('\n').slice(0, 20).join('\n');
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',
@@ -136,6 +138,7 @@ function getCompletionCopy(language = 'zh') {
136
138
  'soul.preset': 'soul 预设',
137
139
  'soul.custom_path': '自定义 soul 路径',
138
140
  'policy.safe_mode': '安全模式开关',
141
+ 'policy.allowed_paths': '安全模式目录白名单',
139
142
  'policy.allow_dangerous_commands': '危险命令开关'
140
143
  },
141
144
  optionHints: {
@@ -145,8 +148,11 @@ function getCompletionCopy(language = 'zh') {
145
148
  'execution.mode': '可选:auto | normal | plan',
146
149
  'shell.default': '常用:bash | powershell',
147
150
  'policy.safe_mode': '可选:true | false',
151
+ 'policy.allowed_paths': 'JSON 数组,例如 ["D:\\\\shared"]',
148
152
  'policy.allow_dangerous_commands': '可选:true | false',
149
- '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'
150
156
  },
151
157
  describeSet: (label, hint) => `设置${label}${hint ? `(${hint})` : ''}`,
152
158
  describeGet: (label, hint) => `查看${label}${hint ? `(${hint})` : ''}`,
@@ -235,6 +241,8 @@ function getCompletionCopy(language = 'zh') {
235
241
  'context.prompt_budget_audit': 'prompt budget audit switch',
236
242
  'context.microcompact_enabled': 'micro-compact enabled',
237
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',
238
246
  'sessions.max_sessions': 'stored session limit',
239
247
  'sessions.retention_days': 'session retention days',
240
248
  'shell.default': 'default shell',
@@ -243,6 +251,7 @@ function getCompletionCopy(language = 'zh') {
243
251
  'soul.preset': 'soul preset',
244
252
  'soul.custom_path': 'custom soul prompt path',
245
253
  'policy.safe_mode': 'safe mode switch',
254
+ 'policy.allowed_paths': 'safe-mode allowed path roots',
246
255
  'policy.allow_dangerous_commands': 'dangerous command allowance'
247
256
  },
248
257
  optionHints: {
@@ -252,8 +261,11 @@ function getCompletionCopy(language = 'zh') {
252
261
  'execution.mode': 'options: auto | normal | plan',
253
262
  'shell.default': 'common: bash | powershell',
254
263
  'policy.safe_mode': 'options: true | false',
264
+ 'policy.allowed_paths': 'JSON array, for example ["D:\\\\shared"]',
255
265
  'policy.allow_dangerous_commands': 'options: true | false',
256
- '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'
257
269
  },
258
270
  describeSet: (label, hint) => `set the ${label}${hint ? ` (${hint})` : ''}`,
259
271
  describeGet: (label, hint) => `show the ${label}${hint ? ` (${hint})` : ''}`,
@@ -1265,6 +1277,7 @@ function isBundledSkillCommand(command) {
1265
1277
  }
1266
1278
 
1267
1279
  function isSkillEnabled(config, name, command = null) {
1280
+ if (command?.metadata?.enabled === false) return false;
1268
1281
  if (isBundledSkillCommand(command)) return true;
1269
1282
  return config.skills?.enabled?.[name] !== false;
1270
1283
  }
@@ -2282,7 +2295,10 @@ function summarizePromptBudgetAudit(audit) {
2282
2295
  }
2283
2296
 
2284
2297
  function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode, extraSession }) {
2285
- 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);
2286
2302
  const subTokens = extraSession ? estimateMessagesTokens(extraSession.messages || []) : 0;
2287
2303
  const currentContextTokens = parentTokens + subTokens;
2288
2304
  const maxContextTokens = effectiveMaxContextTokens(config);
@@ -2301,6 +2317,9 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2301
2317
  maxContextTokens,
2302
2318
  pendingPlanApproval: planState?.status === 'pending_approval'
2303
2319
  ? { goal: planState.goal, summary: planState.finalSummary || planState.summary, filePath: planState.filePath, steps: planState.steps || [] }
2320
+ : null,
2321
+ pendingReflectSkill: planState?.status === 'pending_reflect_skill'
2322
+ ? buildPendingReflectSkillSnapshot(planState)
2304
2323
  : null
2305
2324
  };
2306
2325
  Object.defineProperties(snapshot, {
@@ -2314,11 +2333,6 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2314
2333
  enumerable: false,
2315
2334
  writable: false
2316
2335
  },
2317
- pendingReflectSkill: {
2318
- value: currentSession?.planState?.status === 'pending_reflect_skill',
2319
- enumerable: false,
2320
- writable: false
2321
- },
2322
2336
  replyLanguage: {
2323
2337
  value: getReplyLanguage(config),
2324
2338
  enumerable: false,
@@ -2329,7 +2343,6 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2329
2343
  ...snapshot,
2330
2344
  currentContextTokens,
2331
2345
  contextUsagePct,
2332
- pendingReflectSkill: currentSession?.planState?.status === 'pending_reflect_skill',
2333
2346
  replyLanguage: getReplyLanguage(config)
2334
2347
  }),
2335
2348
  enumerable: false,
@@ -2513,6 +2526,21 @@ function buildPendingReflectSkillMessage(reflectState) {
2513
2526
  return lines.join('\n');
2514
2527
  }
2515
2528
 
2529
+ function buildPendingReflectSkillSnapshot(reflectState) {
2530
+ const candidates = Array.isArray(reflectState?.candidates) ? reflectState.candidates : [];
2531
+ const candidate = candidates[0] || null;
2532
+ if (!candidate) return null;
2533
+ return {
2534
+ scope: reflectState?.targetScope || 'project',
2535
+ request: reflectState?.request || '',
2536
+ name: candidate.name || '',
2537
+ description: candidate.description || '',
2538
+ confidence: Number(candidate.confidence ?? 0.75),
2539
+ targetPath: candidate.targetPath || '',
2540
+ content: candidate.content || ''
2541
+ };
2542
+ }
2543
+
2516
2544
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
2517
2545
  const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
2518
2546
  const lines = [
@@ -2740,8 +2768,10 @@ async function askModel({
2740
2768
  }
2741
2769
  }
2742
2770
 
2771
+ const shouldGenerateTitle = text
2772
+ ? !session.messages.some((msg) => msg?.role === 'user')
2773
+ : false;
2743
2774
  if (text) {
2744
- const shouldGenerateTitle = !session.messages.some((msg) => msg?.role === 'user');
2745
2775
  const modelExtra =
2746
2776
  typeof modelText === 'string' && modelText && modelText !== text ? { model_content: modelText } : {};
2747
2777
  const userMessage = stampedMessage('user', text, modelExtra);
@@ -2773,7 +2803,16 @@ async function askModel({
2773
2803
 
2774
2804
  const { definitions, handlers, formatters, deferredDefinitions, dispose: disposeTools } = getBuiltinTools({
2775
2805
  workspaceRoot: process.cwd(),
2776
- config,
2806
+ config: {
2807
+ ...config,
2808
+ policy: {
2809
+ ...(config.policy || {}),
2810
+ allowed_paths: [
2811
+ ...(Array.isArray(config.policy?.allowed_paths) ? config.policy.allowed_paths : []),
2812
+ path.join(getSessionsDir(), String(session.id))
2813
+ ]
2814
+ }
2815
+ },
2777
2816
  sessionId: session.id,
2778
2817
  onSystemEvent: onAgentEvent,
2779
2818
  getTodos: () => normalizeTodos(session.todos),
@@ -3004,7 +3043,7 @@ async function askModel({
3004
3043
  await flushScheduledSave();
3005
3044
  await saveSession(session);
3006
3045
  // Generate a better title asynchronously after saving
3007
- if (shouldReplaceSessionTitle(session.title)) {
3046
+ if (shouldGenerateTitle) {
3008
3047
  const titleSessionId = session.id;
3009
3048
  generateSessionTitle({
3010
3049
  userText: text,
@@ -4242,11 +4281,22 @@ export async function createChatRuntime({
4242
4281
  if (hasPendingPlanApproval(currentSession)) {
4243
4282
  executionMode = 'plan';
4244
4283
  }
4284
+ let compactState = null;
4285
+ const normalizeCompactThreshold = (value, fallback = 60) => {
4286
+ const num = Number(value);
4287
+ if (!Number.isFinite(num)) return fallback;
4288
+ return Math.min(95, Math.max(50, num));
4289
+ };
4290
+ const syncCompactStateFromConfig = () => {
4291
+ if (!compactState) return;
4292
+ compactState.threshold = normalizeCompactThreshold(config.context?.preflight_trigger_pct, 60);
4293
+ };
4245
4294
  const syncRuntimeFromConfig = async ({ model: nextModel } = {}) => {
4246
4295
  const configuredMode = String(config.execution?.mode || 'normal');
4247
4296
  executionMode = hasPendingPlanApproval(currentSession)
4248
4297
  ? 'plan'
4249
4298
  : (['normal', 'auto', 'plan'].includes(configuredMode) ? configuredMode : 'normal');
4299
+ syncCompactStateFromConfig();
4250
4300
 
4251
4301
  const resolvedModel = String(nextModel || '').trim();
4252
4302
  if (resolvedModel) {
@@ -4269,10 +4319,10 @@ export async function createChatRuntime({
4269
4319
  // Set up tool result store under session directory
4270
4320
  const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
4271
4321
  setResultDir(sessionResultsDir);
4272
- const compactState = {
4322
+ compactState = {
4273
4323
  backupMessages: null,
4274
4324
  autoEnabled: true,
4275
- threshold: 60,
4325
+ threshold: normalizeCompactThreshold(config.context?.preflight_trigger_pct, 60),
4276
4326
  mode: 'conservative'
4277
4327
  };
4278
4328
  let compactedForModel = currentSession.compact?.view || null;
@@ -4349,6 +4399,8 @@ export async function createChatRuntime({
4349
4399
  'context.read_file_max_chars',
4350
4400
  'context.microcompact_enabled',
4351
4401
  'context.microcompact_keep_recent',
4402
+ 'context.project_instructions_enabled',
4403
+ 'context.project_instructions_max_chars',
4352
4404
  'sessions.max_sessions',
4353
4405
  'sessions.retention_days',
4354
4406
  'shell.timeout_ms',
@@ -4356,6 +4408,7 @@ export async function createChatRuntime({
4356
4408
  'soul.preset',
4357
4409
  'soul.custom_path',
4358
4410
  'policy.safe_mode',
4411
+ 'policy.allowed_paths',
4359
4412
  'policy.allow_dangerous_commands'
4360
4413
  ];
4361
4414
 
@@ -4777,6 +4830,11 @@ export async function createChatRuntime({
4777
4830
  };
4778
4831
 
4779
4832
  const persistAssistantExchange = async (userText, assistantText, { includeUser = true, extra = {} } = {}) => {
4833
+ const priorUserCount = currentSession.messages.filter((msg) => msg?.role === 'user').length;
4834
+ const priorAssistantCount = currentSession.messages.filter((msg) => msg?.role === 'assistant').length;
4835
+ const shouldGenerateTitle =
4836
+ (includeUser && userText && priorUserCount === 0) ||
4837
+ (!includeUser && userText && priorUserCount === 1 && priorAssistantCount === 0);
4780
4838
  if (includeUser && userText) {
4781
4839
  appendSessionMessage(stampedMessage('user', userText));
4782
4840
  }
@@ -4787,7 +4845,7 @@ export async function createChatRuntime({
4787
4845
  currentSession.mode = executionMode || config.execution?.mode || 'normal';
4788
4846
  await saveSession(currentSession);
4789
4847
  // Generate a better title asynchronously after saving
4790
- if (shouldReplaceSessionTitle(currentSession.title)) {
4848
+ if (shouldGenerateTitle || shouldReplaceSessionTitle(currentSession.title)) {
4791
4849
  const titleSessionId = currentSession.id;
4792
4850
  generateSessionTitle({
4793
4851
  userText,
@@ -5115,6 +5173,7 @@ export async function createChatRuntime({
5115
5173
  });
5116
5174
  currentSession.planState = null;
5117
5175
  executionMode = 'auto';
5176
+ if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5118
5177
  await reloadCommandsAndSkills();
5119
5178
  const text = `Reflect skill written and loaded: /${written.draft.name}\nPath: ${written.filePath}`;
5120
5179
  await persistLocalExchange(line, text, { includeUser: false });
@@ -5172,6 +5231,12 @@ export async function createChatRuntime({
5172
5231
  workspaceRoot: process.cwd()
5173
5232
  })
5174
5233
  };
5234
+ if (onAgentEvent) {
5235
+ onAgentEvent({
5236
+ type: 'reflect:pending_approval',
5237
+ draft: buildPendingReflectSkillSnapshot(currentSession.planState)
5238
+ });
5239
+ }
5175
5240
  const text = `Reflect skill draft revised.\n${buildPendingReflectSkillMessage(currentSession.planState)}`;
5176
5241
  await persistLocalExchange(line, text);
5177
5242
  return { type: 'system', text };
@@ -5209,6 +5274,7 @@ export async function createChatRuntime({
5209
5274
  if (hasPendingReflectSkill(currentSession)) {
5210
5275
  currentSession.planState = null;
5211
5276
  executionMode = 'auto';
5277
+ if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5212
5278
  const text = 'Reflect skill draft discarded.';
5213
5279
  await persistLocalExchange(line, text, { includeUser: false });
5214
5280
  return { type: 'system', text };
@@ -5668,6 +5734,12 @@ export async function createChatRuntime({
5668
5734
  request: parsedReflect.request,
5669
5735
  candidates
5670
5736
  };
5737
+ if (onAgentEvent) {
5738
+ onAgentEvent({
5739
+ type: 'reflect:pending_approval',
5740
+ draft: buildPendingReflectSkillSnapshot(currentSession.planState)
5741
+ });
5742
+ }
5671
5743
  const text = buildPendingReflectSkillMessage(currentSession.planState);
5672
5744
  await persistLocalExchange(line, text);
5673
5745
  return { type: 'system', text };
@@ -5735,7 +5807,7 @@ export async function createChatRuntime({
5735
5807
  await resetConfig();
5736
5808
  config = await loadConfig();
5737
5809
  await syncRuntimeFromConfig({ model: resolveDefaultModel(config) });
5738
- compactState.threshold = 60;
5810
+ syncCompactStateFromConfig();
5739
5811
  compactState.mode = 'conservative';
5740
5812
  compactState.autoEnabled = true;
5741
5813
  const text = 'Config reset complete';