zames_pro 2.88.1 → 2.89.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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.89.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - **config:** two-level /config menu with groups and sections
15
+ - **config:** diagnostics settings (post-edit check command, timeout, output cap) are now editable from `/config`
16
+
17
+ ## [2.88.2] - 2026-10-27
18
+
19
+ ### Changed
20
+
21
+ - docs: no manual step — the agent owns every file and every release action
22
+
10
23
  ## [2.88.1] - 2026-10-27
11
24
 
12
25
  ### Changed
package/README.md CHANGED
@@ -485,14 +485,15 @@ hidden until the first answer delivers it.
485
485
  Like Codex / Claude Code, zames reads project instructions and reusable
486
486
  workflows from your repository and from `~/.zames`.
487
487
 
488
- - **AGENTS.md** — project instructions. Put one in the repo root (or in any
489
- parent folder of the working directory). Global instructions live in
488
+ - **AGENTS.md** — project instructions, in the repo root (or any parent folder
489
+ of the working directory); the agent writes it with `/init`. Global
490
+ instructions live in
490
491
  `~/.zames/AGENTS.md` (and `~/.claude/CLAUDE.md`). Run `/init` and the agent
491
492
  will explore the project and write an AGENTS.md based on the real build/test
492
493
  commands and conventions (use `/init --force` to overwrite an existing file).
493
494
  - **MEMORY.md** — durable notes that persist between sessions. The agent
494
- appends useful facts here; you can edit it by hand, or add one from the
495
- prompt with `/remember <text>`.
495
+ appends useful facts here as it works; `/remember <text>` adds one from the
496
+ prompt, and any `/task`-style edit through the agent keeps the file in sync.
496
497
  - **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
497
498
  `description`, optional `allowed-tools`, `user-invokable`) plus any helper
498
499
  files. Discovered under `.zames/skills/`, `.claude/skills/`,
@@ -1230,14 +1231,15 @@ zames --task "..." --output-format jsonl \
1230
1231
  Как в Codex / Claude Code, zames читает инструкции проекта и переиспользуемые
1231
1232
  процессы из вашего репозитория и из `~/.zames`.
1232
1233
 
1233
- - **AGENTS.md** — инструкции проекта. Положите его в корень репозитория (или в
1234
- любую родительскую папку рабочей директории). Глобальные инструкции живут в
1234
+ - **AGENTS.md** — инструкции проекта, лежит в корне репозитория (или в любой
1235
+ родительской папке рабочей директории); агент создаёт его командой `/init`.
1236
+ Глобальные инструкции живут в
1235
1237
  `~/.zames/AGENTS.md` (и `~/.claude/CLAUDE.md`). Запустите `/init`, и агент
1236
1238
  изучит проект и напишет AGENTS.md на основе реальных команд сборки/тестов и
1237
1239
  соглашений (`/init --force` перезапишет существующий файл).
1238
1240
  - **MEMORY.md** — долговечные заметки, переживающие сессии. Агент дописывает
1239
- сюда полезные факты; можно править вручную или добавить из промпта через
1240
- `/remember <text>`.
1241
+ сюда полезные факты по ходу работы; `/remember <text>` добавляет заметку из
1242
+ промпта — файл остаётся согласованным без ручного редактирования.
1241
1243
  - **Навыки** — папка с файлом `SKILL.md` (YAML-фронтматтер: `name`,
1242
1244
  `description`, опционально `allowed-tools`, `user-invokable`) плюс любые
1243
1245
  вспомогательные файлы. Ищутся в `.zames/skills/`, `.claude/skills/`,
@@ -53,17 +53,25 @@ export function configValueHint(field, t) {
53
53
  return field.type;
54
54
  }
55
55
  /**
56
- * Render the full settings LIST as an array of lines (grouped by section).
57
- * `get` reads the current value of a path (undefined = default). Returned as
58
- * lines so index.ts only has to print them and the formatting is testable.
56
+ * Render the full settings LIST as an array of lines, TWO levels deep: a
57
+ * top-level group header, then its sections. `get` reads the current value of
58
+ * a path (undefined = default). Returned as lines so index.ts only has to
59
+ * print them and the formatting is testable.
59
60
  */
60
61
  export function formatConfigList(fields, get, t) {
61
62
  const out = [theme.system(t('cfg.title'))];
63
+ let lastTop = '';
62
64
  let lastGroup = '';
63
65
  for (const f of fields) {
66
+ const top = t(f.topKey);
67
+ if (top !== lastTop) {
68
+ out.push(theme.system(theme.bold('\n' + top.toUpperCase())));
69
+ lastTop = top;
70
+ lastGroup = '';
71
+ }
64
72
  const g = t(f.groupKey);
65
73
  if (g !== lastGroup) {
66
- out.push(theme.system('\n ' + g));
74
+ out.push(theme.system(' ' + g));
67
75
  lastGroup = g;
68
76
  }
69
77
  const shown = configListValue(f, get(f.path), t);
@@ -1,14 +1,13 @@
1
- // Interactive settings menu for the /config command.
2
- //
3
- // The menu renders a list of parameters grouped by sections and lets you
4
- // change them with the arrow keys: boolean/enum toggle in place, number/string
5
- // prompt for input. All strings come from i18n (cfg.*), so the menu is
6
- // fully localized.
1
+ // Interactive settings menu for the /config command — the NON-Ink fallback
2
+ // (used when ui.tui is off and in tests). The Ink menu in src/tui/ConfigMenu.tsx
3
+ // is the primary one; this one mirrors its two-level navigation: `/config`
4
+ // shows the GROUPS, Enter opens one, Esc goes back to the group list.
7
5
  //
8
6
  // The module doesn't know about config.ts directly (except for the schema
9
7
  // types): the caller reads and writes values through callbacks. This makes the
10
8
  // menu easy to test and reuse.
11
9
  import { theme } from './theme.js';
10
+ import { CONFIG_TOP_GROUPS } from './config.js';
12
11
  import { visLen } from './input.js';
13
12
  const ESC = String.fromCharCode(27);
14
13
  // Strip ANSI sequences to compute the visible length of a line. A local copy
@@ -41,8 +40,8 @@ function groupLabel(field, t) {
41
40
  }
42
41
  /**
43
42
  * Show the interactive menu. Returns a promise that resolves when the
44
- * user exits (q/Esc/Ctrl+C). In a non-TTY it throws — the caller
45
- * must show a text list.
43
+ * user exits (q/Esc on the group list, Ctrl+C). In a non-TTY it throws — the
44
+ * caller must show a text list.
46
45
  */
47
46
  export function runConfigMenu(opts) {
48
47
  const input = opts.input || process.stdin;
@@ -51,8 +50,14 @@ export function runConfigMenu(opts) {
51
50
  if (!input.isTTY || !input.setRawMode) {
52
51
  return Promise.reject(new Error(t('cfg.menu.notty')));
53
52
  }
53
+ const allGroups = opts.topGroups || [...CONFIG_TOP_GROUPS];
54
+ // A group with no visible field is not offered at all.
55
+ const groups = allGroups.filter((g) => fields.some((f) => f.topKey === g && !f.advanced));
54
56
  // cursor indexes the VISIBLE (filtered) list, not the full `fields`.
55
57
  let cursor = 0;
58
+ // null = the group list (level 1); a topKey = that group's settings.
59
+ let openGroup = null;
60
+ let groupCursor = 0;
56
61
  let editing = false;
57
62
  let editBuf = '';
58
63
  // Reverse/forward filter over the list (/ to enter, typing narrows it).
@@ -68,13 +73,16 @@ export function runConfigMenu(opts) {
68
73
  input.setRawMode(true);
69
74
  input.resume();
70
75
  output.write(ESC + '[?25l'); // hide the cursor while rendering
71
- // Fields that match the current filter (path, label or group contains the
72
- // query, case-insensitive). Empty query -> everything.
76
+ // Fields that match the current filter (path, label or section contains the
77
+ // query, case-insensitive) INSIDE the open group. Empty query -> everything.
73
78
  const visible = () => {
79
+ if (!openGroup)
80
+ return [];
81
+ const inGroup = fields.filter((f) => f.topKey === openGroup && !f.advanced);
74
82
  const q = filterQuery.trim().toLowerCase();
75
83
  if (!q)
76
- return fields;
77
- return fields.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
84
+ return inGroup;
85
+ return inGroup.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
78
86
  .toLowerCase()
79
87
  .includes(q));
80
88
  };
@@ -100,6 +108,27 @@ export function runConfigMenu(opts) {
100
108
  return String(cur) !== String(def);
101
109
  }
102
110
  };
111
+ const enterGroup = (topKey) => {
112
+ openGroup = topKey;
113
+ cursor = 0;
114
+ filterQuery = '';
115
+ filterMode = false;
116
+ pendingReset = null;
117
+ message = null;
118
+ editing = false;
119
+ editBuf = '';
120
+ };
121
+ const leaveGroup = () => {
122
+ const idx = groups.indexOf(openGroup || '');
123
+ openGroup = null;
124
+ groupCursor = idx >= 0 ? idx : 0;
125
+ filterQuery = '';
126
+ filterMode = false;
127
+ pendingReset = null;
128
+ message = null;
129
+ editing = false;
130
+ editBuf = '';
131
+ };
103
132
  // Menu rendering: lines go top to bottom, and we return the cursor to the
104
133
  // start of the block via relative moves so we don't spawn blank lines.
105
134
  let lastRows = 0;
@@ -125,6 +154,10 @@ export function runConfigMenu(opts) {
125
154
  ' ' +
126
155
  t('cfg.menu.hint')));
127
156
  }
157
+ else if (!openGroup) {
158
+ tail.push(' ' +
159
+ theme.dim(t('cfg.menu.group_hint') + ' · ' + t('cfg.menu.back_hint')));
160
+ }
128
161
  else {
129
162
  tail.push(' ' + theme.dim(t('cfg.menu.hint')));
130
163
  tail.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
@@ -136,63 +169,90 @@ export function runConfigMenu(opts) {
136
169
  // Bias one row to the SAFE side (a spare blank row is harmless, a block
137
170
  // that overflows scrolls the top away and breaks the cursor restore).
138
171
  const maxItemRows = Math.max(3, termRows - 2 - tail.length);
139
- // WINDOW over the fields. We walk BACK from the cursor, counting the rows
140
- // the window would take (a group header consumes a row too), until adding
141
- // one more field would overflow. This guarantees the SELECTED item is always
142
- // on screen — the old index-based scrollTop could push it out when many
143
- // group headers sat between the window top and the cursor.
144
- const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
145
- // Rows needed to draw the window [from, cursor] (inclusive).
146
- const rowsFor = (from) => {
147
- let rows = 0;
148
- for (let i = from; i <= cursor; i++) {
149
- if (i === from || groupStart(i))
150
- rows++;
151
- rows++;
152
- }
153
- return rows;
154
- };
155
- let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
156
- while (start > 0 && rowsFor(start - 1) <= maxItemRows)
157
- start--;
158
172
  const lines = [];
159
- // Title with a position counter ("Settings — 12/49") so the operator knows
160
- // how far down a long list they are; a filter narrows both numbers.
161
- const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
162
- lines.push(theme.bold(t('cfg.menu.title')) + theme.dim(pos));
163
- // Group headers also consume a row, so we stop as soon as the item-row
164
- // budget is used up (the window is advisory; this is the hard cap that
165
- // guarantees the block fits the terminal).
166
- let itemRows = 0;
167
- let lastGroup = '';
168
- for (let i = start; i < vis.length; i++) {
169
- const f = vis[i];
170
- const g = groupLabel(f, t);
171
- // Always print the header for the FIRST shown row, so a windowed list
172
- // still tells the operator which section it starts in.
173
- if (i === start || g !== lastGroup) {
173
+ if (!openGroup) {
174
+ // ---- level 1: the group list ----
175
+ const pos = groups.length
176
+ ? ' ' + (groupCursor + 1) + '/' + groups.length
177
+ : '';
178
+ lines.push(theme.bold(t('cfg.menu.groups_title')) + theme.dim(pos));
179
+ // Window the groups around the cursor.
180
+ let start = Math.max(0, Math.min(groupCursor, groups.length - 1));
181
+ while (start > 0 && groupCursor - start + 1 < maxItemRows)
182
+ start--;
183
+ const end = Math.min(groups.length, start + maxItemRows);
184
+ for (let i = start; i < end; i++) {
185
+ const g = groups[i];
186
+ const selected = i === groupCursor;
187
+ const marker = selected ? theme.prompt('\u276f ') : ' ';
188
+ const label = selected ? theme.prompt(t(g)) : t(g);
189
+ const count = fields.filter((f) => f.topKey === g && !f.advanced).length;
190
+ const changed = fields.filter((f) => f.topKey === g && isModified(f)).length;
191
+ let extra = theme.dim(' ' + t('cfg.menu.group_count', { n: count }));
192
+ if (changed > 0) {
193
+ extra += theme.warn(' ' + t('cfg.menu.group_modified', { n: changed }));
194
+ }
195
+ lines.push(marker + label + extra);
196
+ }
197
+ if (!groups.length) {
198
+ lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
199
+ }
200
+ }
201
+ else {
202
+ // ---- level 2: the settings of the open group ----
203
+ const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
204
+ // Rows needed to draw the window [from, cursor] (inclusive).
205
+ const rowsFor = (from) => {
206
+ let rows = 0;
207
+ for (let i = from; i <= cursor; i++) {
208
+ if (i === from || groupStart(i))
209
+ rows++;
210
+ rows++;
211
+ }
212
+ return rows;
213
+ };
214
+ let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
215
+ while (start > 0 && rowsFor(start - 1) <= maxItemRows)
216
+ start--;
217
+ // Title with a position counter ("Settings — 12/49") so the operator
218
+ // knows how far down the list they are; a filter narrows both numbers.
219
+ const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
220
+ lines.push(theme.bold(t(openGroup) + ' \u203a ' + t('cfg.menu.title')) +
221
+ theme.dim(pos));
222
+ // Group headers also consume a row, so we stop as soon as the item-row
223
+ // budget is used up (the window is advisory; this is the hard cap that
224
+ // guarantees the block fits the terminal).
225
+ let itemRows = 0;
226
+ let lastGroup = '';
227
+ for (let i = start; i < vis.length; i++) {
228
+ const f = vis[i];
229
+ const g = groupLabel(f, t);
230
+ // Always print the header for the FIRST shown row, so a windowed list
231
+ // still tells the operator which section it starts in.
232
+ if (i === start || g !== lastGroup) {
233
+ if (itemRows >= maxItemRows)
234
+ break;
235
+ lines.push(g);
236
+ lastGroup = g;
237
+ itemRows++;
238
+ }
174
239
  if (itemRows >= maxItemRows)
175
240
  break;
176
- lines.push(g);
177
- lastGroup = g;
178
241
  itemRows++;
242
+ const selected = i === cursor;
243
+ const marker = selected ? theme.prompt('\u276f ') : ' ';
244
+ const label = selected
245
+ ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path)
246
+ : t(f.labelKey || f.path);
247
+ const val = displayValue(f, get(f.path), t);
248
+ const valText = selected ? theme.assistant(val) : theme.dim(val);
249
+ const mod = isModified(f) ? theme.warn('*') : '';
250
+ const pathText = selected ? theme.dim(' ' + f.path) : '';
251
+ lines.push(marker + label + theme.dim(' = ') + valText + mod + pathText);
252
+ }
253
+ if (!vis.length) {
254
+ lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
179
255
  }
180
- if (itemRows >= maxItemRows)
181
- break;
182
- itemRows++;
183
- const selected = i === cursor;
184
- const marker = selected ? theme.prompt('\u276f ') : ' ';
185
- const label = selected
186
- ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path)
187
- : t(f.labelKey || f.path);
188
- const val = displayValue(f, get(f.path), t);
189
- const valText = selected ? theme.assistant(val) : theme.dim(val);
190
- const mod = isModified(f) ? theme.warn('*') : '';
191
- const pathText = selected ? theme.dim(' ' + f.path) : '';
192
- lines.push(marker + label + theme.dim(' = ') + valText + mod + pathText);
193
- }
194
- if (!vis.length) {
195
- lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
196
256
  }
197
257
  lines.push(...tail);
198
258
  // Erase the previous block and print the new one. We count VISUAL lines
@@ -207,9 +267,9 @@ export function runConfigMenu(opts) {
207
267
  // reported bug). Overcounting by one is a harmless blank row; undercounting
208
268
  // is not, so we bias to the safe side here.
209
269
  const cols = output.columns || 80;
210
- const usable = Math.max(1, cols - 1);
270
+ const usableCols = Math.max(1, cols - 1);
211
271
  const rowsOf = (s) => {
212
- return Math.max(1, Math.ceil(visCols(s) / usable));
272
+ return Math.max(1, Math.ceil(visCols(s) / usableCols));
213
273
  };
214
274
  if (lastRows > 0) {
215
275
  output.write(ESC + '[' + lastRows + 'A');
@@ -351,30 +411,68 @@ export function runConfigMenu(opts) {
351
411
  let i = 0;
352
412
  while (i < s.length) {
353
413
  if (s.startsWith('\x1b[A', i)) {
354
- const n = visible().length;
355
- if (n > 0)
356
- cursor = (cursor - 1 + n) % n;
357
- message = null;
358
- pendingReset = null;
414
+ if (openGroup) {
415
+ const n = visible().length;
416
+ if (n > 0)
417
+ cursor = (cursor - 1 + n) % n;
418
+ message = null;
419
+ pendingReset = null;
420
+ }
421
+ else if (groups.length) {
422
+ groupCursor = (groupCursor - 1 + groups.length) % groups.length;
423
+ }
359
424
  i += 3;
360
425
  continue;
361
426
  }
362
427
  if (s.startsWith('\x1b[B', i)) {
363
- const n = visible().length;
364
- if (n > 0)
365
- cursor = (cursor + 1) % n;
366
- message = null;
367
- pendingReset = null;
428
+ if (openGroup) {
429
+ const n = visible().length;
430
+ if (n > 0)
431
+ cursor = (cursor + 1) % n;
432
+ message = null;
433
+ pendingReset = null;
434
+ }
435
+ else if (groups.length) {
436
+ groupCursor = (groupCursor + 1) % groups.length;
437
+ }
368
438
  i += 3;
369
439
  continue;
370
440
  }
371
441
  const ch = s[i];
372
- if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
373
- // Erase the menu block from the screen so the config list does not
374
- // linger in the terminal history after Esc.
442
+ // ---------- LEVEL 1: the group list ----------
443
+ if (!openGroup) {
444
+ if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
445
+ // Erase the menu block from the screen so the config list does not
446
+ // linger in the terminal history after Esc.
447
+ cleanup(true);
448
+ return;
449
+ }
450
+ if (ch === 'k' && groups.length) {
451
+ groupCursor = (groupCursor - 1 + groups.length) % groups.length;
452
+ }
453
+ else if (ch === 'j' && groups.length) {
454
+ groupCursor = (groupCursor + 1) % groups.length;
455
+ }
456
+ else if (ch === '\r' || ch === '\n' || ch === ' ') {
457
+ const g = groups[groupCursor];
458
+ if (g)
459
+ enterGroup(g);
460
+ }
461
+ i++;
462
+ continue;
463
+ }
464
+ // ---------- LEVEL 2: the settings of one group ----------
465
+ if (ch === 'q' || ch === 'Q' || ch === '\x03') {
375
466
  cleanup(true);
376
467
  return;
377
468
  }
469
+ // Esc = BACK to the group list (not out of /config): inside a group the
470
+ // operator is one level deep, and quitting from there was a surprise.
471
+ if (ch === ESC) {
472
+ leaveGroup();
473
+ i++;
474
+ continue;
475
+ }
378
476
  if (ch === '/') {
379
477
  filterMode = true;
380
478
  message = null;
@@ -407,8 +505,8 @@ export function runConfigMenu(opts) {
407
505
  pendingReset = null;
408
506
  }
409
507
  else if (ch === 'g') {
410
- // Jump to the first field of the NEXT group (and back to the top after
411
- // the last), so a long grouped list can be navigated quickly.
508
+ // Jump to the first field of the NEXT subsection (and back to the top
509
+ // after the last), so a long grouped list can be navigated quickly.
412
510
  const vis = visible();
413
511
  if (vis.length) {
414
512
  let target = -1;