zames_pro 2.55.0 → 2.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,14 +11,14 @@ import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from '.
11
11
  import { runAgentLoop } from './agent-loop.js';
12
12
  import { createSpinner } from './spinner.js';
13
13
  import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
14
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, contentHash, } from './attachments.js';
14
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
15
15
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
16
16
  import { runConfigMenu } from './config-menu.js';
17
17
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
18
18
  import { Transcript } from './transcript.js';
19
19
  import { UndoStore } from './undo.js';
20
20
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
21
- import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
21
+ import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
22
22
  import { performCompact } from './compact.js';
23
23
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
24
24
  import { renderMarkdown, setAnswerWidth } from './markdown.js';
@@ -27,6 +27,10 @@ import { assertCommandInsideRoot, runShell } from './shell.js';
27
27
  import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
28
28
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
29
  const NL = String.fromCharCode(10);
30
+ // The last final assistant message, for the /copy command. Module-level
31
+ // because runTask() (which sees the answers) and the main loop (which handles
32
+ // /copy) are separate functions.
33
+ let lastAssistantMessage = '';
30
34
  // Floor for a /loop interval. A loop faster than the send throttle (15s)
31
35
  // cannot do anything useful and would just hammer the rate limit, so we refuse
32
36
  // it. It does NOT touch the throttle itself.
@@ -54,7 +58,7 @@ function getPositional() {
54
58
  const positional = [];
55
59
  for (let i = 0; i < args.length; i++) {
56
60
  const a = args[i];
57
- if (['--dir', '--task', '--max-iter', '--project', '--chat'].includes(a)) {
61
+ if (['--dir', '--task', '--max-iter', '--chat', '--output-format'].includes(a)) {
58
62
  i++;
59
63
  continue;
60
64
  }
@@ -88,7 +92,6 @@ const headless = hasFlag('--headed')
88
92
  ? false
89
93
  : hasFlag('--headless') || config.headless;
90
94
  const debug = hasFlag('--debug') || config.debug;
91
- const calibrate = hasFlag('--calibrate');
92
95
  const maxIterArg = getArg('--max-iter', null);
93
96
  const maxIter = maxIterArg !== null ? Number(maxIterArg) : config.maxIterations;
94
97
  const positional = getPositional();
@@ -103,6 +106,23 @@ const resendPrompt = hasFlag('--resend-prompt');
103
106
  // --new-chat: kept for compatibility — this is the default behavior anyway.
104
107
  const newChatFlag = hasFlag('--new-chat');
105
108
  const resumeLastFlag = hasFlag('--resume-last');
109
+ // Plan mode (read-only): start with it ON via --plan, toggle with /plan. When
110
+ // on, mutating tools are filtered out of the tool set entirely (see
111
+ // filterToolsForReadOnly). Declared here because tool creation happens before
112
+ // the main loop.
113
+ let planMode = hasFlag('--plan');
114
+ // Machine-readable output for scripts/CI. 'text' (default) is the normal
115
+ // human UI; 'json'/'jsonl' stream the transcript events as JSON lines on
116
+ // stdout, while all human text is pushed to stderr so the stream stays clean.
117
+ // Meant for one-shot mode (--task).
118
+ const outputFormat = (getArg('--output-format', 'text') || 'text').toLowerCase();
119
+ const jsonMode = outputFormat === 'json' || outputFormat === 'jsonl';
120
+ if (jsonMode) {
121
+ // Keep stdout for the event stream only.
122
+ console.log = ((...a) => {
123
+ console.error(...a);
124
+ });
125
+ }
106
126
  // ---------- session persistence ----------
107
127
  // We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
108
128
  // a restart and are available for explicit restore (--resume-last,
@@ -171,6 +191,7 @@ const RELOADABLE = [
171
191
  'transcript',
172
192
  'spinner',
173
193
  'config',
194
+ 'fsutil',
174
195
  ];
175
196
  const mod = {
176
197
  createTools,
@@ -289,8 +310,9 @@ ${theme.bold(t('help.options'))}
289
310
  --max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
290
311
  --headless ${t('help.opt.headless')}
291
312
  --headed ${t('help.opt.headed')}
313
+ --plan ${t('help.opt.plan')}
314
+ --output-format <f> ${t('help.opt.output_format')}
292
315
  --debug ${t('help.opt.debug')}
293
- --calibrate ${t('help.opt.calibrate')}
294
316
  --dev ${t('help.opt.dev')}
295
317
  --no-color ${t('help.opt.no_color')}
296
318
  --version, -v ${t('help.opt.version')}
@@ -312,42 +334,61 @@ ${theme.bold(t('help.while_working'))}
312
334
  ${t('help.key.bang')}
313
335
 
314
336
  ${theme.bold(t('help.commands'))}
337
+
338
+ ${theme.bold(t('help.sec.session'))}
315
339
  ${t('help.cmd.new')}
316
340
  ${t('help.cmd.sessions')}
317
- ${t('help.cmd.resume_id')}
318
341
  ${t('help.cmd.chats')}
319
342
  ${t('help.cmd.resume')}
343
+ ${t('help.cmd.resume_id')}
320
344
  ${t('help.cmd.last')}
345
+ ${t('help.cmd.retry')}
346
+ ${t('help.cmd.rename')}
347
+
348
+ ${theme.bold(t('help.sec.workspace'))}
321
349
  ${t('help.cmd.chat')}
322
350
  ${t('help.cmd.cd')}
323
351
  ${t('help.cmd.pwd')}
324
352
  ${t('help.cmd.status')}
325
353
  ${t('help.cmd.reload')}
326
- ${t('help.cmd.undo')}
327
- ${t('help.cmd.undo_list')}
328
- ${t('help.cmd.transcript')}
354
+
355
+ ${theme.bold(t('help.sec.git'))}
329
356
  ${t('help.cmd.diff')}
330
- ${t('help.cmd.cost')}
331
- ${t('help.cmd.export')}
332
- ${t('help.cmd.doctor')}
333
- ${t('help.cmd.add_dir')}
334
- ${t('help.cmd.review')}
357
+ ${t('help.cmd.diffstat')}
358
+
359
+ ${theme.bold(t('help.sec.agent'))}
335
360
  ${t('help.cmd.goal')}
336
361
  ${t('help.cmd.loop')}
337
362
  ${t('help.cmd.cron')}
338
363
  ${t('help.cmd.jobs')}
339
364
  ${t('help.cmd.thinking')}
340
365
  ${t('help.cmd.web')}
366
+ ${t('help.cmd.plan')}
341
367
  ${t('help.cmd.compact')}
342
368
  ${t('help.cmd.queue')}
343
369
  ${t('help.cmd.tasks')}
344
- ${t('help.cmd.config')}
345
- ${t('help.cmd.lang')}
370
+
371
+ ${theme.bold(t('help.sec.context'))}
372
+ ${t('help.cmd.context')}
346
373
  ${t('help.cmd.skills')}
347
374
  ${t('help.cmd.memory')}
348
375
  ${t('help.cmd.remember')}
349
376
  ${t('help.cmd.init')}
350
377
  ${t('help.cmd.mcp')}
378
+ ${t('help.cmd.config')}
379
+ ${t('help.cmd.lang')}
380
+
381
+ ${theme.bold(t('help.sec.files'))}
382
+ ${t('help.cmd.transcript')}
383
+ ${t('help.cmd.undo')}
384
+ ${t('help.cmd.undo_list')}
385
+ ${t('help.cmd.export')}
386
+ ${t('help.cmd.cost')}
387
+ ${t('help.cmd.copy')}
388
+ ${t('help.cmd.doctor')}
389
+ ${t('help.cmd.add_dir')}
390
+ ${t('help.cmd.review')}
391
+ ${t('help.cmd.improve')}
351
392
  ${t('help.cmd.debug_dom')}
352
393
  ${t('help.cmd.help')}
353
394
  ${t('help.cmd.exit')}
@@ -389,6 +430,10 @@ const SLASH_COMMANDS = [
389
430
  { name: '/resume', key: 'help.cmd.resume' },
390
431
  { name: '/resume-id', key: 'help.cmd.resume_id' },
391
432
  { name: '/last', key: 'help.cmd.last' },
433
+ { name: '/retry', key: 'help.cmd.retry' },
434
+ { name: '/rename', key: 'help.cmd.rename' },
435
+ { name: '/context', key: 'help.cmd.context' },
436
+ { name: '/copy', key: 'help.cmd.copy' },
392
437
  { name: '/chat', key: 'help.cmd.chat' },
393
438
  { name: '/cd', key: 'help.cmd.cd' },
394
439
  { name: '/pwd', key: 'help.cmd.pwd' },
@@ -398,17 +443,20 @@ const SLASH_COMMANDS = [
398
443
  { name: '/undo-list', key: 'help.cmd.undo_list' },
399
444
  { name: '/transcript', key: 'help.cmd.transcript' },
400
445
  { name: '/diff', key: 'help.cmd.diff' },
446
+ { name: '/diffstat', key: 'help.cmd.diffstat' },
401
447
  { name: '/cost', key: 'help.cmd.cost' },
402
448
  { name: '/export', key: 'help.cmd.export' },
403
449
  { name: '/doctor', key: 'help.cmd.doctor' },
404
450
  { name: '/add-dir', key: 'help.cmd.add_dir' },
405
451
  { name: '/review', key: 'help.cmd.review' },
452
+ { name: '/improve', key: 'help.cmd.improve' },
406
453
  { name: '/goal', key: 'help.cmd.goal' },
407
454
  { name: '/loop', key: 'help.cmd.loop' },
408
455
  { name: '/cron', key: 'help.cmd.cron' },
409
456
  { name: '/jobs', key: 'help.cmd.jobs' },
410
457
  { name: '/thinking', key: 'help.cmd.thinking' },
411
458
  { name: '/web', key: 'help.cmd.web' },
459
+ { name: '/plan', key: 'help.cmd.plan' },
412
460
  { name: '/compact', key: 'help.cmd.compact' },
413
461
  { name: '/queue', key: 'help.cmd.queue' },
414
462
  { name: '/tasks', key: 'help.cmd.tasks' },
@@ -1008,7 +1056,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
1008
1056
  }
1009
1057
  // ---------- task runner ----------
1010
1058
  async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
1011
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, } = opts;
1059
+ const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, } = opts;
1012
1060
  // In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
1013
1061
  // Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
1014
1062
  // mode (pipes) — a regular spinner + watchInput.
@@ -1016,6 +1064,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
1016
1064
  browser._stopped = false;
1017
1065
  browser._abort = false;
1018
1066
  const ui = editor || mod.createSpinner(currentLocale);
1067
+ // Non-TTY parity: the ora spinner shows the same "tasks: 2/5" badge the
1068
+ // LineEditor shows (the editor gets it via onTasksQuery in main()).
1069
+ if (!editor && todosQuery && 'onTasksQuery' in ui) {
1070
+ ;
1071
+ ui.onTasksQuery = todosQuery;
1072
+ }
1019
1073
  // Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
1020
1074
  // Esc / Ctrl+C abort the current generation only while busy; without this a
1021
1075
  // long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
@@ -1089,7 +1143,14 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
1089
1143
  ui.toolCall(name, toolArgs);
1090
1144
  },
1091
1145
  onToolResult: (result) => ui.toolResult(result),
1092
- onAssistantMessage: (msg) => ui.assistant(msg),
1146
+ onAssistantMessage: (msg) => {
1147
+ // Remember the last answer so /copy can put it on the clipboard
1148
+ // without re-reading the chat. Wrapped: a UI failure is swallowed
1149
+ // by agent-loop anyway.
1150
+ lastAssistantMessage = msg;
1151
+ opts.onAssistantFinal?.(msg);
1152
+ ui.assistant(msg);
1153
+ },
1093
1154
  onWarning: (msg) => ui.warning(msg),
1094
1155
  onChatReady,
1095
1156
  debugLog: debug,
@@ -1221,9 +1282,6 @@ async function main() {
1221
1282
  printHelp();
1222
1283
  return;
1223
1284
  }
1224
- if (calibrate) {
1225
- console.log(theme.warn(t('calibrate')));
1226
- }
1227
1285
  await cleanTmpDir();
1228
1286
  let currentWorkdir;
1229
1287
  let sandboxRoot;
@@ -1242,6 +1300,9 @@ async function main() {
1242
1300
  dir: config.transcript.dir,
1243
1301
  enabled: config.transcript.enabled,
1244
1302
  sessionName: dirLabel(currentWorkdir),
1303
+ // In json/jsonl mode mirror every event to stdout as a JSON line, even
1304
+ // when the transcript FILE is disabled.
1305
+ onLine: jsonMode ? (line) => process.stdout.write(line) : null,
1245
1306
  });
1246
1307
  if (transcript.file) {
1247
1308
  console.log(theme.system(t('msg.transcript', { v: transcript.file })));
@@ -1263,6 +1324,8 @@ async function main() {
1263
1324
  if (srv.error)
1264
1325
  console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
1265
1326
  }
1327
+ for (const w of st.warnings)
1328
+ console.error(theme.warn(w));
1266
1329
  }
1267
1330
  catch (e) {
1268
1331
  console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
@@ -1303,7 +1366,10 @@ async function main() {
1303
1366
  }
1304
1367
  // One-shot mode
1305
1368
  if (task) {
1306
- const tools = mod.createTools(currentWorkdir, { undo });
1369
+ const tools = mod.createTools(currentWorkdir, {
1370
+ undo,
1371
+ readOnly: planMode,
1372
+ });
1307
1373
  if (mcpPool)
1308
1374
  tools.push(...mcpPool.tools);
1309
1375
  let freshChat = true;
@@ -1366,6 +1432,8 @@ async function main() {
1366
1432
  const promptOnResume = () => resendPrompt || config.browser.resendPromptOnResume === true;
1367
1433
  let lastChats = [];
1368
1434
  let currentChatId = null;
1435
+ // The last task the operator sent (used by /retry). Plain text only.
1436
+ let lastTaskText = '';
1369
1437
  let running = true;
1370
1438
  // The agent's TodoWrite checklist, shared with the tool factory and
1371
1439
  // persisted in the session JSON. A store (not module state) so /reload does
@@ -2746,6 +2814,28 @@ async function main() {
2746
2814
  continue;
2747
2815
  }
2748
2816
  }
2817
+ if (lower === '/plan' || lower.startsWith('/plan ')) {
2818
+ const arg = trimmed.slice('/plan'.length).trim().toLowerCase();
2819
+ if (arg === '' || arg === 'toggle')
2820
+ planMode = !planMode;
2821
+ else if (arg === 'on' || arg === '1' || arg === 'true' || arg === 'вкл')
2822
+ planMode = true;
2823
+ else if (arg === 'off' ||
2824
+ arg === '0' ||
2825
+ arg === 'false' ||
2826
+ arg === 'выкл')
2827
+ planMode = false;
2828
+ else {
2829
+ console.error(theme.warn(t('msg.unknown_cmd', { v: trimmed })));
2830
+ continue;
2831
+ }
2832
+ const line = t(planMode ? 'plan.on' : 'plan.off');
2833
+ if (editor)
2834
+ editor.printAbove(theme.system(line));
2835
+ else
2836
+ console.log(theme.system(line));
2837
+ continue;
2838
+ }
2749
2839
  if (lower === '/mcp') {
2750
2840
  if (!mcpPool) {
2751
2841
  console.log(theme.dim(t('mcp.none')));
@@ -2768,6 +2858,8 @@ async function main() {
2768
2858
  theme.dim('(' + String(srv.tools.length) + ')'));
2769
2859
  }
2770
2860
  }
2861
+ for (const w of st.warnings)
2862
+ console.log(theme.warn(' ' + w));
2771
2863
  continue;
2772
2864
  }
2773
2865
  if (lower === '/skills') {
@@ -2999,6 +3091,98 @@ async function main() {
2999
3091
  console.log(theme.system(transcript.file || t('common.off')));
3000
3092
  continue;
3001
3093
  }
3094
+ if (lower === '/context') {
3095
+ // Show what buildSystemPrompt() injects (AGENTS/MEMORY/skills/commands)
3096
+ // so the operator can see why the context is large. Best-effort: a broken
3097
+ // file must not break the command.
3098
+ try {
3099
+ const { loadProjectContext } = await import('./context.js');
3100
+ const ctx = await loadProjectContext(currentWorkdir);
3101
+ const sysLen = mod.buildSystemPrompt({
3102
+ workdir: currentWorkdir,
3103
+ tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
3104
+ locale: currentLocale,
3105
+ context: ctx,
3106
+ }).length;
3107
+ console.log(theme.system(formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
3108
+ }
3109
+ catch (e) {
3110
+ console.error(theme.error(e.message));
3111
+ }
3112
+ continue;
3113
+ }
3114
+ if (lower === '/rename' || lower.startsWith('/rename ')) {
3115
+ const title = trimmed.slice('/rename'.length).trim();
3116
+ if (!title) {
3117
+ console.log(theme.dim(t('rename.usage')));
3118
+ continue;
3119
+ }
3120
+ if (!currentChatId) {
3121
+ currentChatId = await browser.getCurrentChatId();
3122
+ }
3123
+ if (!currentChatId) {
3124
+ console.error(theme.error(t('chats.not_created')));
3125
+ continue;
3126
+ }
3127
+ saveLastChat(currentChatId, currentWorkdir, title);
3128
+ console.log(theme.assistant(t('rename.done', { v: title })));
3129
+ continue;
3130
+ }
3131
+ if (lower === '/copy') {
3132
+ if (!lastAssistantMessage) {
3133
+ console.log(theme.dim(t('copy.none')));
3134
+ continue;
3135
+ }
3136
+ const res = writeClipboardText(lastAssistantMessage);
3137
+ if (res.ok) {
3138
+ console.log(theme.assistant(t('copy.done', { tool: res.via })));
3139
+ }
3140
+ else {
3141
+ console.error(theme.warn(t('copy.failed', { tool: res.via })));
3142
+ }
3143
+ continue;
3144
+ }
3145
+ if (lower === '/retry') {
3146
+ if (!lastTaskText) {
3147
+ console.log(theme.dim(t('retry.none')));
3148
+ continue;
3149
+ }
3150
+ console.log(theme.system(t('retry.running')));
3151
+ // Re-send the last task into the SAME chat (no new chat, no system
3152
+ // prompt), so a truncated/empty answer can be retried cheaply.
3153
+ const tools = mod.createTools(currentWorkdir, {
3154
+ undo,
3155
+ todos: todoStore,
3156
+ readOnly: planMode,
3157
+ });
3158
+ if (mcpPool)
3159
+ tools.push(...mcpPool.tools);
3160
+ if (editor)
3161
+ editor.busy = true;
3162
+ try {
3163
+ await runTask(browser, tools, lastTaskText, currentWorkdir, {
3164
+ transcript,
3165
+ freshChat: false,
3166
+ sendSystemPrompt: false,
3167
+ queue: pendingQueue,
3168
+ ui: editor || null,
3169
+ onChatReady: (chatId) => {
3170
+ if (chatId) {
3171
+ currentChatId = chatId;
3172
+ saveLastChat(chatId, currentWorkdir);
3173
+ }
3174
+ },
3175
+ contextLimit: config.ui.contextLimit,
3176
+ getTokenUsage: () => browser.getLastTokenUsage(),
3177
+ goal: sessionGoal,
3178
+ });
3179
+ }
3180
+ finally {
3181
+ if (editor)
3182
+ editor.busy = false;
3183
+ }
3184
+ continue;
3185
+ }
3002
3186
  if (lower === '/diff' || lower.startsWith('/diff ')) {
3003
3187
  const staged = lower.indexOf('--staged') !== -1;
3004
3188
  const { runGit } = await import('./gitTools.js');
@@ -3011,6 +3195,17 @@ async function main() {
3011
3195
  console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
3012
3196
  continue;
3013
3197
  }
3198
+ if (lower === '/diffstat') {
3199
+ const { runGit } = await import('./gitTools.js');
3200
+ const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
3201
+ if (probe.trim() !== 'true') {
3202
+ console.error(theme.error(t('diff.not_repo')));
3203
+ continue;
3204
+ }
3205
+ const out = await runGit('git diff --stat', currentWorkdir, 20_000);
3206
+ console.log(theme.system(formatDiffStat(out, { maxLines: 60 }, t)));
3207
+ continue;
3208
+ }
3014
3209
  if (lower === '/cost' || lower === '/usage') {
3015
3210
  let stats = summarizeTranscript([]);
3016
3211
  if (transcript.file) {
@@ -3143,6 +3338,58 @@ async function main() {
3143
3338
  }
3144
3339
  continue;
3145
3340
  }
3341
+ if (lower === '/improve' || lower.startsWith('/improve ')) {
3342
+ // Backlog-driven self-improvement: pick the next open BACKLOG item (or a
3343
+ // specific id) and run the standard task loop on it. The agent reads
3344
+ // BACKLOG.md itself for the details, so a FRESH agent needs no external
3345
+ // context — this is the reproducible hand-off path.
3346
+ const arg = trimmed.slice('/improve'.length).trim();
3347
+ let backlogText;
3348
+ try {
3349
+ backlogText = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
3350
+ }
3351
+ catch {
3352
+ console.error(theme.warn(t('improve.no_backlog')));
3353
+ continue;
3354
+ }
3355
+ const items = parseBacklogItems(backlogText);
3356
+ const item = nextBacklogItem(items, arg || undefined);
3357
+ if (!item) {
3358
+ console.log(theme.dim(arg ? t('improve.not_found', { v: arg }) : t('improve.all_done')));
3359
+ continue;
3360
+ }
3361
+ console.log(theme.system(t('improve.start', { id: item.id, title: item.title })));
3362
+ const improveTools = mod.createTools(currentWorkdir, {
3363
+ undo,
3364
+ todos: todoStore,
3365
+ readOnly: planMode,
3366
+ });
3367
+ if (mcpPool)
3368
+ improveTools.push(...mcpPool.tools);
3369
+ if (editor)
3370
+ editor.busy = true;
3371
+ try {
3372
+ await runTask(browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
3373
+ transcript,
3374
+ freshChat: false,
3375
+ sendSystemPrompt: false,
3376
+ queue: pendingQueue,
3377
+ ui: editor || null,
3378
+ onChatReady: (chatId) => {
3379
+ if (chatId) {
3380
+ currentChatId = chatId;
3381
+ saveLastChat(chatId, currentWorkdir);
3382
+ }
3383
+ },
3384
+ getTokenUsage: () => browser.getLastTokenUsage(),
3385
+ });
3386
+ }
3387
+ finally {
3388
+ if (editor)
3389
+ editor.busy = false;
3390
+ }
3391
+ continue;
3392
+ }
3146
3393
  if (lower === '/review' || lower.startsWith('/review ')) {
3147
3394
  const rest = trimmed.slice('/review'.length).trim();
3148
3395
  const staged = rest.indexOf('--staged') !== -1;
@@ -3182,7 +3429,15 @@ async function main() {
3182
3429
  transcript.log('undo', { path: result.record.originalPath });
3183
3430
  }
3184
3431
  else {
3185
- console.error(theme.error(t('undo.failed', { v: String(result.reason ?? '') })));
3432
+ // undo.ts returns machine codes ('empty'/'disabled') — localize here so
3433
+ // an English UI never shows a Russian reason.
3434
+ const reasonKey = result.reason === 'empty'
3435
+ ? 'undo.reason_empty'
3436
+ : result.reason === 'disabled'
3437
+ ? 'undo.reason_disabled'
3438
+ : null;
3439
+ const reason = reasonKey ? t(reasonKey) : String(result.reason ?? '');
3440
+ console.error(theme.error(t('undo.failed', { v: reason })));
3186
3441
  }
3187
3442
  continue;
3188
3443
  }
@@ -3266,6 +3521,7 @@ async function main() {
3266
3521
  }
3267
3522
  // ---- Regular task (including in review mode) ----
3268
3523
  const taskText = expandedTask !== null ? expandedTask : trimmed;
3524
+ lastTaskText = taskText;
3269
3525
  // Dev mode: pick up fresh logic modules before the task.
3270
3526
  await autoReload();
3271
3527
  // Re-scan skills/custom commands before every task: a SKILL.md or a
@@ -3275,7 +3531,11 @@ async function main() {
3275
3531
  if (editor)
3276
3532
  editor.setCommands(buildSlashCommands());
3277
3533
  transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
3278
- const tools = mod.createTools(currentWorkdir, { undo, todos: todoStore });
3534
+ const tools = mod.createTools(currentWorkdir, {
3535
+ undo,
3536
+ todos: todoStore,
3537
+ readOnly: planMode,
3538
+ });
3279
3539
  if (mcpPool)
3280
3540
  tools.push(...mcpPool.tools);
3281
3541
  if (editor)
@@ -3330,6 +3590,7 @@ async function main() {
3330
3590
  contextLimit: config.ui.contextLimit,
3331
3591
  getTokenUsage: () => browser.getLastTokenUsage(),
3332
3592
  goal: sessionGoal,
3593
+ todosQuery: () => todosSummary(),
3333
3594
  }, inputAttachments);
3334
3595
  }
3335
3596
  finally {
package/dist/mcp.js CHANGED
@@ -16,6 +16,24 @@ function packageVersion() {
16
16
  }
17
17
  return '0.0.0';
18
18
  }
19
+ /**
20
+ * Flag stdio servers pinned to a floating version (`@latest` / `@next`).
21
+ * A floating version can change between runs and silently alter the tools the
22
+ * agent sees, so the operator is warned and told to pin an exact version.
23
+ * Pure; exported for tests.
24
+ */
25
+ export function floatingVersionWarnings(servers) {
26
+ const out = [];
27
+ for (const [name, cfg] of Object.entries(servers)) {
28
+ if (cfg.disabled)
29
+ continue;
30
+ const args = [String(cfg.command || ''), ...(cfg.args || [])].join(' ');
31
+ if (/@(latest|next)\b/.test(args)) {
32
+ out.push(name);
33
+ }
34
+ }
35
+ return out;
36
+ }
19
37
  /** Names of the servers found in a config, before any connection. */
20
38
  export async function listConfiguredServers(workdir) {
21
39
  const files = await discoverConfigFiles(workdir);
@@ -298,6 +316,7 @@ export async function createMcpPool(opts) {
298
316
  const config = await loadConfigFiles(files);
299
317
  const servers = config.mcpServers || {};
300
318
  const names = Object.keys(servers).filter((n) => !servers[n].disabled);
319
+ const floating = floatingVersionWarnings(servers);
301
320
  const infos = [];
302
321
  const connections = [];
303
322
  const tools = [];
@@ -339,6 +358,7 @@ export async function createMcpPool(opts) {
339
358
  status: () => ({
340
359
  servers: infos,
341
360
  toolCount: tools.length,
361
+ warnings: floating.map((n) => `MCP server "${n}" uses a floating version (@latest/@next); pin an exact version so the tool set does not change between runs.`),
342
362
  }),
343
363
  close: async () => {
344
364
  if (closed)
package/dist/sessions.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { writeJsonAtomic } from './fsutil.js';
4
5
  // We store sessions (DeepSeek chats) in a separate folder so they aren't lost
5
6
  // after a process restart. Each session is a separate JSON file
6
7
  // <id>.json in ~/.zames/.sessions. last.json points to the last
@@ -8,11 +9,6 @@ import os from 'os';
8
9
  const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
9
10
  const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
10
11
  export const SESSIONS_FORMAT_VERSION = 1;
11
- function atomicWriteJson(file, value) {
12
- const tmp = file + '.tmp-' + process.pid;
13
- fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf-8');
14
- fs.renameSync(tmp, file);
15
- }
16
12
  function ensureDir() {
17
13
  fs.mkdirSync(SESSIONS_DIR, { recursive: true });
18
14
  }
@@ -46,7 +42,7 @@ export function saveSession({ id, title = '', workdir = '', todos, }) {
46
42
  const nextTodos = todos !== undefined ? todos : prev.todos;
47
43
  if (nextTodos !== undefined)
48
44
  data.todos = nextTodos;
49
- atomicWriteJson(file, data);
45
+ writeJsonAtomic(file, data);
50
46
  writeIndex({ id, workdir: data.workdir });
51
47
  return data;
52
48
  }
@@ -71,7 +67,7 @@ function writeIndex({ id, workdir }) {
71
67
  index.byWorkdir[key] = id;
72
68
  index.last = id;
73
69
  index.updatedAt = new Date().toISOString();
74
- atomicWriteJson(INDEX_FILE, index);
70
+ writeJsonAtomic(INDEX_FILE, index);
75
71
  }
76
72
  catch { }
77
73
  }
@@ -154,7 +150,7 @@ export function saveHistory(history) {
154
150
  try {
155
151
  ensureDir();
156
152
  const trimmed = history.slice(-HISTORY_LIMIT);
157
- atomicWriteJson(HISTORY_FILE, trimmed);
153
+ writeJsonAtomic(HISTORY_FILE, trimmed);
158
154
  }
159
155
  catch { }
160
156
  }