zames_pro 2.95.6 → 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 +11 -0
- package/dist/agent-loop.js +33 -5
- package/dist/commands.js +15 -0
- package/dist/dev-commands.js +13 -1
- package/dist/extraTools.js +20 -4
- package/dist/i18n/common.js +4 -0
- package/dist/i18n/help.js +24 -0
- package/dist/index.js +29 -0
- package/dist/loop-prompts.js +61 -0
- package/dist/release-policy.js +100 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ 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
|
+
|
|
10
21
|
## [2.95.6] - 2026-11-05
|
|
11
22
|
|
|
12
23
|
### Added
|
package/dist/agent-loop.js
CHANGED
|
@@ -1392,9 +1392,38 @@ export function _setParserBindings(m) {
|
|
|
1392
1392
|
parseToolCall = m.parseToolCall;
|
|
1393
1393
|
responseLooksLikeToolCall = m.responseLooksLikeToolCall;
|
|
1394
1394
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
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/commands.js
CHANGED
|
@@ -1096,6 +1096,13 @@ export function buildImprovePrompt(item) {
|
|
|
1096
1096
|
export const DEV_ONLY_COMMANDS = [
|
|
1097
1097
|
'/improve',
|
|
1098
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',
|
|
1099
1106
|
'/self-review',
|
|
1100
1107
|
'/self-fix',
|
|
1101
1108
|
'/self-done',
|
|
@@ -1168,6 +1175,14 @@ export const SLASH_COMMANDS = [
|
|
|
1168
1175
|
{ name: '/review', key: 'help.cmd.review' },
|
|
1169
1176
|
{ name: '/improve', key: 'help.cmd.improve' },
|
|
1170
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' },
|
|
1171
1186
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
1172
1187
|
{ name: '/loop', key: 'help.cmd.loop' },
|
|
1173
1188
|
{ name: '/cron', key: 'help.cmd.cron' },
|
package/dist/dev-commands.js
CHANGED
|
@@ -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,
|
package/dist/extraTools.js
CHANGED
|
@@ -198,10 +198,26 @@ export function applyUpdateHunk(content, hunkLines) {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
if (!oldLines.length) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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) {
|
package/dist/i18n/common.js
CHANGED
|
@@ -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') +
|
|
@@ -3138,6 +3151,21 @@ async function main() {
|
|
|
3138
3151
|
sendSystemPromptNext = false;
|
|
3139
3152
|
}
|
|
3140
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
|
+
};
|
|
3141
3169
|
/** /goal: set/show/clear the condition. Returns whether it was SET. */
|
|
3142
3170
|
const goalCommand = async (line) => {
|
|
3143
3171
|
const sub = await handleGoal(line);
|
|
@@ -3603,6 +3631,7 @@ async function main() {
|
|
|
3603
3631
|
selfList: () => selfListCommand(''),
|
|
3604
3632
|
selfDiff: (name) => selfDiffCommand(name),
|
|
3605
3633
|
selfApply: (name) => selfApplyCommand(name),
|
|
3634
|
+
cycle: (id, rest) => cycleCommand(id, rest),
|
|
3606
3635
|
});
|
|
3607
3636
|
if (await dispatchCommand(devCommands, trimmed, {
|
|
3608
3637
|
print: (line) => console.log(line),
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
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": {
|