zames_pro 2.95.5 → 2.96.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/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.96.0] - 2026-11-05
11
+
12
+ ### Added
13
+
14
+ - **loop:** dev-only /loop improvement cycles with a release policy
15
+
16
+ ### Fixed
17
+
18
+ - **extraTools:** apply an insertion-only ApplyPatch hunk from its @@ header
19
+ - **dev:** keep the unparsed-call dump directory bounded
20
+
21
+ ## [2.95.6] - 2026-11-05
22
+
23
+ ### Added
24
+
25
+ - **compact:** carry the todo checklist and the /goal condition into the new chat
26
+ - **site:** a generated changelog page and one source for RU/EN layout strings
27
+
28
+ ### Fixed
29
+
30
+ - **changelog:** keep a drafted group heading off the previous bullet
31
+
10
32
  ## [2.95.5] - 2026-11-05
11
33
 
12
34
  ### Added
package/README.md CHANGED
@@ -72,7 +72,7 @@ and runs right after it, in the same chat. `Esc` aborts the current generation,
72
72
  - [Why zames?](#why-zames) · [Requirements](#requirements) · [Signing in](#signing-in)
73
73
  - [Switching providers (DeepSeek / Qwen / GLM)](#switching-providers-deepseek--qwen--glm)
74
74
  - [Installation](#installation) · [Usage](#usage) · [FAQ](#faq)
75
- - Reference: [Commands and tools](docs/commands.md) · [MCP](docs/mcp.md) · [Configuration and themes](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md)
75
+ - Reference: [Commands and tools](docs/commands.md) · [MCP](docs/mcp.md) · [Configuration and themes](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md) · [Changelog](https://viqto0r.github.io/zames_pro/changelog.html)
76
76
  - [Keywords and alternatives](#keywords-and-alternatives) · [Links](#links) · [License](#license)
77
77
 
78
78
  ## How it works
@@ -534,7 +534,7 @@ zames # один раз войти, дальше прост
534
534
  - [Почему zames?](#почему-zames) · [Требования](#требования) · [Вход в аккаунт](#вход-в-аккаунт)
535
535
  - [Переключение провайдера (DeepSeek / Qwen / GLM)](#переключение-провайдера-deepseek--qwen--glm)
536
536
  - [Установка](#установка) · [Использование](#использование) · [FAQ](#faq)
537
- - Справочник: [Команды и инструменты](docs/commands-ru.md) · [MCP](docs/mcp-ru.md) · [Конфигурация и темы](docs/configuration-ru.md) · [Troubleshooting](docs/troubleshooting-ru.md)
537
+ - Справочник: [Команды и инструменты](docs/commands-ru.md) · [MCP](docs/mcp-ru.md) · [Конфигурация и темы](docs/configuration-ru.md) · [Troubleshooting](docs/troubleshooting-ru.md) · [История изменений](https://viqto0r.github.io/zames_pro/changelog-ru.html)
538
538
  - [Ключевые слова и аналоги](#ключевые-слова-и-аналоги) · [Ссылки](#ссылки) · [Лицензия](#лицензия)
539
539
 
540
540
  ## Как это работает
@@ -1392,9 +1392,38 @@ export function _setParserBindings(m) {
1392
1392
  parseToolCall = m.parseToolCall;
1393
1393
  responseLooksLikeToolCall = m.responseLooksLikeToolCall;
1394
1394
  }
1395
- let unparsedDumps = 0;
1396
- const UNPARSED_DUMP_MAX = 20;
1395
+ // The dev dump is a POST-MORTEM aid, not an archive. `npm run dev` turns it
1396
+ // on, and a /loop run is ONE process: the old per-process counter (stop after
1397
+ // 20, never delete) left every earlier file in place, so the project
1398
+ // `.zames/unparsed` grew without bound (93 files against a cap of 20). The
1399
+ // net-log dump already rotates for the same reason, so this directory keeps
1400
+ // the newest UNPARSED_DUMP_KEEP dumps.
1401
+ const UNPARSED_DUMP_KEEP = 20;
1397
1402
  const UNPARSED_DUMP_BYTES = 64 * 1024;
1403
+ /** Our names start with an ISO stamp, so a plain sort is chronological. */
1404
+ const UNPARSED_DUMP_RE = /^\d{4}-\d{2}-\d{2}T[\d-]+Z-/;
1405
+ /**
1406
+ * Keeps only the newest `keep` dumps in `dir`. A foreign `.txt` is neither
1407
+ * deleted nor allowed to eat a slot of the keep window. Exported for the test:
1408
+ * this rotation is the only thing bounding the directory.
1409
+ */
1410
+ export async function rotateUnparsedDumps(dir, keep = UNPARSED_DUMP_KEEP) {
1411
+ const fs = await import('node:fs/promises');
1412
+ const p = await import('node:path');
1413
+ const names = (await fs.readdir(dir).catch(() => [])).filter((n) => n.endsWith('.txt') && UNPARSED_DUMP_RE.test(n));
1414
+ if (names.length <= keep)
1415
+ return 0;
1416
+ names.sort();
1417
+ const stale = names.slice(0, names.length - keep);
1418
+ let removed = 0;
1419
+ for (const n of stale) {
1420
+ await fs
1421
+ .rm(p.join(dir, n), { force: true })
1422
+ .then(() => removed++)
1423
+ .catch(() => { });
1424
+ }
1425
+ return removed;
1426
+ }
1398
1427
  /** Dev mode: save an unrecognized (but call-looking) answer for analysis. */
1399
1428
  /**
1400
1429
  * True when the raw answer is a bare `{"tool":"BashOutput","args":{"id":...}}`
@@ -1421,8 +1450,6 @@ export function isEmptyPollCall(raw) {
1421
1450
  return !!a && typeof a.id === 'string';
1422
1451
  }
1423
1452
  export async function dumpUnparsedCall(raw, meta) {
1424
- if (unparsedDumps >= UNPARSED_DUMP_MAX)
1425
- return '';
1426
1453
  const text = String(raw || '');
1427
1454
  if (!text.trim())
1428
1455
  return '';
@@ -1431,7 +1458,6 @@ export async function dumpUnparsedCall(raw, meta) {
1431
1458
  const dir = p.join(meta.workdir || process.cwd(), '.zames', 'unparsed');
1432
1459
  try {
1433
1460
  await fs.mkdir(dir, { recursive: true });
1434
- unparsedDumps++;
1435
1461
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1436
1462
  const name = stamp + '-' + String(meta.where || 'x').replace(/[^a-z0-9_-]+/gi, '_');
1437
1463
  const file = p.join(dir, name + '.txt');
@@ -1455,6 +1481,8 @@ export async function dumpUnparsedCall(raw, meta) {
1455
1481
  NL +
1456
1482
  NL;
1457
1483
  await fs.writeFile(file, head + text.slice(0, UNPARSED_DUMP_BYTES), 'utf-8');
1484
+ // Rotate AFTER the write, so the dump just produced survives.
1485
+ await rotateUnparsedDumps(dir);
1458
1486
  return file;
1459
1487
  }
1460
1488
  catch {
package/dist/changelog.js CHANGED
@@ -97,13 +97,25 @@ export function syncUnreleased(changelog, body) {
97
97
  // carries several groups (`## Added` … `## Fixed`) without a blank line
98
98
  // between them, the next group used to stay glued to the last bullet
99
99
  // (`- a### Fixed`) — it was not a heading at all, so the item vanished
100
- // from the rendered changelog.
101
- .replace(/([^\n])\n?(?=## )/g, '$1' + nl + nl)
100
+ // from the rendered changelog. The lookahead is what keeps a CONTINUATION
101
+ // line of a multi-line bullet in place: gluing on `\n` as well joined the
102
+ // indent line to the next bullet and truncated the note.
103
+ // The draft's groups are still `## ` at this point (they become `### `
104
+ // below), and the FIRST group is already preceded by the blank line of
105
+ // the draft — so a group always starts a line, and this rule only has to
106
+ // punch in the missing blank line after a bullet.
107
+ .replace(/([^\n])(?=## )/g, '$1' + nl + nl)
102
108
  .trim()
103
109
  // The draft renders groups as `## Added` (top-level), but INSIDE the file
104
110
  // a version's groups are `### Added` (a subsection of the version heading).
105
- // `## [` (the next version) is left alone.
106
- .replace(/^## (?!\[)/gm, '### ');
111
+ // `## [` (the next version) is left alone. The FIRST group follows the
112
+ // version heading directly (no bullet before it), so the blank-line rule
113
+ // above never fired for it — `## [Unreleased]` came out as a bare `#`
114
+ // followed by `# Added`. The heading owns a blank line, always.
115
+ .replace(/^## (?!\[)/gm, '### ')
116
+ // One blank line between the version heading and its first group, not
117
+ // the three the rules above can stack up.
118
+ .replace(/^\n+(?=### )/gm, '');
107
119
  const section = cleanBody ? nl + nl + cleanBody + nl : nl;
108
120
  return changelog.slice(0, afterAnchor) + section + tail;
109
121
  }
package/dist/commands.js CHANGED
@@ -512,20 +512,80 @@ export function isUsableCompactSummary(text) {
512
512
  return false;
513
513
  return true;
514
514
  }
515
+ function todoMark(s) {
516
+ return s === 'completed' ? '[x]' : s === 'in_progress' ? '[~]' : '[ ]';
517
+ }
518
+ /** The TODO block carried into the new chat. Empty when there are no tasks. */
519
+ export function renderCompactTodos(todos = []) {
520
+ const items = Array.isArray(todos) ? todos : [];
521
+ if (!items.length)
522
+ return '';
523
+ const lines = items.map((x, i) => String(i + 1) +
524
+ '. ' +
525
+ todoMark(String(x.status)) +
526
+ ' ' +
527
+ String(x.content));
528
+ const done = items.filter((x) => x.status === 'completed').length;
529
+ return ('## Task checklist (carried over by the agent, still in force)' +
530
+ NL +
531
+ 'This list is SESSION state: it survived the compaction unchanged. ' +
532
+ 'Do NOT recreate it from the summary — keep it up to date with ' +
533
+ 'TodoWrite instead.' +
534
+ NL +
535
+ 'Todo list (' +
536
+ String(done) +
537
+ '/' +
538
+ String(items.length) +
539
+ ' done):' +
540
+ NL +
541
+ lines.join(NL));
542
+ }
543
+ /** The goal block carried into the new chat. Empty when no goal is set. */
544
+ export function renderCompactGoal(goal) {
545
+ const g = String(goal?.goal ?? '').trim();
546
+ if (!g)
547
+ return '';
548
+ const head = goal?.isCondition
549
+ ? '## Completion goal (carried over, still in force)'
550
+ : '## Session goal (carried over, still in force)';
551
+ return (head +
552
+ NL +
553
+ 'This goal is still active in this session; the compaction did not ' +
554
+ 'cancel it. Keep working toward it.' +
555
+ NL +
556
+ NL +
557
+ g);
558
+ }
559
+ /**
560
+ * Re-pin the SESSION state after a compaction. The chat is new, but the todo
561
+ * store and the /goal file live in the process and on disk, so they survive —
562
+ * only the model's KNOWLEDGE of them does not. Sent as its own message right
563
+ * after the carryover, so it reads as current state, not as history.
564
+ */
565
+ export function buildCompactToolResult(todos = [], goal = null) {
566
+ return [renderCompactTodos(todos), renderCompactGoal(goal)]
567
+ .filter(Boolean)
568
+ .join(NL + NL);
569
+ }
515
570
  /**
516
571
  * Wrap the model's summary into the text posted as the first message of the
517
572
  * NEW chat. The system prompt is sent separately (sendSystemPrompt), so here
518
573
  * we only mark the block as a carried-over context and add the operator's
519
- * original goal so the model does not lose it.
574
+ * original task. The todo checklist and the /goal condition are appended too:
575
+ * they are session state the model-written summary is not guaranteed to keep.
520
576
  */
521
- export function buildCompactCarryover(summary, task) {
577
+ export function buildCompactCarryover(summary, task, todos = [], goal = null) {
522
578
  const body = String(summary ?? '').trim();
523
- const goal = String(task ?? '').trim();
579
+ const goalText = String(task ?? '').trim();
524
580
  let out = 'Context carried over from a previous chat (compacted). ' +
525
581
  'Treat it as the history of our work so far and continue from the current state.';
526
582
  out += NL + NL + body;
527
- if (goal) {
528
- out += NL + NL + 'Original task: ' + goal;
583
+ if (goalText) {
584
+ out += NL + NL + 'Original task: ' + goalText;
585
+ }
586
+ for (const extra of [renderCompactTodos(todos), renderCompactGoal(goal)]) {
587
+ if (extra)
588
+ out += NL + NL + extra;
529
589
  }
530
590
  return out;
531
591
  }
@@ -1036,6 +1096,13 @@ export function buildImprovePrompt(item) {
1036
1096
  export const DEV_ONLY_COMMANDS = [
1037
1097
  '/improve',
1038
1098
  '/backlog',
1099
+ // The /loop improvement cycles (src/loop-prompts.ts): they edit QUEUE.md,
1100
+ // read the dev dumps and push to master — a regular user has none of that.
1101
+ '/bughunt',
1102
+ '/textaudit',
1103
+ '/coverage',
1104
+ '/dumps',
1105
+ '/smoke',
1039
1106
  '/self-review',
1040
1107
  '/self-fix',
1041
1108
  '/self-done',
@@ -1108,6 +1175,14 @@ export const SLASH_COMMANDS = [
1108
1175
  { name: '/review', key: 'help.cmd.review' },
1109
1176
  { name: '/improve', key: 'help.cmd.improve' },
1110
1177
  { name: '/backlog', key: 'help.cmd.backlog' },
1178
+ // The DEV-ONLY improvement cycles (src/loop-prompts.ts). printHelp filters
1179
+ // them out for a regular user (isDevOnlyCommand); the «/» list drops them
1180
+ // in buildSlashCommands, so a release build never advertises them.
1181
+ { name: '/bughunt', key: 'help.cmd.bughunt' },
1182
+ { name: '/textaudit', key: 'help.cmd.textaudit' },
1183
+ { name: '/coverage', key: 'help.cmd.coverage' },
1184
+ { name: '/dumps', key: 'help.cmd.dumps' },
1185
+ { name: '/smoke', key: 'help.cmd.smoke' },
1111
1186
  { name: '/goal', key: 'help.cmd.goal' },
1112
1187
  { name: '/loop', key: 'help.cmd.loop' },
1113
1188
  { name: '/cron', key: 'help.cmd.cron' },
package/dist/compact.js CHANGED
@@ -16,7 +16,7 @@ export const COMPACT_FALLBACK_LIMIT = 40;
16
16
  * The caller owns the browser and the throttle; this function only sends.
17
17
  */
18
18
  export async function performCompact(opts) {
19
- const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, selfImprovement = false, contextExhausted = false, } = opts;
19
+ const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, selfImprovement = false, contextExhausted = false, todos = [], goal = null, } = opts;
20
20
  const t = translate(locale);
21
21
  // The active provider's LABEL for operator-facing text: performCompact is
22
22
  // shared by every backend, so it cannot hardcode "DeepSeek". `providerArg`
@@ -189,7 +189,14 @@ export async function performCompact(opts) {
189
189
  timeout: 60_000,
190
190
  agent: false,
191
191
  });
192
- await browser.ask(buildCompactCarryover(summary, task), {
192
+ // The checklist and the goal are SESSION state (todoStore /
193
+ // .zames-goal), not chat state: they survive a compaction by themselves,
194
+ // but the model must be told they are still in force, otherwise it
195
+ // re-creates the tasks or treats the goal as gone because the summary
196
+ // does not mention it. They ride INSIDE the carryover, not as a separate
197
+ // turn: an extra send is another rate-limit pause AND every extra
198
+ // message of the new chat is another turn the model may answer.
199
+ await browser.ask(buildCompactCarryover(summary, task, todos, goal), {
193
200
  timeout: 60_000,
194
201
  agent: false,
195
202
  });
@@ -1,14 +1,26 @@
1
1
  // N2 group (e): the DEV-ONLY self-development commands — /improve, /backlog,
2
- // /self-review, /self-fix, /self-done, /self-list, /self-diff, /self-apply.
2
+ // /self-review, /self-fix, /self-done, /self-list, /self-diff, /self-apply,
3
+ // plus the `/loop` improvement CYCLES (/bughunt, /textaudit, /coverage,
4
+ // /dumps, /smoke).
3
5
  //
4
6
  // They are registry entries with `devOnly: true`, so the guard that used to be
5
7
  // a separate pre-check in the main loop is the SAME data the dispatch reads:
6
8
  // a regular user gets `msg.dev_only`, dev mode runs the handler. The list here
7
9
  // and `DEV_ONLY_COMMANDS` in src/commands.ts are kept in sync by a unit test.
10
+ import { LOOP_CYCLES } from './loop-prompts.js';
8
11
  export function createDevCommands(deps) {
9
12
  return [
10
13
  { name: 'improve', devOnly: true, handler: (c) => deps.improve(c.rest) },
11
14
  { name: 'backlog', devOnly: true, handler: (c) => deps.backlog(c.rest) },
15
+ // `backlog` has BOTH a dev command (record a note) and a cycle (implement
16
+ // an item): the command keeps its name, the cycle is reachable as
17
+ // `/backlog` only when no explicit entry exists, so the registry list is
18
+ // deduplicated by name below.
19
+ ...LOOP_CYCLES.filter((id) => id !== 'backlog').map((id) => ({
20
+ name: id,
21
+ devOnly: true,
22
+ handler: (c) => deps.cycle(id, c.rest),
23
+ })),
12
24
  {
13
25
  name: 'self-review',
14
26
  devOnly: true,
@@ -198,10 +198,26 @@ export function applyUpdateHunk(content, hunkLines) {
198
198
  }
199
199
  }
200
200
  if (!oldLines.length) {
201
- return {
202
- ok: false,
203
- error: 'Hunk has no context or removed lines to anchor on.',
204
- };
201
+ // An INSERTION-ONLY hunk (`@@ -N,0 +N,M @@` + `+line`s) has nothing to
202
+ // match, so it was rejected outright — yet appending a line is a normal
203
+ // unified-diff form and the model uses it. Position it from the header's
204
+ // old-side start (a `-N,0` hunk inserts AFTER line N, hence the index N);
205
+ // without a usable header there is no anchor and we still refuse.
206
+ const hinted = block.oldStart !== null ? block.oldStart : -1;
207
+ if (hinted < 0) {
208
+ return {
209
+ ok: false,
210
+ error: 'Insertion-only hunk needs a @@ -N,0 +N,M @@ header to know where to insert.',
211
+ };
212
+ }
213
+ const atInsert = Math.min(Math.max(hinted, searchFrom), fileLines.length);
214
+ fileLines = [
215
+ ...fileLines.slice(0, atInsert),
216
+ ...newLines,
217
+ ...fileLines.slice(atInsert),
218
+ ];
219
+ searchFrom = atInsert + newLines.length;
220
+ continue;
205
221
  }
206
222
  const at = locateBlock(fileLines, oldLines, block.oldStart, searchFrom);
207
223
  if (at === -1) {
@@ -169,6 +169,10 @@ export const commonMessages = {
169
169
  ru: 'Команда {v} доступна только в dev-режиме (--dev или hotReload).',
170
170
  en: 'The {v} command is only available in dev mode (--dev or hotReload).',
171
171
  },
172
+ 'cycle.unknown': {
173
+ ru: 'Неизвестный цикл {v}: ожидается /bughunt, /textaudit, /coverage, /dumps, /backlog или /smoke.',
174
+ en: 'Unknown cycle {v}: expected /bughunt, /textaudit, /coverage, /dumps, /backlog or /smoke.',
175
+ },
172
176
  'msg.bye': { ru: 'Выход.', en: 'Bye.' },
173
177
  'shell.empty': {
174
178
  ru: 'После ! нужна команда, например !git status.',
package/dist/i18n/help.js CHANGED
@@ -320,6 +320,30 @@ export const helpMessages = {
320
320
  ru: 'стартовать в режиме плана (только чтение)',
321
321
  en: 'start in plan mode (read-only)',
322
322
  },
323
+ 'help.sec.cycles': {
324
+ ru: 'Циклы улучшения (/loop, только dev)',
325
+ en: 'Improvement cycles (/loop, dev only)',
326
+ },
327
+ 'help.cmd.bughunt': {
328
+ ru: '/bughunt [интервал] dev-only: /loop с охотой за багами по docs/BUGHUNT.md',
329
+ en: '/bughunt [interval] dev-only: /loop the bug hunt from docs/BUGHUNT.md',
330
+ },
331
+ 'help.cmd.textaudit': {
332
+ ru: '/textaudit [интервал] dev-only: /loop с аудитом текстов по docs/TEXT-AUDIT.md',
333
+ en: '/textaudit [interval] dev-only: /loop the text audit from docs/TEXT-AUDIT.md',
334
+ },
335
+ 'help.cmd.coverage': {
336
+ ru: '/coverage [интервал] dev-only: /loop с заполнением пробелов покрытия тестами',
337
+ en: '/coverage [interval] dev-only: /loop filling test-coverage gaps',
338
+ },
339
+ 'help.cmd.dumps': {
340
+ ru: '/dumps [интервал] dev-only: /loop с разбором дампов непойманных tool-вызовов',
341
+ en: '/dumps [interval] dev-only: /loop triaging the unparsed-call dumps',
342
+ },
343
+ 'help.cmd.smoke': {
344
+ ru: '/smoke [интервал] dev-only: /loop с живым smoke-прогоном провайдера',
345
+ en: '/smoke [interval] dev-only: /loop a live provider smoke run',
346
+ },
323
347
  'help.cmd.loop': {
324
348
  ru: '/loop <интервал> <задача> повторять задачу (s/m/h; без единицы — секунды; первый запуск сразу; минимум 15с)',
325
349
  en: '/loop <interval> <task> repeat a task (s/m/h; no unit means seconds; the first run is immediate; minimum 15s)',
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { createSessionCommands } from './session-commands.js';
17
17
  import { createTreeCommands } from './tree-commands.js';
18
18
  import { createSchedulerCommands } from './scheduler-commands.js';
19
19
  import { createDevCommands } from './dev-commands.js';
20
+ import { buildCycleLoopLine } from './loop-prompts.js';
20
21
  import { shouldRunWizard, chooseLanguage, chooseProvider, providerNote, } from './first-run.js';
21
22
  import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
22
23
  import { QwenBrowser } from './qwen-browser.js';
@@ -481,6 +482,18 @@ function printHelp() {
481
482
  t('help.self.diff') +
482
483
  '\n ' +
483
484
  t('help.self.apply') +
485
+ '\n\n' +
486
+ theme.bold(t('help.sec.cycles')) +
487
+ '\n ' +
488
+ t('help.cmd.bughunt') +
489
+ '\n ' +
490
+ t('help.cmd.textaudit') +
491
+ '\n ' +
492
+ t('help.cmd.coverage') +
493
+ '\n ' +
494
+ t('help.cmd.dumps') +
495
+ '\n ' +
496
+ t('help.cmd.smoke') +
484
497
  '\n\n'
485
498
  : ' ' +
486
499
  t('help.cmd.debug_dom') +
@@ -1302,6 +1315,8 @@ async function main() {
1302
1315
  skipUiLock: true,
1303
1316
  selfImprovement: devMode,
1304
1317
  contextExhausted: true,
1318
+ todos: getTodos(todoStore),
1319
+ goal: sessionGoal ? { goal: sessionGoal, isCondition: true } : null,
1305
1320
  });
1306
1321
  if (!res.ok)
1307
1322
  return false;
@@ -3125,6 +3140,8 @@ async function main() {
3125
3140
  answerTimeoutMs: providerAnswerTimeoutMs(),
3126
3141
  fallbackLimit: COMPACT_FALLBACK_LIMIT,
3127
3142
  selfImprovement: devMode,
3143
+ todos: getTodos(todoStore),
3144
+ goal: sessionGoal ? { goal: sessionGoal, isCondition: true } : null,
3128
3145
  });
3129
3146
  if (res.ok) {
3130
3147
  currentChatId = res.chatId;
@@ -3134,6 +3151,21 @@ async function main() {
3134
3151
  sendSystemPromptNext = false;
3135
3152
  }
3136
3153
  };
3154
+ /**
3155
+ * A dev-only /loop improvement cycle (`/bughunt 10m`, `/coverage`): build
3156
+ * the ONE `/loop <interval> <task>` line and arm the scheduler through the
3157
+ * SAME path `/loop` uses, so the interval/job bookkeeping has one owner.
3158
+ */
3159
+ const cycleCommand = (id, rest) => {
3160
+ const arg = rest.trim();
3161
+ const interval = arg.split(/\s+/)[0] || '';
3162
+ const line = buildCycleLoopLine(id, interval);
3163
+ if (!line) {
3164
+ console.error(theme.error(t('cycle.unknown', { v: '/' + id })));
3165
+ return;
3166
+ }
3167
+ loopCommand(line.slice('/loop '.length));
3168
+ };
3137
3169
  /** /goal: set/show/clear the condition. Returns whether it was SET. */
3138
3170
  const goalCommand = async (line) => {
3139
3171
  const sub = await handleGoal(line);
@@ -3599,6 +3631,7 @@ async function main() {
3599
3631
  selfList: () => selfListCommand(''),
3600
3632
  selfDiff: (name) => selfDiffCommand(name),
3601
3633
  selfApply: (name) => selfApplyCommand(name),
3634
+ cycle: (id, rest) => cycleCommand(id, rest),
3602
3635
  });
3603
3636
  if (await dispatchCommand(devCommands, trimmed, {
3604
3637
  print: (line) => console.log(line),
@@ -4730,6 +4763,10 @@ async function main() {
4730
4763
  quiet: true,
4731
4764
  skipUiLock: true,
4732
4765
  selfImprovement: devMode,
4766
+ todos: getTodos(todoStore),
4767
+ goal: sessionGoal
4768
+ ? { goal: sessionGoal, isCondition: true }
4769
+ : null,
4733
4770
  });
4734
4771
  if (res.ok) {
4735
4772
  currentChatId = res.chatId;
@@ -0,0 +1,61 @@
1
+ // The DEV-ONLY `/loop` cycles of zames_pro itself: bughunt, textaudit,
2
+ // coverage, dumps, backlog, smoke. Each command expands into ONE `/loop`
3
+ // line, so the interval lives in the scheduler (a fired tick is queued and
4
+ // skipped while the same job is still queued) and the operator never re-pastes
5
+ // a long prompt.
6
+ //
7
+ // WHY dev-only: these cycles edit BACKLOG.md/QUEUE.md, read the dev dumps and
8
+ // push to `master` — a regular npm user has none of that, and `/bughunt` on an
9
+ // arbitrary project would hunt bugs in THEIR code with OUR protocol. The list
10
+ // here and `DEV_ONLY_COMMANDS` in src/commands.ts are kept in sync by a test.
11
+ export const LOOP_CYCLES = [
12
+ 'bughunt',
13
+ 'textaudit',
14
+ 'coverage',
15
+ 'dumps',
16
+ 'backlog',
17
+ 'smoke',
18
+ ];
19
+ /** Default period of a cycle: 5m. The floor is MIN_LOOP_MS (15s). */
20
+ export const DEFAULT_LOOP_INTERVAL = '5m';
21
+ const RULES = 'run the gates (typecheck, test, lint, format:check, build), append a step entry to QUEUE.md with the finding + gates + commit SHA, update the journal list, commit (see the subject rule below) and push to master (git push origin master), then check the release policy: git describe --tags --abbrev=0, git log <tag>..HEAD --pretty=format:%s and src/release-policy.ts decideRelease() — if it reports a READY patch/minor, run `npm run release <bump>`, push the branch AND the tag (git push && git push origin vX.Y.Z), and record the new version in QUEUE.md; a major or a BREAKING change is NEVER released by the loop, only reported. Do NOT bump the version by hand.';
22
+ const BODIES = {
23
+ bughunt: 'Bug hunt, one step. Read docs/BUGHUNT.md and follow it exactly: pick the next NOT-yet-covered module from its scope table (skip the ones under "Покрытые области" in QUEUE.md unless their code changed after the covering date), find ONE real reproducible defect, write the FAILING test first (RED), fix the root cause (GREEN), ' +
24
+ RULES +
25
+ ' If the module is clean, add it to "Покрытые области", write a `чисто` entry with what you verified, and commit nothing. Stop the loop with /jobs clear when every module in the scope table is covered and no covered module has changed since its covering date.',
26
+ textaudit: 'Text audit, one step. Read docs/TEXT-AUDIT.md and follow it exactly: pick the next NOT-yet-covered file from its scope table (skip the ones listed under "Covered areas" in QUEUE.md), audit it against the code as the source of truth, fix ONE defect family in the source file(s) plus both languages, regenerate the generated files (npm run build:readme && npm run build:llms), ' +
27
+ RULES +
28
+ ' If the file is clean, add it to "Covered areas", write a `чисто` entry with what you verified, and commit nothing. Stop the loop with /jobs clear when every file in the scope table is covered.',
29
+ coverage: 'Coverage step, one step: run `npm run coverage:gate`, find the module or branch whose line coverage is lowest or has NO test file at all (list `src/*.ts` without a matching `test/*.test.ts`), pick ONE of them, and write pure unit tests that pin the CURRENT behaviour — do not change the source unless a test uncovers a real bug (then switch to a fix: commit and cite docs/BUGHUNT.md). ' +
30
+ RULES +
31
+ ' Stop the loop with /jobs clear when every source module has a test file and no gate module is below its floor.',
32
+ dumps: 'Unparsed-dump step, one step: list `.zames/unparsed/*.txt` (dev dumps of answers that looked like a tool call but were NOT parsed) and `~/.zames/net-log` if ZAMES_NET_DEBUG was on; take the NEWEST dump not yet analysed in QUEUE.md, classify WHY parseToolCall (src/answer-parse.ts) and the XML/DSML path (src/xml-toolcall.ts) missed it, then either extend the parser with a failing test FIRST (test/answer-parse*.test.ts or test/xml-toolcall*.test.ts) or record in QUEUE.md that the answer is genuinely unparseable (a reasoning paragraph, not a call) and why. ' +
33
+ RULES +
34
+ ' Do NOT delete a dump you have not classified. Stop the loop with /jobs clear when every dump in the directory is classified.',
35
+ backlog: 'Backlog step, one item: read BACKLOG.md, take the highest-priority item that is NOT archived in a <details> block and is small enough for one step, implement it (a failing test first when the item is testable), ' +
36
+ RULES +
37
+ " Move the finished item under the archived <details> block with the /backlog helpers. Skip an item that needs the operator's decision and note WHY in QUEUE.md. Stop the loop with /jobs clear when BACKLOG.md has no open item left.",
38
+ smoke: "Live smoke step, one step: run the matching smoke script (`npm run self-smoke` for deepseek, `npm run qwen-smoke`, `npm run glm-smoke -- --headed`); it launches an ISOLATED zames instance (throwaway HOME) and must NEVER touch the operator's ~/.zames/profile or another agent's browser. If a scenario FAILS, read its output, reproduce the failing unit-level behaviour with a focused test if possible, fix the root cause (src/browser*.ts / src/qwen-*.ts / src/glm-*.ts / src/*-net-capture.ts), rerun the smoke and " +
39
+ RULES +
40
+ ' If every scenario PASSES, write a `чисто` entry with the exit code and commit nothing. Stop the loop with /jobs clear after 3 consecutive PASS runs with no new failure.',
41
+ };
42
+ /** True when `id` is one of the cycles. */
43
+ export function isLoopCycle(id) {
44
+ return LOOP_CYCLES.includes(String(id ?? '').toLowerCase());
45
+ }
46
+ /**
47
+ * Build the ONE line a cycle sends to `/loop`. `interval` is the FIRST token
48
+ * (the scheduler parses it), so a bare `/bughunt 10m` retunes the period.
49
+ * Returns null for an unknown cycle.
50
+ */
51
+ export function buildCycleLoopLine(id, interval = DEFAULT_LOOP_INTERVAL) {
52
+ const cycle = String(id ?? '').toLowerCase();
53
+ if (!isLoopCycle(cycle))
54
+ return null;
55
+ // Collapse whitespace: a newline would split the interval off the command.
56
+ const every = String(interval ?? '')
57
+ .trim()
58
+ .replace(/\s+/g, '');
59
+ const period = every || DEFAULT_LOOP_INTERVAL;
60
+ return '/loop ' + period + ' ' + BODIES[cycle];
61
+ }
@@ -0,0 +1,100 @@
1
+ // WHEN a /loop improvement cycle may cut a release. PURE and unit-tested: the
2
+ // decision is made from the commits since the last tag, and it is the one rule
3
+ // that keeps an unattended run from either spamming npm with a patch per fix
4
+ // or sitting on a pile of unreleased work forever.
5
+ //
6
+ // The policy is deliberately COARSE. DEVELOPING.md says several fixes land as
7
+ // ONE patch release (a 142-patch 2.82.x branch made the CHANGELOG and npm
8
+ // unreadable), so a lone fix is NOT enough: at least MIN_COMMITS user-visible
9
+ // changes must have accumulated. A feature or a breaking change is the
10
+ // operator's own signal and releases alone.
11
+ //
12
+ // docs/chore/test/ci/build commits do not count (they never appear in a
13
+ // user-facing changelog), but they also must not BLOCK a release: they are
14
+ // simply ignored.
15
+ import { parseConventionalCommit, groupCommits, } from './changelog.js';
16
+ /** User-visible commits (feat/fix/perf/refactor/revert/remove) that release together. */
17
+ export const MIN_COMMITS = 5;
18
+ /**
19
+ * Decide whether the commits in `subjects` (git log --pretty=format:%s since
20
+ * the last tag) justify a release, and which bump it is.
21
+ *
22
+ * - a `!` marker -> major (alone);
23
+ * - any `feat:` -> minor (alone);
24
+ * - otherwise at least MIN_COMMITS visible commits -> patch;
25
+ * - anything less -> `bump: null` (wait; the next step adds to the pile).
26
+ */
27
+ export function decideRelease(subjects, opts = {}) {
28
+ const min = opts.minCommits ?? MIN_COMMITS;
29
+ const parsed = (subjects || [])
30
+ .map((s) => parseConventionalCommit(String(s ?? '')))
31
+ .filter((c) => c !== null);
32
+ // A chore(release) commit is the release itself; it must not be counted as
33
+ // work and must not trigger a second release.
34
+ const work = parsed.filter((c) => !(c.type === 'chore' && c.scope === 'release'));
35
+ const breaking = work.filter((c) => c.breaking);
36
+ if (breaking.length) {
37
+ return {
38
+ bump: 'major',
39
+ counted: work.length,
40
+ kinds: kindsOf(work),
41
+ blocked: breakingNote(breaking),
42
+ };
43
+ }
44
+ const feats = work.filter((c) => c.type === 'feat');
45
+ if (feats.length) {
46
+ return { bump: 'minor', counted: work.length, kinds: kindsOf(work) };
47
+ }
48
+ // The changelog groups are the source of truth for "user-visible": a type
49
+ // that renders NO line (docs/test/chore/ci/build) is not work worth a
50
+ // release on its own.
51
+ const groups = groupCommits(work);
52
+ const visible = groups.Added.length +
53
+ groups.Fixed.length +
54
+ groups.Changed.length +
55
+ groups.Removed.length;
56
+ if (visible < min) {
57
+ return { bump: null, counted: visible, kinds: kindsOf(work) };
58
+ }
59
+ return { bump: 'patch', counted: visible, kinds: kindsOf(work) };
60
+ }
61
+ function kindsOf(list) {
62
+ const seen = [];
63
+ for (const c of list)
64
+ if (!seen.includes(c.type))
65
+ seen.push(c.type);
66
+ return seen;
67
+ }
68
+ function breakingNote(list) {
69
+ const first = list[0];
70
+ return ('a BREAKING change (' +
71
+ (first.scope ? first.scope + '): ' : '') +
72
+ first.description +
73
+ ') must be released by the operator, not by an unattended /loop');
74
+ }
75
+ /**
76
+ * A `/loop` must NEVER publish. `npm run release` only commits and tags, but
77
+ * the tag is what triggers the npm publish workflow — so a cycle that tagged
78
+ * would publish on its own.
79
+ */
80
+ export const LOOP_RELEASE_BLOCKED = 'an unattended /loop must not tag: a tag push publishes to npm. ' +
81
+ 'Report the ready bump and let the operator run `npm run release <bump>`.';
82
+ /**
83
+ * The one line a cycle prints (and writes into QUEUE.md) when the pile is big
84
+ * enough for a release.
85
+ */
86
+ export function releaseHint(d, version) {
87
+ if (d.blocked)
88
+ return 'release blocked: ' + d.blocked;
89
+ if (!d.bump) {
90
+ const left = Math.max(0, MIN_COMMITS - d.counted);
91
+ return ('release: not yet (' +
92
+ d.counted +
93
+ '/' +
94
+ MIN_COMMITS +
95
+ ' user-visible commits; ' +
96
+ left +
97
+ ' more)');
98
+ }
99
+ return 'release READY: npm run release ' + d.bump + ' (from ' + version + ')';
100
+ }
package/llms-full.txt CHANGED
@@ -57,7 +57,7 @@ and runs right after it, in the same chat. `Esc` aborts the current generation,
57
57
  - [Why zames?](#why-zames) · [Requirements](#requirements) · [Signing in](#signing-in)
58
58
  - [Switching providers (DeepSeek / Qwen / GLM)](#switching-providers-deepseek--qwen--glm)
59
59
  - [Installation](#installation) · [Usage](#usage) · [FAQ](#faq)
60
- - Reference: [Commands and tools](docs/commands.md) · [MCP](docs/mcp.md) · [Configuration and themes](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md)
60
+ - Reference: [Commands and tools](docs/commands.md) · [MCP](docs/mcp.md) · [Configuration and themes](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md) · [Changelog](https://viqto0r.github.io/zames_pro/changelog.html)
61
61
  - [Keywords and alternatives](#keywords-and-alternatives) · [Links](#links) · [License](#license)
62
62
 
63
63
  ## How it works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.95.5",
3
+ "version": "2.96.0",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,8 @@
44
44
  "render-how": "tsx scripts/render-how-it-works.mts",
45
45
  "build:readme": "tsx scripts/build-readme.mts",
46
46
  "build:llms": "tsx scripts/build-llms.mts",
47
+ "build:changelog": "tsx scripts/build-changelog.mts",
48
+ "build:site-data": "tsx scripts/build-site-data.mts",
47
49
  "postinstall": "node scripts/postinstall.mjs",
48
50
  "prepare": "husky",
49
51
  "glm-smoke": "tsx scripts/glm-smoke.mts",
@@ -170,5 +172,10 @@
170
172
  "funding": {
171
173
  "type": "github",
172
174
  "url": "https://github.com/sponsors/Viqto0r"
175
+ },
176
+ "overrides": {
177
+ "@typescript-eslint/typescript-estree": {
178
+ "minimatch": "^11.0.4"
179
+ }
173
180
  }
174
181
  }