codemini-cli 0.3.2 → 0.3.3

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.
@@ -13,13 +13,6 @@ import { listSessions, loadSession, pruneSessions, saveSession } from './session
13
13
  import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
14
14
  import { evaluateCommandPolicy } from './command-policy.js';
15
15
  import { appendInputHistory, loadInputHistory } from './input-history-store.js';
16
- import {
17
- clearTasks,
18
- createTasks,
19
- deleteTasks,
20
- loadTasks,
21
- updateTask
22
- } from './task-store.js';
23
16
  import { createCheckpoint, listCheckpoints, loadCheckpoint } from './checkpoint-store.js';
24
17
  import {
25
18
  compactMessagesLocally,
@@ -32,6 +25,7 @@ import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSess
32
25
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
33
26
  import { buildMemorySnapshot } from './memory-prompt.js';
34
27
  import { forgetMemory, listMemories, searchMemories } from './memory-store.js';
28
+ import { countActiveTodos, normalizeTodos } from './todo-state.js';
35
29
 
36
30
  function toOpenAIMessages(sessionMessages) {
37
31
  const mapped = [];
@@ -143,7 +137,6 @@ function getCompletionCopy(language = 'zh') {
143
137
  status: '查看运行状态(mode/model/session)',
144
138
  mode: '设置执行模式:normal|auto|plan',
145
139
  compact: '压缩消息上下文',
146
- tasks: '任务面板管理',
147
140
  checkpoint: '创建/查看/加载检查点',
148
141
  spec: '在 .codemini/specs 中创建 spec',
149
142
  plan: '在 .codemini/plans 中创建实施计划',
@@ -158,7 +151,6 @@ function getCompletionCopy(language = 'zh') {
158
151
  configCommand: '配置命令',
159
152
  historyCommand: '历史会话命令',
160
153
  modeCommand: '切换执行模式',
161
- taskCommand: '任务面板命令',
162
154
  checkpointCommand: '检查点命令',
163
155
  specCommand: '创建 spec 文件',
164
156
  planCommand: '规划命令',
@@ -232,7 +224,6 @@ function getCompletionCopy(language = 'zh') {
232
224
  status: 'show runtime status (mode/model/session)',
233
225
  mode: 'set execution mode: normal|auto|plan',
234
226
  compact: 'compress message context',
235
- tasks: 'task board management',
236
227
  checkpoint: 'create/list/load conversation checkpoints',
237
228
  spec: 'create a spec markdown file in .codemini/specs',
238
229
  plan: 'create an implementation plan markdown file in .codemini/plans',
@@ -247,7 +238,6 @@ function getCompletionCopy(language = 'zh') {
247
238
  configCommand: 'config command',
248
239
  historyCommand: 'history command',
249
240
  modeCommand: 'switch execution mode',
250
- taskCommand: 'task board command',
251
241
  checkpointCommand: 'checkpoint command',
252
242
  specCommand: 'create a spec file',
253
243
  planCommand: 'planning command',
@@ -276,7 +266,7 @@ const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
276
266
  const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
277
267
  const SUB_AGENT_EVIDENCE_MAX_ITEMS = 3;
278
268
  const SUB_AGENT_HANDOFF_MAX_ITEMS = 6;
279
- function getSubAgentRolePrompt(role) {
269
+ export function getSubAgentRolePrompt(role) {
280
270
  if (role === 'planner') {
281
271
  return 'You are a planning sub-agent. Produce a concrete implementation plan with risks and verification.';
282
272
  }
@@ -315,7 +305,11 @@ function getSubAgentRolePrompt(role) {
315
305
  '- <single best next step>'
316
306
  ].join('\n');
317
307
  }
318
- return 'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.';
308
+ return [
309
+ 'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.',
310
+ 'Stop when: you have produced the code change and verified it compiles/passes basic checks.',
311
+ 'If blocked: report what blocked you and what you tried, then stop.'
312
+ ].join('\n');
319
313
  }
320
314
 
321
315
  function trimInlineText(value, maxLen = 220) {
@@ -1753,14 +1747,19 @@ async function askModel({
1753
1747
 
1754
1748
  const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
1755
1749
  const effectiveSystemPrompt = projectContextSnippet
1756
- ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Query the project index before broad globs or reading many files, then use targeted reads for fresh verification.`
1750
+ ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance and verify important details with fresh reads when needed.`
1757
1751
  : systemPrompt;
1758
1752
 
1759
1753
  const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
1760
1754
  workspaceRoot: process.cwd(),
1761
1755
  config,
1762
1756
  sessionId: session.id,
1763
- onSystemEvent: onAgentEvent
1757
+ onSystemEvent: onAgentEvent,
1758
+ getTodos: () => normalizeTodos(session.todos),
1759
+ onTodosUpdate: (todos) => {
1760
+ session.todos = normalizeTodos(todos);
1761
+ scheduleSessionSave();
1762
+ }
1764
1763
  });
1765
1764
 
1766
1765
  let activeAssistantIndex = -1;
@@ -2122,6 +2121,17 @@ export async function createChatRuntime({
2122
2121
  summary: initialIndex.summary
2123
2122
  });
2124
2123
  }
2124
+ const initialTodos = normalizeTodos(session?.todos);
2125
+ if (initialTodos.length > 0) {
2126
+ startupEvents.push({
2127
+ type: 'tool',
2128
+ id: `startup-todos-${String(session?.id || 'session')}`,
2129
+ name: 'update_todos',
2130
+ status: 'done',
2131
+ arguments: { todos: initialTodos },
2132
+ summary: `${initialTodos.length} todo item(s)`
2133
+ });
2134
+ }
2125
2135
  let currentSession = session;
2126
2136
  let config = initialConfig;
2127
2137
  const baseSystemPrompt = systemPrompt;
@@ -2211,7 +2221,6 @@ export async function createChatRuntime({
2211
2221
  '/memory',
2212
2222
  '/mode',
2213
2223
  '/plan',
2214
- '/tasks',
2215
2224
  '/history',
2216
2225
  '/checkpoint',
2217
2226
  '/agents',
@@ -2230,7 +2239,6 @@ export async function createChatRuntime({
2230
2239
  { name: 'status', description: completionCopy.commands.status },
2231
2240
  { name: 'mode', description: completionCopy.commands.mode },
2232
2241
  { name: 'compact', description: completionCopy.commands.compact },
2233
- { name: 'tasks', description: completionCopy.commands.tasks },
2234
2242
  { name: 'checkpoint', description: completionCopy.commands.checkpoint },
2235
2243
  { name: 'spec', description: completionCopy.commands.spec },
2236
2244
  { name: 'plan', description: completionCopy.commands.plan },
@@ -2275,7 +2283,6 @@ export async function createChatRuntime({
2275
2283
  const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
2276
2284
  const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
2277
2285
  const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
2278
- const taskTemplates = ['/tasks', '/tasks add <title>', '/tasks start <id>', '/tasks done <id>', '/tasks remove <id>', '/tasks clear'];
2279
2286
  const checkpointTemplates = [
2280
2287
  '/checkpoint create <name>',
2281
2288
  '/checkpoint list',
@@ -2292,7 +2299,6 @@ export async function createChatRuntime({
2292
2299
  ...memoryTemplates,
2293
2300
  ...historyTemplates,
2294
2301
  ...modeTemplates,
2295
- ...taskTemplates,
2296
2302
  ...checkpointTemplates,
2297
2303
  ...specTemplates,
2298
2304
  ...planTemplates,
@@ -2338,7 +2344,6 @@ export async function createChatRuntime({
2338
2344
  'memory',
2339
2345
  'compact',
2340
2346
  'mode',
2341
- 'tasks',
2342
2347
  'checkpoint',
2343
2348
  'plan',
2344
2349
  'agents',
@@ -2358,7 +2363,6 @@ export async function createChatRuntime({
2358
2363
  for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
2359
2364
  for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
2360
2365
  for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
2361
- for (const template of taskTemplates) registerSuggestion(template, completionCopy.generic.taskCommand);
2362
2366
  for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
2363
2367
  for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
2364
2368
  for (const template of planTemplates) {
@@ -2460,15 +2464,6 @@ export async function createChatRuntime({
2460
2464
  }
2461
2465
  return materializeSuggestions(modeTemplates);
2462
2466
  }
2463
- if (commandPart === 'tasks') {
2464
- if (tokens.length <= 2 && !hasTrailingSpace) {
2465
- const sub = tokens[1] || '';
2466
- return ['add', 'start', 'done', 'remove', 'rm', 'clear']
2467
- .filter((s) => s.startsWith(sub))
2468
- .map((s) => registerSuggestion(`/tasks ${s}`, completionCopy.generic.taskCommand));
2469
- }
2470
- return materializeSuggestions(taskTemplates);
2471
- }
2472
2467
  if (commandPart === 'checkpoint') {
2473
2468
  if (tokens.length <= 2 && !hasTrailingSpace) {
2474
2469
  const sub = tokens[1] || '';
@@ -2614,7 +2609,7 @@ export async function createChatRuntime({
2614
2609
  workspaceRoot: process.cwd()
2615
2610
  }).catch(() => '');
2616
2611
  const memoryGuide =
2617
- 'Persistent memory is for durable preferences, project conventions, and stable workflow knowledge. Use fresh file reads when the code can verify details. Only write memory when the fact is likely to matter in future sessions, is stable over time, and is not sensitive. Do not store temporary task details, speculative guesses, or secrets. Choose remember_user for user preferences, communication habits, and long-term constraints. Choose remember_global for reusable workflow knowledge that helps across many projects. Choose remember_project for repository-specific conventions, architecture notes, important modules, and local workflow expectations. When memory includes command names, file paths, identifiers, or exact wording, preserve those tokens exactly instead of paraphrasing them.';
2612
+ 'Persistent memory stores durable preferences and stable workflow knowledge. Verify changeable details from files, and only write memory for future-useful, non-sensitive facts.';
2618
2613
  return [soulPrompt, memorySnapshot, memoryGuide].filter(Boolean).join('\n\n');
2619
2614
  };
2620
2615
 
@@ -2633,7 +2628,6 @@ export async function createChatRuntime({
2633
2628
  'commands',
2634
2629
  'status',
2635
2630
  'mode',
2636
- 'tasks',
2637
2631
  'checkpoint',
2638
2632
  'history',
2639
2633
  'memory',
@@ -2664,14 +2658,14 @@ export async function createChatRuntime({
2664
2658
  if (parsedInput.command === 'help') {
2665
2659
  return {
2666
2660
  type: 'system',
2667
- text: 'Commands: /help /exit /commands /status /mode /compact /tasks /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
2661
+ text: 'Commands: /help /exit /commands /status /mode /compact /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
2668
2662
  };
2669
2663
  }
2670
2664
  if (parsedInput.command === 'status') {
2671
- const taskCount = (await loadTasks(process.cwd(), currentSession.id)).length;
2665
+ const todoCount = countActiveTodos(currentSession.todos);
2672
2666
  return {
2673
2667
  type: 'system',
2674
- text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | tasks=${taskCount}`
2668
+ text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
2675
2669
  };
2676
2670
  }
2677
2671
  if (parsedInput.command === 'mode') {
@@ -2689,74 +2683,15 @@ export async function createChatRuntime({
2689
2683
  await persistLocalExchange(line, text);
2690
2684
  return { type: 'system', text };
2691
2685
  }
2692
- if (parsedInput.command === 'tasks') {
2693
- const sub = (parsedInput.args[0] || '').trim().toLowerCase();
2694
- if (!sub) {
2695
- const tasks = await loadTasks(process.cwd(), currentSession.id);
2696
- if (tasks.length === 0) return { type: 'system', text: 'No tasks' };
2697
- const rows = tasks.map((t, idx) => `${idx + 1}. ${t.id} | ${t.status} | ${t.title}`);
2698
- return { type: 'system', text: rows.join('\n') };
2699
- }
2700
- if (sub === 'add') {
2701
- const title = parsedInput.args.slice(1).join(' ').trim();
2702
- if (!title) return { type: 'system', text: 'Usage: /tasks add <title>' };
2703
- const created = await createTasks([{ title }], process.cwd(), currentSession.id);
2704
- const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
2705
- await persistLocalExchange(line, text);
2706
- return { type: 'system', text };
2707
- }
2708
- if (sub === 'start') {
2709
- const id = parsedInput.args[1];
2710
- if (!id) return { type: 'system', text: 'Usage: /tasks start <id>' };
2711
- const updated = await updateTask(id, { status: 'in_progress' }, process.cwd(), currentSession.id);
2712
- if (!updated) return { type: 'system', text: `Task not found: ${id}` };
2713
- const text = `Task in progress: ${id}`;
2714
- await persistLocalExchange(line, text);
2715
- return { type: 'system', text };
2716
- }
2717
- if (sub === 'done') {
2718
- const id = parsedInput.args[1];
2719
- if (!id) return { type: 'system', text: 'Usage: /tasks done <id>' };
2720
- const updated = await updateTask(id, { status: 'completed' }, process.cwd(), currentSession.id);
2721
- if (!updated) return { type: 'system', text: `Task not found: ${id}` };
2722
- const text = `Task completed: ${id}`;
2723
- await persistLocalExchange(line, text);
2724
- return { type: 'system', text };
2725
- }
2726
- if (sub === 'remove' || sub === 'rm') {
2727
- const id = parsedInput.args[1];
2728
- if (!id) return { type: 'system', text: 'Usage: /tasks remove <id>' };
2729
- const result = await deleteTasks([id], process.cwd(), currentSession.id);
2730
- const text = `Removed=${result.removed}, Remaining=${result.remaining}`;
2731
- await persistLocalExchange(line, text);
2732
- return { type: 'system', text };
2733
- }
2734
- if (sub === 'clear') {
2735
- await clearTasks(process.cwd(), currentSession.id);
2736
- const text = 'All tasks cleared';
2737
- await persistLocalExchange(line, text);
2738
- return { type: 'system', text };
2739
- }
2740
- // shorthand: /tasks implement x
2741
- const title = parsedInput.args.join(' ').trim();
2742
- if (title) {
2743
- const created = await createTasks([{ title }], process.cwd(), currentSession.id);
2744
- const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
2745
- await persistLocalExchange(line, text);
2746
- return { type: 'system', text };
2747
- }
2748
- }
2749
2686
  if (parsedInput.command === 'checkpoint') {
2750
2687
  const sub = (parsedInput.args[0] || 'list').trim().toLowerCase();
2751
2688
  if (sub === 'create') {
2752
2689
  const name = parsedInput.args.slice(1).join(' ').trim();
2753
- const tasks = await loadTasks(process.cwd(), currentSession.id);
2754
2690
  const cp = await createCheckpoint(
2755
2691
  {
2756
2692
  name,
2757
2693
  session: currentSession,
2758
- config,
2759
- tasks
2694
+ config
2760
2695
  },
2761
2696
  process.cwd()
2762
2697
  );
@@ -2791,17 +2726,6 @@ export async function createChatRuntime({
2791
2726
  config = cp.config;
2792
2727
  executionMode = config.execution?.mode || executionMode;
2793
2728
  }
2794
- if (Array.isArray(cp?.tasks)) {
2795
- await clearTasks(process.cwd(), currentSession.id);
2796
- if (cp.tasks.length > 0) {
2797
- // restore with new ids to avoid stale references
2798
- await createTasks(
2799
- cp.tasks.map((t) => ({ title: t.title, description: t.description })),
2800
- process.cwd(),
2801
- currentSession.id
2802
- );
2803
- }
2804
- }
2805
2729
  const text = `Checkpoint loaded: ${id}`;
2806
2730
  await persistLocalExchange(line, text, { includeUser: false });
2807
2731
  return { type: 'system', text };
@@ -16,7 +16,7 @@ function makeId(name = '') {
16
16
  return `${stamp}-${slug || 'checkpoint'}`;
17
17
  }
18
18
 
19
- export async function createCheckpoint({ name, session, config, tasks }, cwd = process.cwd()) {
19
+ export async function createCheckpoint({ name, session, config }, cwd = process.cwd()) {
20
20
  const dir = checkpointsDir(cwd);
21
21
  await fs.mkdir(dir, { recursive: true });
22
22
  const id = makeId(name);
@@ -26,8 +26,7 @@ export async function createCheckpoint({ name, session, config, tasks }, cwd = p
26
26
  name: String(name || ''),
27
27
  createdAt: new Date().toISOString(),
28
28
  session,
29
- config,
30
- tasks: Array.isArray(tasks) ? tasks : []
29
+ config
31
30
  };
32
31
  await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
33
32
  return payload;
@@ -8,6 +8,138 @@ function firstToken(command) {
8
8
  return base.replace(/\.exe$/i, '');
9
9
  }
10
10
 
11
+ function splitCommandSegments(command) {
12
+ const text = String(command || '').trim();
13
+ if (!text) return [];
14
+ const segments = [];
15
+ let current = '';
16
+ let quote = '';
17
+ let escapeNext = false;
18
+
19
+ for (let i = 0; i < text.length; i += 1) {
20
+ const ch = text[i];
21
+ const next = text[i + 1];
22
+
23
+ if (escapeNext) {
24
+ current += ch;
25
+ escapeNext = false;
26
+ continue;
27
+ }
28
+
29
+ if (ch === '\\' && quote !== '\'') {
30
+ current += ch;
31
+ escapeNext = true;
32
+ continue;
33
+ }
34
+
35
+ if (quote) {
36
+ current += ch;
37
+ if (ch === quote) quote = '';
38
+ continue;
39
+ }
40
+
41
+ if (ch === '"' || ch === '\'') {
42
+ quote = ch;
43
+ current += ch;
44
+ continue;
45
+ }
46
+
47
+ if ((ch === '&' && next === '&') || (ch === '|' && next === '|')) {
48
+ if (current.trim()) segments.push(current.trim());
49
+ current = '';
50
+ i += 1;
51
+ continue;
52
+ }
53
+
54
+ if (ch === '|' || ch === ';' || ch === '&') {
55
+ if (current.trim()) segments.push(current.trim());
56
+ current = '';
57
+ continue;
58
+ }
59
+
60
+ current += ch;
61
+ }
62
+
63
+ if (current.trim()) segments.push(current.trim());
64
+ return segments;
65
+ }
66
+
67
+ function tokenizeTopLevel(command) {
68
+ const text = String(command || '').trim();
69
+ if (!text) return [];
70
+ const tokens = [];
71
+ let current = '';
72
+ let quote = '';
73
+ let escapeNext = false;
74
+
75
+ for (let i = 0; i < text.length; i += 1) {
76
+ const ch = text[i];
77
+ if (escapeNext) {
78
+ current += ch;
79
+ escapeNext = false;
80
+ continue;
81
+ }
82
+ if (ch === '\\' && quote !== '\'') {
83
+ escapeNext = true;
84
+ continue;
85
+ }
86
+ if (quote) {
87
+ if (ch === quote) {
88
+ quote = '';
89
+ } else {
90
+ current += ch;
91
+ }
92
+ continue;
93
+ }
94
+ if (ch === '"' || ch === '\'') {
95
+ quote = ch;
96
+ continue;
97
+ }
98
+ if (/\s/.test(ch)) {
99
+ if (current) {
100
+ tokens.push(current);
101
+ current = '';
102
+ }
103
+ continue;
104
+ }
105
+ current += ch;
106
+ }
107
+
108
+ if (current) tokens.push(current);
109
+ return tokens;
110
+ }
111
+
112
+ function unwrapShellPayload(command) {
113
+ const tokens = tokenizeTopLevel(command);
114
+ const token = firstToken(command);
115
+ if (!['bash', 'sh', 'zsh', 'powershell', 'pwsh', 'cmd'].includes(token)) return '';
116
+
117
+ const index = tokens.findIndex((item, itemIndex) => {
118
+ if (token === 'cmd') return itemIndex > 0 && /^\/c$/i.test(item);
119
+ return /^-(?:c|lc|command)$/i.test(item);
120
+ });
121
+ if (index < 0 || index + 1 >= tokens.length) return '';
122
+ return tokens.slice(index + 1).join(' ').trim();
123
+ }
124
+
125
+ function collectCommandTokens(command) {
126
+ const cmd = String(command || '').trim();
127
+ if (!cmd) return [];
128
+
129
+ const chained = splitCommandSegments(cmd);
130
+ if (chained.length > 1) {
131
+ return chained.flatMap((segment) => collectCommandTokens(segment));
132
+ }
133
+
134
+ const token = firstToken(cmd);
135
+ const out = token ? [{ token, raw: cmd }] : [];
136
+ const wrapped = unwrapShellPayload(cmd);
137
+ if (wrapped && wrapped !== cmd) {
138
+ out.push(...collectCommandTokens(wrapped));
139
+ }
140
+ return out;
141
+ }
142
+
11
143
  function includesAny(haystackLower, patterns = []) {
12
144
  return patterns.some((p) => haystackLower.includes(String(p).toLowerCase()));
13
145
  }
@@ -46,17 +178,19 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
46
178
  }
47
179
 
48
180
  const token = firstToken(cmd);
49
- if (includesAny(token, policy.blocked_commands)) {
50
- return { allowed: false, reason: `blocked command: ${token}`, suggestion: suggestionForToken(token, config) };
51
- }
52
-
181
+ const inspectedTokens = collectCommandTokens(cmd);
53
182
  const allowlist = Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [];
54
- if (allowlist.length > 0 && !allowlist.includes(token)) {
55
- return {
56
- allowed: false,
57
- reason: `command not in allowlist: ${token}`,
58
- suggestion: suggestionForToken(token, config)
59
- };
183
+ for (const item of inspectedTokens) {
184
+ if (includesAny(item.token, policy.blocked_commands)) {
185
+ return { allowed: false, reason: `blocked command: ${item.token}`, suggestion: suggestionForToken(item.token, config) };
186
+ }
187
+ if (allowlist.length > 0 && !allowlist.includes(item.token)) {
188
+ return {
189
+ allowed: false,
190
+ reason: `command not in allowlist: ${item.token}`,
191
+ suggestion: suggestionForToken(item.token, config)
192
+ };
193
+ }
60
194
  }
61
195
 
62
196
  const workspaceLower = String(workspaceRoot).toLowerCase().replace(/\//g, '\\');
@@ -46,11 +46,9 @@ const DEFAULT_CONFIG = {
46
46
  'run',
47
47
  'patch',
48
48
  'generate_diff',
49
- 'start_service',
50
- 'list_services',
51
- 'get_service_status',
52
- 'get_service_logs',
53
- 'stop_service'
49
+ 'list_background_tasks',
50
+ 'get_background_task',
51
+ 'stop_background_task'
54
52
  ],
55
53
  max_steps: 16
56
54
  },
@@ -154,11 +152,9 @@ function normalizePolicyLists(config) {
154
152
  'write',
155
153
  'run',
156
154
  'generate_diff',
157
- 'start_service',
158
- 'list_services',
159
- 'get_service_status',
160
- 'get_service_logs',
161
- 'stop_service',
155
+ 'list_background_tasks',
156
+ 'get_background_task',
157
+ 'stop_background_task',
162
158
  ...rawTools
163
159
  ].filter((name) => String(name) !== 'list_files')
164
160
  );
@@ -19,7 +19,13 @@ export function estimateMessagesTokens(messages) {
19
19
  for (const message of messages || []) {
20
20
  const roleOverhead = 6;
21
21
  const text = textFromContent(message.content);
22
- total += roleOverhead + Math.ceil(text.length / 4);
22
+ let asciiChars = 0;
23
+ let nonAsciiChars = 0;
24
+ for (const char of text) {
25
+ if (char.charCodeAt(0) <= 0x7f) asciiChars += 1;
26
+ else nonAsciiChars += 1;
27
+ }
28
+ total += roleOverhead + Math.ceil(asciiChars / 4) + Math.ceil(nonAsciiChars / 2);
23
29
  }
24
30
  return total;
25
31
  }
@@ -18,6 +18,11 @@ Assistant: first narrow the search with the project index
18
18
  Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
19
19
  Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
20
20
 
21
+ If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
22
+ Example:
23
+ Tool: tool_search({"query":"glob"})
24
+ Tool: glob({"pattern":"src/**/*.ts"})
25
+
21
26
  2. Targeted search then exact text edit
22
27
  User: rename loginUser to signInUser
23
28
  Assistant: first find the exact occurrences
@@ -29,7 +34,13 @@ User: inspect the reducer around line 120
29
34
  Assistant: read only the needed range
30
35
  Tool: read({"path":"${cwd}/src/store/reducer.ts:110-150"})
31
36
 
32
- 4. Create a new file
37
+ 4. Track a complex task with todos
38
+ User: update the login flow and verify it
39
+ Assistant: create a focused todo checklist before starting
40
+ Tool: update_todos({"todos":[{"content":"Inspect the current login flow","activeForm":"Inspecting the current login flow","status":"in_progress"},{"content":"Implement the requested login changes","activeForm":"Implementing the requested login changes","status":"pending"},{"content":"Run focused verification for the login flow","activeForm":"Running focused verification for the login flow","status":"pending"}]})
41
+ Assistant: keep the checklist updated as each phase finishes, and do not give a completion-style wrap-up until the checklist is complete or a blocker is recorded
42
+
43
+ 5. Create a new file
33
44
  User: add a notes file
34
45
  Assistant: create the file directly
35
46
  Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
@@ -1,5 +1,11 @@
1
1
  const SECRET_PATTERNS = [
2
2
  /\b(api[_-]?key|token|secret|password|passwd|bearer)\b/i,
3
+ /\b(database_url|aws_secret_access_key|aws_access_key_id|openai_api_key|github_token|github_pat|slack_bot_token)\b\s*[:=]\s*\S+/i,
4
+ /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^/\s:@]+:[^@\s]+@/i,
5
+ /\bAKIA[0-9A-Z]{16}\b/,
6
+ /\bghp_[a-z0-9]{20,}\b/i,
7
+ /\bgithub_pat_[a-z0-9_]{20,}\b/i,
8
+ /\bglpat-[a-z0-9_-]{20,}\b/i,
3
9
  /\bsk-[a-z0-9]{8,}\b/i,
4
10
  /-----BEGIN [A-Z ]+PRIVATE KEY-----/i
5
11
  ];
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { getSessionsDir } from './paths.js';
4
+ import { normalizeTodos } from './todo-state.js';
4
5
 
5
6
  const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
6
7
 
@@ -86,6 +87,9 @@ function sanitizeSession(session, fallbackId = '') {
86
87
  }
87
88
  }
88
89
 
90
+ const todos = normalizeTodos(session?.todos);
91
+ if (todos.length > 0) out.todos = todos;
92
+
89
93
  return out;
90
94
  }
91
95