zames_pro 2.60.0 → 2.61.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,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.61.0]
11
+
12
+ ### Added
13
+
14
+ - `/backlog <text>` — record an improvement idea in `BACKLOG.md` without
15
+ implementing it. Deterministic: a fresh `N<n>` id under the matching
16
+ `P0..P3` section; an optional leading `P0..P3` picks the section, the first
17
+ line is the title, the rest the body. `/backlog collapse` prunes the
18
+ archived blocks.
19
+ - BACKLOG.md maintenance (`src/backlog.ts`, pure/tested): `/improve` now
20
+ auto-prunes a finished item's archived `<details>` copy after a successful
21
+ run (the `### X. [x] ... done` summary line is kept), and a startup warning
22
+ fires when BACKLOG.md exceeds 500 lines / 60 KB. The collapser tolerates an
23
+ UNCLOSED `<details>` (a real file had one).
24
+ - Dev-mode self-improvement note: in `--dev` the system prompt tells the model
25
+ it may append ONE short BACKLOG.md bullet when it spots an improvement
26
+ outside the current task (off in a normal run).
27
+
28
+ ### Changed
29
+
30
+ - Self-development commands (`/improve`, `/backlog`, `/self-review`,
31
+ `/self-fix`, `/self-done`, `/self-list`, `/self-diff`, `/self-apply`) are now
32
+ DEV-ONLY: they are hidden from `/help` and the «/» hints, and rejected by the
33
+ main loop, unless the operator runs in dev mode (`--dev` or
34
+ `config.hotReload`). A regular package install no longer advertises them, and
35
+ a hand-typed `/improve` can no longer edit an unrelated project's
36
+ BACKLOG.md. The list is `DEV_ONLY_COMMANDS` / `isDevOnlyCommand()` in
37
+ `src/commands.ts` (pure, tested).
38
+
10
39
  ## [2.60.0]
11
40
 
12
41
  ### Added
@@ -220,7 +249,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
220
249
  - Session banner warns when no saved DeepSeek session exists.
221
250
  - `--no-color` flag (explicit `NO_COLOR`).
222
251
 
223
- [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.60.0...HEAD
252
+ [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.61.0...HEAD
253
+ [2.61.0]: https://github.com/Viqto0r/zames_pro/compare/v2.60.0...v2.61.0
224
254
  [2.60.0]: https://github.com/Viqto0r/zames_pro/compare/v2.59.0...v2.60.0
225
255
  [2.59.0]: https://github.com/Viqto0r/zames_pro/compare/v2.58.0...v2.59.0
226
256
  [2.58.0]: https://github.com/Viqto0r/zames_pro/compare/v2.57.1...v2.58.0
package/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  [![tests](https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml/badge.svg)](https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](package.json)
8
- [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
9
8
 
10
9
  ![zames logo](https://raw.githubusercontent.com/Viqto0r/zames_pro/master/logo.jpg)
11
10
 
@@ -29,8 +28,6 @@ directory, reads and edits files, runs commands, and commits to git.
29
28
  custom commands from the repo and `~/.zames`, the same idea as Codex / Claude
30
29
  Code.
31
30
  - **MCP support** — plug in external tool servers (e.g. `@playwright/mcp`).
32
- - **Self-review** — `/self-review` snapshots `src/` so the agent can review and
33
- fix itself in a sandbox (`/self-fix`, `/self-apply`).
34
31
  - **Scheduling** — `/loop`, `/cron` and `/jobs` repeat tasks on a timer.
35
32
  - **Bilingual UI** — Russian / English (`/config lang`).
36
33
 
@@ -47,7 +44,7 @@ directory, reads and edits files, runs commands, and commits to git.
47
44
 
48
45
  ## Requirements
49
46
 
50
- - Node.js >= 20 (CI and development use Node 24; see `.nvmrc`)
47
+ - Node.js >= 20
51
48
  - A DeepSeek account. On first launch zames asks for your DeepSeek
52
49
  login/password in the terminal (and stores them in `~/.zames/config.json`
53
50
  after a successful sign-in, so a later logout is handled automatically
@@ -89,7 +86,6 @@ Credentials and toggles can also be edited from `/config`
89
86
 
90
87
  - npm: <https://www.npmjs.com/package/zames_pro>
91
88
  - Changelog: [`CHANGELOG.md`](CHANGELOG.md)
92
- - Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
93
89
  - Security policy: [`SECURITY.md`](SECURITY.md)
94
90
 
95
91
  ## Installation
@@ -247,10 +243,6 @@ Codex CLI:
247
243
  instead of the whole list.
248
244
  - /review [focus] [--staged] — ask the agent to review uncommitted changes
249
245
  and report findings (no code changes).
250
- - /improve [id] — self-improvement loop: take the next open item from
251
- `BACKLOG.md` (or a specific id, e.g. `/improve B3`), implement it, run the
252
- typecheck/lint/tests, mark it done and add a CHANGELOG entry. Nothing is
253
- committed — the changes stay in the working tree for review.
254
246
  - /plan [on|off] — plan (read-only) mode. While it is on, the mutating tools
255
247
  (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush) are removed
256
248
  from the tool set, so the agent can investigate without touching the tree.
@@ -384,7 +376,7 @@ Changes are written to the project `.zamesrc.json` and applied right away
384
376
  (help, messages, spinner) and the language the agent answers you in. The
385
377
  locale lives in `ui.locale` in the config file.
386
378
 
387
- Agent data is stored in `~/.zames`: browser profile, logs, undo history, self-review snapshots.
379
+ Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
388
380
 
389
381
  ## FAQ
390
382
 
@@ -6,7 +6,7 @@ import { translate } from './i18n.js';
6
6
  import { substituteAttachmentMarkers } from './commands.js';
7
7
  import { normText } from './browser.js';
8
8
  import { loadHooks, runPreToolUse, runPostToolUse, } from './hooks.js';
9
- export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, }) {
9
+ export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, selfImprovement = false, }) {
10
10
  // Resolve the hook config ONCE per task: a read per tool call would be
11
11
  // wasteful, and a mid-task edit of hooks.json is not something to chase.
12
12
  // `undefined` means "read .zames/hooks.json"; an explicit null disables
@@ -102,6 +102,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
102
102
  gitContext: gitText,
103
103
  locale,
104
104
  context,
105
+ selfImprovement,
105
106
  });
106
107
  transcript?.log('system_prompt', {
107
108
  length: systemPrompt.length,
@@ -0,0 +1,166 @@
1
+ import { parseBacklogItems } from './commands.js';
2
+ // BACKLOG.md maintenance — the pure half of /backlog and the automatic pruning
3
+ // that runs after /improve. BACKLOG.md is the agent's own improvement-notes
4
+ // file (gitignored); it is READ by /improve, so keeping it small matters —
5
+ // every finished item used to keep a full copy of its original text inside a
6
+ // <details> block, and the file grew without bound.
7
+ //
8
+ // Everything here is pure and unit-tested; index.ts only reads/writes the file.
9
+ const NL = String.fromCharCode(10);
10
+ // Agent-appended items use this id prefix. A distinct letter keeps them apart
11
+ // from the hand-numbered A/B/C/D/E entries, so it is obvious which notes the
12
+ // agent added on its own.
13
+ const NEW_ITEM_PREFIX = 'N';
14
+ // Past these thresholds BACKLOG.md costs more tokens (on /improve) than it is
15
+ // worth, so the operator is nudged to collapse it.
16
+ export const BACKLOG_WARN_LINES = 500;
17
+ export const BACKLOG_WARN_CHARS = 60_000;
18
+ function squashBlankLines(lines) {
19
+ const out = [];
20
+ let blanks = 0;
21
+ for (const line of lines) {
22
+ if (line.trim() === '') {
23
+ blanks++;
24
+ if (blanks > 2)
25
+ continue;
26
+ }
27
+ else {
28
+ blanks = 0;
29
+ }
30
+ out.push(line);
31
+ }
32
+ return out;
33
+ }
34
+ // A real, kept heading: a `## P<n>` section or a `### X<n>.` item heading that
35
+ // is NOT the archived copy (archived copies carry `~~` right after the id).
36
+ function isRealBoundary(line) {
37
+ const t = line.trim();
38
+ if (/^##\s/.test(t))
39
+ return true;
40
+ return /^###\s+[A-Z]\d+[.]/.test(t) && !/^###\s+[A-Z]\d+[.]\s+~~/.test(t);
41
+ }
42
+ function isDetailsOpen(line) {
43
+ return line.trim().startsWith('<details');
44
+ }
45
+ function isArchivedHeading(line) {
46
+ return /^###\s+[A-Z]\d+[.]\s+~~/.test(line.trim());
47
+ }
48
+ /**
49
+ * Remove archived copies of finished items. A finished item keeps its
50
+ * `### X. [x] ... done` summary line; the `### X. ~~original~~` copy and its
51
+ * body (normally wrapped in a <details> block) are dropped.
52
+ *
53
+ * Deliberately tolerant of an UNCLOSED <details>: a real backlog item was
54
+ * written as `### A1. [x] ...` then `<details>...` with no `</details>`, so the
55
+ * whole file became one nested block. Keying the end of a block on the next
56
+ * REAL heading (a `### X.` without `~~`, or a `## ` section) — not only on
57
+ * `</details>` — keeps the summary lines and the open items even then. Pure and
58
+ * unit-tested; a heading that looks like an item INSIDE an archive is skipped
59
+ * because it carries `~~`.
60
+ */
61
+ export function collapseBacklog(text) {
62
+ const src = String(text ?? '');
63
+ const lines = src.split(NL);
64
+ const out = [];
65
+ let collapsed = 0;
66
+ let i = 0;
67
+ while (i < lines.length) {
68
+ if (isDetailsOpen(lines[i]) || isArchivedHeading(lines[i])) {
69
+ // Skip to the end of the archive: the first `</details>` OR the first
70
+ // real heading, whichever comes first (see the note above).
71
+ let j = i + 1;
72
+ while (j < lines.length) {
73
+ const t = lines[j].trim();
74
+ if (t.startsWith('</details>')) {
75
+ j++;
76
+ break;
77
+ }
78
+ if (isRealBoundary(t))
79
+ break;
80
+ j++;
81
+ }
82
+ collapsed++;
83
+ i = j;
84
+ continue;
85
+ }
86
+ out.push(lines[i]);
87
+ i++;
88
+ }
89
+ const next = squashBlankLines(out).join(NL);
90
+ return { text: next, changed: next !== src, collapsed };
91
+ }
92
+ export function backlogStats(text) {
93
+ const src = String(text ?? '');
94
+ const items = parseBacklogItems(src);
95
+ let archived = 0;
96
+ for (const line of src.split(NL)) {
97
+ if (line.trim().startsWith('<details'))
98
+ archived++;
99
+ }
100
+ return {
101
+ lines: src.split(NL).length,
102
+ chars: src.length,
103
+ open: items.filter((i) => i.status === 'open').length,
104
+ done: items.filter((i) => i.status === 'done').length,
105
+ archived,
106
+ };
107
+ }
108
+ /** True when the file is big enough to deserve a collapse nudge. */
109
+ export function backlogNeedsPruning(stats) {
110
+ return stats.lines > BACKLOG_WARN_LINES || stats.chars > BACKLOG_WARN_CHARS;
111
+ }
112
+ /** The next free N<n> id not yet used in the file. */
113
+ export function nextBacklogId(text) {
114
+ let max = 0;
115
+ const re = /^###[ ]+([A-Z])([0-9]+)[.]/gm;
116
+ let m;
117
+ while ((m = re.exec(String(text ?? '')))) {
118
+ if (m[1] === NEW_ITEM_PREFIX)
119
+ max = Math.max(max, Number(m[2]));
120
+ }
121
+ return NEW_ITEM_PREFIX + (max + 1);
122
+ }
123
+ /**
124
+ * Insert a new OPEN item under the matching `## P<n>` section (or append a new
125
+ * section at the end). Returns the new file text and the generated id. Pure;
126
+ * the caller writes the file.
127
+ */
128
+ export function appendBacklogItem(text, item) {
129
+ const src = String(text ?? '');
130
+ const title = String(item.title ?? '')
131
+ .trim()
132
+ .replace(/ {2,}/g, ' ');
133
+ const prio = /^P[0-3]$/i.test(String(item.priority ?? '').trim())
134
+ ? String(item.priority).trim().toUpperCase()
135
+ : 'P2';
136
+ const note = String(item.note ?? '').trim();
137
+ const id = nextBacklogId(src);
138
+ const entry = ['### ' + id + '. ' + title, ''];
139
+ if (note) {
140
+ for (const l of note.split(NL))
141
+ entry.push(l);
142
+ entry.push('');
143
+ }
144
+ const lines = src.split(NL);
145
+ const headerRe = new RegExp('^##[ ]+' + prio + '(?![0-9])');
146
+ let at = -1;
147
+ for (let i = 0; i < lines.length; i++) {
148
+ if (headerRe.test(lines[i])) {
149
+ // Insert right after the header and any blank lines that follow it, so
150
+ // the newest note sits at the top of its priority block.
151
+ let j = i + 1;
152
+ while (j < lines.length && lines[j].trim() === '')
153
+ j++;
154
+ at = j;
155
+ break;
156
+ }
157
+ }
158
+ if (at === -1) {
159
+ while (lines.length && lines[lines.length - 1].trim() === '')
160
+ lines.pop();
161
+ const out = lines.concat(['', '## ' + prio, '', ...entry]).join(NL);
162
+ return { text: out, id };
163
+ }
164
+ const out = lines.slice(0, at).concat(entry, lines.slice(at)).join(NL);
165
+ return { text: out, id };
166
+ }
package/dist/commands.js CHANGED
@@ -781,6 +781,28 @@ export function buildImprovePrompt(item) {
781
781
  '(files changed, tests run, whether BACKLOG/CHANGELOG were updated).');
782
782
  }
783
783
  // ---------- /review ----------
784
+ // Commands that only make sense while developing zames itself. A regular user
785
+ // who installed the package must not see them in /help or the «/» hints, and
786
+ // the main loop rejects them unless dev mode is on (--dev / config.hotReload).
787
+ // Kept here (pure) so the list has ONE source of truth and is unit-tested.
788
+ export const DEV_ONLY_COMMANDS = [
789
+ '/improve',
790
+ '/backlog',
791
+ '/self-review',
792
+ '/self-fix',
793
+ '/self-done',
794
+ '/self-list',
795
+ '/self-diff',
796
+ '/self-apply',
797
+ ];
798
+ /** True when the typed line invokes a dev-only command. */
799
+ export function isDevOnlyCommand(text) {
800
+ const name = String(text ?? '')
801
+ .trim()
802
+ .split(/\s+/)[0]
803
+ .toLowerCase();
804
+ return DEV_ONLY_COMMANDS.find((c) => c === name) || null;
805
+ }
784
806
  export function buildReviewPrompt(focus, hasStaged = false) {
785
807
  const scope = hasStaged ? 'staged' : 'uncommitted';
786
808
  const f = String(focus ?? '').trim();
package/dist/compact.js CHANGED
@@ -14,7 +14,7 @@ export const COMPACT_FALLBACK_LIMIT = 40;
14
14
  * The caller owns the browser and the throttle; this function only sends.
15
15
  */
16
16
  export async function performCompact(opts) {
17
- const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, } = opts;
17
+ const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, selfImprovement = false, } = opts;
18
18
  const t = translate(locale);
19
19
  const log = (msg) => {
20
20
  if (!quiet)
@@ -124,7 +124,7 @@ export async function performCompact(opts) {
124
124
  });
125
125
  // 2) New chat + system prompt + the summary as the first message.
126
126
  await browser.newChat();
127
- await browser.ask(buildSystemPrompt({ workdir, tools, locale }), {
127
+ await browser.ask(buildSystemPrompt({ workdir, tools, locale, selfImprovement }), {
128
128
  timeout: 60_000,
129
129
  agent: false,
130
130
  });
package/dist/i18n.js CHANGED
@@ -259,6 +259,34 @@ export const CATALOG = {
259
259
  ru: 'Улучшение {id}: {title}',
260
260
  en: 'Improving {id}: {title}',
261
261
  },
262
+ 'improve.collapsed': {
263
+ ru: 'BACKLOG свёрнут: удалено архивных блоков — {n}.',
264
+ en: 'BACKLOG collapsed: {n} archived block(s) removed.',
265
+ },
266
+ 'help.cmd.backlog': {
267
+ ru: '/backlog <текст> записать идею в BACKLOG.md',
268
+ en: '/backlog <text> record an idea in BACKLOG.md',
269
+ },
270
+ 'backlog.usage': {
271
+ ru: 'Использование: /backlog <текст идеи>',
272
+ en: 'Usage: /backlog <idea text>',
273
+ },
274
+ 'backlog.none': {
275
+ ru: 'BACKLOG.md не найден в рабочей директории.',
276
+ en: 'BACKLOG.md was not found in the working directory.',
277
+ },
278
+ 'backlog.adding': {
279
+ ru: 'Записываю в BACKLOG: {v}',
280
+ en: 'Recording in BACKLOG: {v}',
281
+ },
282
+ 'backlog.warn': {
283
+ ru: 'BACKLOG.md разросся ({lines} строк, {chars} символов). Сверни сделанное: /backlog collapse',
284
+ en: 'BACKLOG.md has grown large ({lines} lines, {chars} chars). Collapse it: /backlog collapse',
285
+ },
286
+ 'backlog.empty': {
287
+ ru: 'Свёртывать нечего: архивных блоков нет.',
288
+ en: 'Nothing to collapse: there are no archived blocks.',
289
+ },
262
290
  'help.cmd.compact': {
263
291
  ru: '/compact сжать историю и открыть новый чат с резюме',
264
292
  en: '/compact compact the history and open a new chat with the summary',
@@ -680,6 +708,10 @@ export const CATALOG = {
680
708
  ru: 'Неизвестная команда: {v}. Набери /help.',
681
709
  en: 'Unknown command: {v}. Type /help.',
682
710
  },
711
+ 'msg.dev_only': {
712
+ ru: 'Команда {v} доступна только в dev-режиме (--dev или hotReload).',
713
+ en: 'The {v} command is only available in dev mode (--dev or hotReload).',
714
+ },
683
715
  'msg.bye': { ru: 'Выход.', en: 'Bye.' },
684
716
  'shell.empty': {
685
717
  ru: 'После ! нужна команда, например !git status.',
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
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, 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';
21
+ import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, 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';
@@ -192,6 +192,7 @@ const RELOADABLE = [
192
192
  'spinner',
193
193
  'config',
194
194
  'fsutil',
195
+ 'backlog',
195
196
  ];
196
197
  const mod = {
197
198
  createTools,
@@ -204,6 +205,10 @@ const mod = {
204
205
  closeWeb,
205
206
  createSpinner,
206
207
  createMcpPool: null,
208
+ collapseBacklog: null,
209
+ appendBacklogItem: null,
210
+ backlogStats: null,
211
+ backlogNeedsPruning: null,
207
212
  };
208
213
  async function reloadModules() {
209
214
  const stamp = Date.now();
@@ -242,6 +247,14 @@ async function reloadModules() {
242
247
  mod.createSpinner = pick('spinner', 'createSpinner');
243
248
  if (pick('mcp', 'createMcpPool'))
244
249
  mod.createMcpPool = pick('mcp', 'createMcpPool');
250
+ if (pick('backlog', 'collapseBacklog'))
251
+ mod.collapseBacklog = pick('backlog', 'collapseBacklog');
252
+ if (pick('backlog', 'appendBacklogItem'))
253
+ mod.appendBacklogItem = pick('backlog', 'appendBacklogItem');
254
+ if (pick('backlog', 'backlogStats'))
255
+ mod.backlogStats = pick('backlog', 'backlogStats');
256
+ if (pick('backlog', 'backlogNeedsPruning'))
257
+ mod.backlogNeedsPruning = pick('backlog', 'backlogNeedsPruning');
245
258
  return { count: loaded.size, errors };
246
259
  }
247
260
  // Initial load so that mod.buildSystemPrompt and the rest are populated.
@@ -297,6 +310,42 @@ function warnIfBrowserChanged() {
297
310
  }
298
311
  // ---------- helpers ----------
299
312
  function printHelp() {
313
+ // Self-development commands (backlog / self-review) are shown ONLY in dev
314
+ // mode. A regular package install must not advertise them to the user — and
315
+ // the main loop rejects them too (see the guard at the dispatch).
316
+ const devHelp = devMode
317
+ ? ' ' +
318
+ t('help.cmd.improve') +
319
+ '\n ' +
320
+ t('help.cmd.backlog') +
321
+ '\n ' +
322
+ t('help.cmd.debug_dom') +
323
+ '\n ' +
324
+ t('help.cmd.help') +
325
+ '\n ' +
326
+ t('help.cmd.exit') +
327
+ '\n\n' +
328
+ theme.bold(t('help.self_review')) +
329
+ '\n ' +
330
+ t('help.self.review') +
331
+ '\n ' +
332
+ t('help.self.fix') +
333
+ '\n ' +
334
+ t('help.self.done') +
335
+ '\n ' +
336
+ t('help.self.list') +
337
+ '\n ' +
338
+ t('help.self.diff') +
339
+ '\n ' +
340
+ t('help.self.apply') +
341
+ '\n\n'
342
+ : ' ' +
343
+ t('help.cmd.debug_dom') +
344
+ '\n ' +
345
+ t('help.cmd.help') +
346
+ '\n ' +
347
+ t('help.cmd.exit') +
348
+ '\n';
300
349
  console.log(`
301
350
  ${theme.bold('zames')} — ${t('app.tagline')}
302
351
 
@@ -388,19 +437,7 @@ ${theme.bold(t('help.sec.files'))}
388
437
  ${t('help.cmd.doctor')}
389
438
  ${t('help.cmd.add_dir')}
390
439
  ${t('help.cmd.review')}
391
- ${t('help.cmd.improve')}
392
- ${t('help.cmd.debug_dom')}
393
- ${t('help.cmd.help')}
394
- ${t('help.cmd.exit')}
395
-
396
- ${theme.bold(t('help.self_review'))}
397
- ${t('help.self.review')}
398
- ${t('help.self.fix')}
399
- ${t('help.self.done')}
400
- ${t('help.self.list')}
401
- ${t('help.self.diff')}
402
- ${t('help.self.apply')}
403
- ${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + d.name.padEnd(24) + ' ' + d.description).join('\n') : ''}
440
+ ${devHelp}${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + d.name.padEnd(24) + ' ' + d.description).join('\n') : ''}
404
441
 
405
442
  ${theme.bold(t('help.files'))}
406
443
  ${t('help.files.logs')} ${config.transcript.dir}
@@ -421,6 +458,12 @@ function dirLabel(p) {
421
458
  // List of slash commands for completion when you type «/» (Tab — complete).
422
459
  // The description is localized by the help.cmd.* key at display time — see
423
460
  // buildSlashCommands().
461
+ //
462
+ // Dev-only commands (backlog-driven self-improvement, self-review snapshots)
463
+ // are filtered out of the hints and rejected by the main loop unless the
464
+ // operator is in dev mode — a regular package install must never advertise
465
+ // them, and a muscle-memory `/improve` must not edit the user's BACKLOG.md.
466
+ // The list itself lives in commands.ts (`isDevOnlyCommand`), one source.
424
467
  const SLASH_COMMANDS = [
425
468
  { name: '/help', key: 'help.cmd.help' },
426
469
  { name: '/new', key: 'help.cmd.new' },
@@ -450,6 +493,7 @@ const SLASH_COMMANDS = [
450
493
  { name: '/add-dir', key: 'help.cmd.add_dir' },
451
494
  { name: '/review', key: 'help.cmd.review' },
452
495
  { name: '/improve', key: 'help.cmd.improve' },
496
+ { name: '/backlog', key: 'help.cmd.backlog' },
453
497
  { name: '/goal', key: 'help.cmd.goal' },
454
498
  { name: '/loop', key: 'help.cmd.loop' },
455
499
  { name: '/cron', key: 'help.cmd.cron' },
@@ -548,8 +592,10 @@ async function expandSlashTarget(workdir, name, rest) {
548
592
  return null;
549
593
  }
550
594
  // Slash-command descriptions in the current language (for LineEditor hints).
595
+ // Dev-only commands are filtered out unless the operator is in dev mode, so a
596
+ // regular install never advertises (or accepts) them.
551
597
  function buildSlashCommands() {
552
- const base = SLASH_COMMANDS.map((c) => ({
598
+ const base = SLASH_COMMANDS.filter((c) => devMode || !isDevOnlyCommand(c.name)).map((c) => ({
553
599
  name: c.name,
554
600
  description: t(c.key),
555
601
  }));
@@ -1056,7 +1102,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
1056
1102
  }
1057
1103
  // ---------- task runner ----------
1058
1104
  async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
1059
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, } = opts;
1105
+ const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, } = opts;
1060
1106
  // In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
1061
1107
  // Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
1062
1108
  // mode (pipes) — a regular spinner + watchInput.
@@ -1166,6 +1212,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
1166
1212
  autoCompactPct,
1167
1213
  contextLimit,
1168
1214
  getTokenUsage,
1215
+ selfImprovement,
1169
1216
  });
1170
1217
  // The loop may end WITHOUT a model answer: an exhausted iteration
1171
1218
  // limit or an ask() watchdog (the model stopped responding). In that
@@ -1402,6 +1449,7 @@ async function main() {
1402
1449
  transcript,
1403
1450
  freshChat,
1404
1451
  sendSystemPrompt,
1452
+ selfImprovement: devMode,
1405
1453
  onChatReady: (chatId) => {
1406
1454
  if (chatId)
1407
1455
  saveLastChat(chatId, currentWorkdir);
@@ -1423,6 +1471,21 @@ async function main() {
1423
1471
  if (!authMarkerExists()) {
1424
1472
  console.log(theme.warn(t('msg.first_login_hint')));
1425
1473
  }
1474
+ // Nudge when BACKLOG.md has grown large: /improve reads the whole file, so a
1475
+ // bloated backlog costs tokens on every self-improvement run. Best-effort.
1476
+ if (devMode) {
1477
+ try {
1478
+ const bl = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
1479
+ const st = mod.backlogStats(bl);
1480
+ if (mod.backlogNeedsPruning(st)) {
1481
+ console.log(theme.warn(t('backlog.warn', {
1482
+ lines: String(st.lines),
1483
+ chars: String(st.chars),
1484
+ })));
1485
+ }
1486
+ }
1487
+ catch { }
1488
+ }
1426
1489
  let freshChatNext = true;
1427
1490
  let sendSystemPromptNext = true;
1428
1491
  // Resuming an existing chat already has the system-prompt at its start, so
@@ -2226,6 +2289,16 @@ async function main() {
2226
2289
  const lower = trimmed.toLowerCase();
2227
2290
  if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
2228
2291
  break;
2292
+ // Dev-only commands (backlog / self-review) must not run outside dev mode.
2293
+ // Hiding them from /help is not enough: a user could type the command by
2294
+ // hand, and it would rewrite their project's BACKLOG.md or snapshot src/.
2295
+ if (!devMode) {
2296
+ const devCmd = isDevOnlyCommand(lower);
2297
+ if (devCmd) {
2298
+ console.error(theme.warn(t('msg.dev_only', { v: devCmd })));
2299
+ continue;
2300
+ }
2301
+ }
2229
2302
  // `!command` — run a shell command directly, without asking the model
2230
2303
  // (like Claude Code's bash mode). The same sandbox guard as the Bash tool
2231
2304
  // applies, so a direct command cannot leave the project either.
@@ -3328,6 +3401,7 @@ async function main() {
3328
3401
  tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
3329
3402
  answerTimeoutMs: config.browser.answerTimeoutMs,
3330
3403
  fallbackLimit: COMPACT_FALLBACK_LIMIT,
3404
+ selfImprovement: devMode,
3331
3405
  });
3332
3406
  if (res.ok) {
3333
3407
  currentChatId = res.chatId;
@@ -3382,12 +3456,80 @@ async function main() {
3382
3456
  }
3383
3457
  },
3384
3458
  getTokenUsage: () => browser.getLastTokenUsage(),
3459
+ selfImprovement: devMode,
3385
3460
  });
3386
3461
  }
3387
3462
  finally {
3388
3463
  if (editor)
3389
3464
  editor.busy = false;
3390
3465
  }
3466
+ // Auto-prune: a finished item keeps its summary line but loses the
3467
+ // archived <details> copy of its original text, so BACKLOG.md does not
3468
+ // grow on every /improve. Best-effort — a read/write failure must not
3469
+ // break the command.
3470
+ try {
3471
+ const raw = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
3472
+ const col = mod.collapseBacklog(raw);
3473
+ if (col.changed) {
3474
+ await fs.writeFile(path.join(currentWorkdir, 'BACKLOG.md'), col.text, 'utf-8');
3475
+ console.log(theme.dim(t('improve.collapsed', { n: String(col.collapsed) })));
3476
+ }
3477
+ }
3478
+ catch { }
3479
+ continue;
3480
+ }
3481
+ if (lower === '/backlog' || lower.startsWith('/backlog ')) {
3482
+ // Record an improvement idea without implementing it. `/backlog
3483
+ // collapse` prunes the archived blocks instead. The LLM only PLACES
3484
+ // the item; the pure helpers in src/backlog.ts own the format.
3485
+ const note = trimmed.slice('/backlog'.length).trim();
3486
+ const backlogFile = path.join(currentWorkdir, 'BACKLOG.md');
3487
+ let backlogText;
3488
+ try {
3489
+ backlogText = await fs.readFile(backlogFile, 'utf-8');
3490
+ }
3491
+ catch {
3492
+ console.error(theme.warn(t('backlog.none')));
3493
+ continue;
3494
+ }
3495
+ if (!note) {
3496
+ console.log(theme.dim(t('backlog.usage')));
3497
+ continue;
3498
+ }
3499
+ if (note === 'collapse') {
3500
+ const col = mod.collapseBacklog(backlogText);
3501
+ if (!col.changed) {
3502
+ console.log(theme.dim(t('backlog.empty')));
3503
+ }
3504
+ else {
3505
+ await fs.writeFile(backlogFile, col.text, 'utf-8');
3506
+ console.log(theme.system(t('improve.collapsed', { n: String(col.collapsed) })));
3507
+ }
3508
+ continue;
3509
+ }
3510
+ // Deterministic append: no model round-trip is needed to record a
3511
+ // one-line note, and a pure helper cannot invent a different id than
3512
+ // the one the operator is shown. An optional leading `P0..P3` sets the
3513
+ // priority; the first line is the title, the rest becomes the body.
3514
+ let priority = 'P2';
3515
+ let body = note;
3516
+ for (const p of ['P0', 'P1', 'P2', 'P3']) {
3517
+ if (note.toUpperCase().startsWith(p + ' ')) {
3518
+ priority = p;
3519
+ body = note.slice(p.length + 1);
3520
+ break;
3521
+ }
3522
+ }
3523
+ const bodyLines = body.split(String.fromCharCode(10));
3524
+ const title = bodyLines[0];
3525
+ const rest = bodyLines.slice(1).join(String.fromCharCode(10)).trim();
3526
+ const res = mod.appendBacklogItem(backlogText, {
3527
+ title,
3528
+ priority,
3529
+ note: rest || undefined,
3530
+ });
3531
+ await fs.writeFile(backlogFile, res.text, 'utf-8');
3532
+ console.log(theme.system(t('backlog.adding', { v: res.id })));
3391
3533
  continue;
3392
3534
  }
3393
3535
  if (lower === '/review' || lower.startsWith('/review ')) {
@@ -3578,6 +3720,7 @@ async function main() {
3578
3720
  answerTimeoutMs: config.browser.answerTimeoutMs,
3579
3721
  quiet: true,
3580
3722
  skipUiLock: true,
3723
+ selfImprovement: devMode,
3581
3724
  });
3582
3725
  if (res.ok) {
3583
3726
  currentChatId = res.chatId;
@@ -3591,6 +3734,7 @@ async function main() {
3591
3734
  getTokenUsage: () => browser.getLastTokenUsage(),
3592
3735
  goal: sessionGoal,
3593
3736
  todosQuery: () => todosSummary(),
3737
+ selfImprovement: devMode,
3594
3738
  }, inputAttachments);
3595
3739
  }
3596
3740
  finally {
@@ -1,4 +1,25 @@
1
1
  import { translate } from './i18n.js';
2
+ // The dev-mode BACKLOG instruction. English (agent-facing) like the rest of the
3
+ // prompt. Deliberately narrow: one short note at most, and only when the idea
4
+ // is OUTSIDE the current task — otherwise the model documents trivia instead
5
+ // of doing the work, and BACKLOG.md grows on every run.
6
+ const BACKLOG_SECTION = `## Self-improvement notes (dev mode)
7
+
8
+ You are running in the operator's dev mode, so this project is zames itself
9
+ and you may improve it. While working, if you notice a concrete improvement
10
+ that is OUTSIDE the current task — a bug in another module, a cleanup, a
11
+ missing test — append ONE short bullet to BACKLOG.md (Write/Edit) instead of
12
+ silently forgetting it, then continue the task.
13
+
14
+ Rules for that note:
15
+ - at most one or two per task; do NOT let it distract from the task;
16
+ - do NOT duplicate an item already in BACKLOG.md (read it first if unsure);
17
+ - keep it to a single line: what, why it matters, and a one-line fix sketch;
18
+ - put it under the matching priority section (P0 bug/data loss, P1 noticeable
19
+ pain, P2 quality, P3 nice-to-have);
20
+ - this is the ONE exception to "do not touch unrelated files". If nothing
21
+ worth noting, add nothing.
22
+ `;
2
23
  /** Render the AGENTS.md / MEMORY / skills / commands blocks of the prompt. */
3
24
  export function renderContextSection(context) {
4
25
  if (!context)
@@ -62,7 +83,7 @@ export function renderContextSection(context) {
62
83
  }
63
84
  return out;
64
85
  }
65
- export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, }) {
86
+ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
66
87
  const t = translate(locale);
67
88
  const toolDescriptions = tools
68
89
  .map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
@@ -75,6 +96,7 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
75
96
  `Each [image#N] / [file#N] marker corresponds to a file the user pasted into the terminal; the file was uploaded to the chat and also saved under <project>/tmp. Look at the images in the chat; for files, read the copy in tmp if you need the contents.\n`
76
97
  : '';
77
98
  const contextSection = renderContextSection(context);
99
+ const selfSection = selfImprovement ? '\n' + BACKLOG_SECTION + '\n' : '';
78
100
  return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
79
101
 
80
102
  You work in the directory: ${workdir}
@@ -127,7 +149,7 @@ task is done (or when you must ask the operator), not between tool calls.
127
149
  You have access to the following tools:
128
150
 
129
151
  ${toolDescriptions}
130
- ${gitSection}${attachSection}${contextSection}
152
+ ${gitSection}${attachSection}${contextSection}${selfSection}
131
153
  ## Choosing the right tool
132
154
 
133
155
  Prefer the most specific tool; a wrong choice wastes a turn:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.60.0",
3
+ "version": "2.61.0",
4
4
  "description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
5
5
  "type": "module",
6
6
  "bin": {