zames_pro 2.60.0 → 2.63.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 +93 -2
- package/README.md +64 -23
- package/dist/agent-loop.js +55 -2
- package/dist/backlog.js +166 -0
- package/dist/checkpoint.js +197 -0
- package/dist/commands.js +157 -0
- package/dist/compact.js +2 -2
- package/dist/config.js +20 -0
- package/dist/context.js +88 -1
- package/dist/i18n.js +121 -0
- package/dist/index.js +390 -26
- package/dist/input.js +3 -2
- package/dist/path-token.js +75 -0
- package/dist/permissions.js +98 -0
- package/dist/system-prompt.js +39 -2
- package/logo-small.jpg +0 -0
- package/package.json +3 -2
- package/scripts/coverage-gate.mjs +2 -0
package/dist/index.js
CHANGED
|
@@ -17,11 +17,13 @@ import { runConfigMenu } from './config-menu.js';
|
|
|
17
17
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
18
18
|
import { Transcript } from './transcript.js';
|
|
19
19
|
import { UndoStore } from './undo.js';
|
|
20
|
+
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
20
21
|
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';
|
|
22
|
+
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, expandCommandArgs, missingCommandArgs, } from './commands.js';
|
|
22
23
|
import { performCompact } from './compact.js';
|
|
23
24
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
24
25
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
26
|
+
import { extractAtFileRefs } from './path-token.js';
|
|
25
27
|
import { closeWeb } from './web.js';
|
|
26
28
|
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
27
29
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
@@ -188,10 +190,12 @@ const RELOADABLE = [
|
|
|
188
190
|
'self-review',
|
|
189
191
|
'diff',
|
|
190
192
|
'undo',
|
|
193
|
+
'checkpoint',
|
|
191
194
|
'transcript',
|
|
192
195
|
'spinner',
|
|
193
196
|
'config',
|
|
194
197
|
'fsutil',
|
|
198
|
+
'backlog',
|
|
195
199
|
];
|
|
196
200
|
const mod = {
|
|
197
201
|
createTools,
|
|
@@ -204,6 +208,10 @@ const mod = {
|
|
|
204
208
|
closeWeb,
|
|
205
209
|
createSpinner,
|
|
206
210
|
createMcpPool: null,
|
|
211
|
+
collapseBacklog: null,
|
|
212
|
+
appendBacklogItem: null,
|
|
213
|
+
backlogStats: null,
|
|
214
|
+
backlogNeedsPruning: null,
|
|
207
215
|
};
|
|
208
216
|
async function reloadModules() {
|
|
209
217
|
const stamp = Date.now();
|
|
@@ -242,6 +250,14 @@ async function reloadModules() {
|
|
|
242
250
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
243
251
|
if (pick('mcp', 'createMcpPool'))
|
|
244
252
|
mod.createMcpPool = pick('mcp', 'createMcpPool');
|
|
253
|
+
if (pick('backlog', 'collapseBacklog'))
|
|
254
|
+
mod.collapseBacklog = pick('backlog', 'collapseBacklog');
|
|
255
|
+
if (pick('backlog', 'appendBacklogItem'))
|
|
256
|
+
mod.appendBacklogItem = pick('backlog', 'appendBacklogItem');
|
|
257
|
+
if (pick('backlog', 'backlogStats'))
|
|
258
|
+
mod.backlogStats = pick('backlog', 'backlogStats');
|
|
259
|
+
if (pick('backlog', 'backlogNeedsPruning'))
|
|
260
|
+
mod.backlogNeedsPruning = pick('backlog', 'backlogNeedsPruning');
|
|
245
261
|
return { count: loaded.size, errors };
|
|
246
262
|
}
|
|
247
263
|
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
@@ -297,6 +313,42 @@ function warnIfBrowserChanged() {
|
|
|
297
313
|
}
|
|
298
314
|
// ---------- helpers ----------
|
|
299
315
|
function printHelp() {
|
|
316
|
+
// Self-development commands (backlog / self-review) are shown ONLY in dev
|
|
317
|
+
// mode. A regular package install must not advertise them to the user — and
|
|
318
|
+
// the main loop rejects them too (see the guard at the dispatch).
|
|
319
|
+
const devHelp = devMode
|
|
320
|
+
? ' ' +
|
|
321
|
+
t('help.cmd.improve') +
|
|
322
|
+
'\n ' +
|
|
323
|
+
t('help.cmd.backlog') +
|
|
324
|
+
'\n ' +
|
|
325
|
+
t('help.cmd.debug_dom') +
|
|
326
|
+
'\n ' +
|
|
327
|
+
t('help.cmd.help') +
|
|
328
|
+
'\n ' +
|
|
329
|
+
t('help.cmd.exit') +
|
|
330
|
+
'\n\n' +
|
|
331
|
+
theme.bold(t('help.self_review')) +
|
|
332
|
+
'\n ' +
|
|
333
|
+
t('help.self.review') +
|
|
334
|
+
'\n ' +
|
|
335
|
+
t('help.self.fix') +
|
|
336
|
+
'\n ' +
|
|
337
|
+
t('help.self.done') +
|
|
338
|
+
'\n ' +
|
|
339
|
+
t('help.self.list') +
|
|
340
|
+
'\n ' +
|
|
341
|
+
t('help.self.diff') +
|
|
342
|
+
'\n ' +
|
|
343
|
+
t('help.self.apply') +
|
|
344
|
+
'\n\n'
|
|
345
|
+
: ' ' +
|
|
346
|
+
t('help.cmd.debug_dom') +
|
|
347
|
+
'\n ' +
|
|
348
|
+
t('help.cmd.help') +
|
|
349
|
+
'\n ' +
|
|
350
|
+
t('help.cmd.exit') +
|
|
351
|
+
'\n';
|
|
300
352
|
console.log(`
|
|
301
353
|
${theme.bold('zames')} — ${t('app.tagline')}
|
|
302
354
|
|
|
@@ -382,29 +434,20 @@ ${theme.bold(t('help.sec.files'))}
|
|
|
382
434
|
${t('help.cmd.transcript')}
|
|
383
435
|
${t('help.cmd.undo')}
|
|
384
436
|
${t('help.cmd.undo_list')}
|
|
437
|
+
${t('help.cmd.rewind')}
|
|
438
|
+
${t('help.cmd.rewind_list')}
|
|
385
439
|
${t('help.cmd.export')}
|
|
386
440
|
${t('help.cmd.cost')}
|
|
387
441
|
${t('help.cmd.copy')}
|
|
388
442
|
${t('help.cmd.doctor')}
|
|
389
443
|
${t('help.cmd.add_dir')}
|
|
390
444
|
${t('help.cmd.review')}
|
|
391
|
-
|
|
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') : ''}
|
|
445
|
+
${devHelp}${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + (d.hint ? d.name + ' ' + d.hint : d.name).padEnd(24) + ' ' + d.description).join('\n') : ''}
|
|
404
446
|
|
|
405
447
|
${theme.bold(t('help.files'))}
|
|
406
448
|
${t('help.files.logs')} ${config.transcript.dir}
|
|
407
449
|
${t('help.files.undo')} ~/.zames/undo
|
|
450
|
+
${t('help.files.checkpoints')} ~/.zames/checkpoints
|
|
408
451
|
${t('help.files.sessions')} ${sessionsDir()}
|
|
409
452
|
${t('help.files.profile')} ~/.zames/profile
|
|
410
453
|
${t('help.files.snapshots')} ~/.zames/snapshots
|
|
@@ -421,6 +464,12 @@ function dirLabel(p) {
|
|
|
421
464
|
// List of slash commands for completion when you type «/» (Tab — complete).
|
|
422
465
|
// The description is localized by the help.cmd.* key at display time — see
|
|
423
466
|
// buildSlashCommands().
|
|
467
|
+
//
|
|
468
|
+
// Dev-only commands (backlog-driven self-improvement, self-review snapshots)
|
|
469
|
+
// are filtered out of the hints and rejected by the main loop unless the
|
|
470
|
+
// operator is in dev mode — a regular package install must never advertise
|
|
471
|
+
// them, and a muscle-memory `/improve` must not edit the user's BACKLOG.md.
|
|
472
|
+
// The list itself lives in commands.ts (`isDevOnlyCommand`), one source.
|
|
424
473
|
const SLASH_COMMANDS = [
|
|
425
474
|
{ name: '/help', key: 'help.cmd.help' },
|
|
426
475
|
{ name: '/new', key: 'help.cmd.new' },
|
|
@@ -441,6 +490,8 @@ const SLASH_COMMANDS = [
|
|
|
441
490
|
{ name: '/reload', key: 'help.cmd.reload' },
|
|
442
491
|
{ name: '/undo', key: 'help.cmd.undo' },
|
|
443
492
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
493
|
+
{ name: '/rewind', key: 'help.cmd.rewind' },
|
|
494
|
+
{ name: '/rewind-list', key: 'help.cmd.rewind_list' },
|
|
444
495
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
445
496
|
{ name: '/diff', key: 'help.cmd.diff' },
|
|
446
497
|
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
@@ -450,6 +501,7 @@ const SLASH_COMMANDS = [
|
|
|
450
501
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
451
502
|
{ name: '/review', key: 'help.cmd.review' },
|
|
452
503
|
{ name: '/improve', key: 'help.cmd.improve' },
|
|
504
|
+
{ name: '/backlog', key: 'help.cmd.backlog' },
|
|
453
505
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
454
506
|
{ name: '/loop', key: 'help.cmd.loop' },
|
|
455
507
|
{ name: '/cron', key: 'help.cmd.cron' },
|
|
@@ -495,9 +547,12 @@ async function refreshDynamicCommands(workdir) {
|
|
|
495
547
|
}
|
|
496
548
|
const cmds = await loadCommands(workdir);
|
|
497
549
|
for (const c of cmds) {
|
|
550
|
+
// B7: carry the declared argument hint as a separate field so the hint is
|
|
551
|
+
// SHOWN in the suggest list but never inserted into the input line.
|
|
498
552
|
out.push({
|
|
499
553
|
name: '/' + c.name,
|
|
500
554
|
description: c.description || t('help.cmd.custom'),
|
|
555
|
+
hint: c.argumentHint,
|
|
501
556
|
});
|
|
502
557
|
}
|
|
503
558
|
}
|
|
@@ -507,16 +562,22 @@ async function refreshDynamicCommands(workdir) {
|
|
|
507
562
|
dynamicCommands = out;
|
|
508
563
|
}
|
|
509
564
|
// Expand a slash target (skill or custom command) into a task string.
|
|
510
|
-
// Returns null when the name is neither a skill nor a custom command.
|
|
565
|
+
// Returns null when the name is neither a skill nor a custom command. When a
|
|
566
|
+
// custom command declares mandatory `arguments:` and some are missing, returns
|
|
567
|
+
// `{ missing }` so the caller can print the usage hint instead of sending an
|
|
568
|
+
// incomplete prompt.
|
|
511
569
|
async function expandSlashTarget(workdir, name, rest) {
|
|
512
570
|
try {
|
|
513
571
|
const { loadSkills, loadCommands, skillBody } = await import('./context.js');
|
|
514
572
|
const commands = await loadCommands(workdir);
|
|
515
573
|
const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
516
574
|
if (cmd) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
575
|
+
// B7: refuse to run when declared positional arguments are missing.
|
|
576
|
+
const missing = missingCommandArgs(cmd.arguments, rest);
|
|
577
|
+
if (missing.length)
|
|
578
|
+
return { missing };
|
|
579
|
+
const body = expandCommandArgs(cmd.body, rest, cmd.arguments);
|
|
580
|
+
return { text: body || rest };
|
|
520
581
|
}
|
|
521
582
|
const skills = await loadSkills(workdir);
|
|
522
583
|
const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
|
|
@@ -539,7 +600,7 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
539
600
|
const extra = rest
|
|
540
601
|
? '\n\nAdditional instructions from the operator: ' + rest
|
|
541
602
|
: '';
|
|
542
|
-
return header + '\n\n' + body + extra;
|
|
603
|
+
return { text: header + '\n\n' + body + extra };
|
|
543
604
|
}
|
|
544
605
|
}
|
|
545
606
|
catch {
|
|
@@ -548,8 +609,10 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
548
609
|
return null;
|
|
549
610
|
}
|
|
550
611
|
// Slash-command descriptions in the current language (for LineEditor hints).
|
|
612
|
+
// Dev-only commands are filtered out unless the operator is in dev mode, so a
|
|
613
|
+
// regular install never advertises (or accepts) them.
|
|
551
614
|
function buildSlashCommands() {
|
|
552
|
-
const base = SLASH_COMMANDS.map((c) => ({
|
|
615
|
+
const base = SLASH_COMMANDS.filter((c) => devMode || !isDevOnlyCommand(c.name)).map((c) => ({
|
|
553
616
|
name: c.name,
|
|
554
617
|
description: t(c.key),
|
|
555
618
|
}));
|
|
@@ -597,6 +660,55 @@ async function resolveAttachPath(workdir, raw) {
|
|
|
597
660
|
}
|
|
598
661
|
return null;
|
|
599
662
|
}
|
|
663
|
+
// Per-file and total caps for `@path` inlining (B5). A reference should SAVE
|
|
664
|
+
// round-trips, not flood the context: a huge file is truncated with a note
|
|
665
|
+
// telling the agent to read the rest itself.
|
|
666
|
+
const AT_REF_FILE_CAP = 60 * 1024;
|
|
667
|
+
const AT_REF_TOTAL_CAP = 200 * 1024;
|
|
668
|
+
// Inline `@path` references found in a task text (B5). Returns the augmented
|
|
669
|
+
// text plus the list of files actually inlined (for the operator notice).
|
|
670
|
+
// Unknown/unreadable references are left in place untouched — they may be part
|
|
671
|
+
// of the prose; we never fail the task over a missing file.
|
|
672
|
+
async function inlineAtRefs(workdir, text) {
|
|
673
|
+
const refs = extractAtFileRefs(text);
|
|
674
|
+
if (!refs.length)
|
|
675
|
+
return { text, inlined: [] };
|
|
676
|
+
const blocks = [];
|
|
677
|
+
const inlined = [];
|
|
678
|
+
let total = 0;
|
|
679
|
+
for (const ref of refs) {
|
|
680
|
+
if (total >= AT_REF_TOTAL_CAP)
|
|
681
|
+
break;
|
|
682
|
+
const file = await resolveAttachPath(workdir, ref);
|
|
683
|
+
if (!file)
|
|
684
|
+
continue;
|
|
685
|
+
let raw;
|
|
686
|
+
try {
|
|
687
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
let body = raw;
|
|
693
|
+
let note = '';
|
|
694
|
+
if (body.length > AT_REF_FILE_CAP) {
|
|
695
|
+
body = body.slice(0, AT_REF_FILE_CAP);
|
|
696
|
+
note = NL + NL + '(truncated at ' + AT_REF_FILE_CAP + ' chars)';
|
|
697
|
+
}
|
|
698
|
+
total += body.length;
|
|
699
|
+
inlined.push(path.relative(workdir, file) || file);
|
|
700
|
+
blocks.push('### ' + ref + NL + '```' + NL + body + NL + '```' + note);
|
|
701
|
+
}
|
|
702
|
+
if (!blocks.length)
|
|
703
|
+
return { text, inlined: [] };
|
|
704
|
+
const section = NL +
|
|
705
|
+
NL +
|
|
706
|
+
'## Files referenced with @ in the task' +
|
|
707
|
+
NL +
|
|
708
|
+
NL +
|
|
709
|
+
blocks.join(NL + NL);
|
|
710
|
+
return { text: text + section, inlined };
|
|
711
|
+
}
|
|
600
712
|
async function cleanTmpDir() {
|
|
601
713
|
try {
|
|
602
714
|
await fs.rm(TMP_DIR, { recursive: true, force: true });
|
|
@@ -1055,8 +1167,29 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
1055
1167
|
}
|
|
1056
1168
|
}
|
|
1057
1169
|
// ---------- task runner ----------
|
|
1170
|
+
// Ask the operator to approve a tool call flagged by an `ask` permission rule
|
|
1171
|
+
// (C1). While the LineEditor owns the terminal it must be paused first, or the
|
|
1172
|
+
// confirmation prompt and the input line fight for the same keys. A non-TTY run
|
|
1173
|
+
// has no way to answer, so it DENIES (never silently allows a guarded call).
|
|
1174
|
+
async function askOperatorConfirm(editor, question) {
|
|
1175
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
1176
|
+
return false;
|
|
1177
|
+
if (editor)
|
|
1178
|
+
editor.pause();
|
|
1179
|
+
try {
|
|
1180
|
+
const a = await promptOnce(question + ' [y/N] ');
|
|
1181
|
+
return /^(y|yes|д|да)$/i.test(a.trim());
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
finally {
|
|
1187
|
+
if (editor)
|
|
1188
|
+
editor.resume();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1058
1191
|
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;
|
|
1192
|
+
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
1193
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
1061
1194
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
1062
1195
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -1152,6 +1285,19 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1152
1285
|
ui.assistant(msg);
|
|
1153
1286
|
},
|
|
1154
1287
|
onWarning: (msg) => ui.warning(msg),
|
|
1288
|
+
onAskPermission: async (info) => {
|
|
1289
|
+
// C1: a rule with action "ask". Show the reason and wait for the
|
|
1290
|
+
// operator. In non-TTY this denies (see askOperatorConfirm).
|
|
1291
|
+
ui.warning(t('perm.ask', { tool: info.tool, reason: info.reason }));
|
|
1292
|
+
const ok = await askOperatorConfirm(editor || null, t('perm.confirm'));
|
|
1293
|
+
ui.warning(ok ? t('perm.allowed') : t('perm.denied'));
|
|
1294
|
+
transcript?.log('permission_ask', {
|
|
1295
|
+
tool: info.tool,
|
|
1296
|
+
reason: info.reason,
|
|
1297
|
+
allowed: ok,
|
|
1298
|
+
});
|
|
1299
|
+
return ok;
|
|
1300
|
+
},
|
|
1155
1301
|
onChatReady,
|
|
1156
1302
|
debugLog: debug,
|
|
1157
1303
|
locale: currentLocale,
|
|
@@ -1166,6 +1312,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1166
1312
|
autoCompactPct,
|
|
1167
1313
|
contextLimit,
|
|
1168
1314
|
getTokenUsage,
|
|
1315
|
+
selfImprovement,
|
|
1169
1316
|
});
|
|
1170
1317
|
// The loop may end WITHOUT a model answer: an exhausted iteration
|
|
1171
1318
|
// limit or an ask() watchdog (the model stopped responding). In that
|
|
@@ -1308,6 +1455,7 @@ async function main() {
|
|
|
1308
1455
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
1309
1456
|
}
|
|
1310
1457
|
const undo = new UndoStore(config.undo);
|
|
1458
|
+
const checkpoints = new CheckpointStore(config.checkpoint);
|
|
1311
1459
|
// MCP servers: optional external tool providers (e.g. @playwright/mcp).
|
|
1312
1460
|
let mcpPool = null;
|
|
1313
1461
|
try {
|
|
@@ -1402,6 +1550,7 @@ async function main() {
|
|
|
1402
1550
|
transcript,
|
|
1403
1551
|
freshChat,
|
|
1404
1552
|
sendSystemPrompt,
|
|
1553
|
+
selfImprovement: devMode,
|
|
1405
1554
|
onChatReady: (chatId) => {
|
|
1406
1555
|
if (chatId)
|
|
1407
1556
|
saveLastChat(chatId, currentWorkdir);
|
|
@@ -1423,6 +1572,21 @@ async function main() {
|
|
|
1423
1572
|
if (!authMarkerExists()) {
|
|
1424
1573
|
console.log(theme.warn(t('msg.first_login_hint')));
|
|
1425
1574
|
}
|
|
1575
|
+
// Nudge when BACKLOG.md has grown large: /improve reads the whole file, so a
|
|
1576
|
+
// bloated backlog costs tokens on every self-improvement run. Best-effort.
|
|
1577
|
+
if (devMode) {
|
|
1578
|
+
try {
|
|
1579
|
+
const bl = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
|
|
1580
|
+
const st = mod.backlogStats(bl);
|
|
1581
|
+
if (mod.backlogNeedsPruning(st)) {
|
|
1582
|
+
console.log(theme.warn(t('backlog.warn', {
|
|
1583
|
+
lines: String(st.lines),
|
|
1584
|
+
chars: String(st.chars),
|
|
1585
|
+
})));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
catch { }
|
|
1589
|
+
}
|
|
1426
1590
|
let freshChatNext = true;
|
|
1427
1591
|
let sendSystemPromptNext = true;
|
|
1428
1592
|
// Resuming an existing chat already has the system-prompt at its start, so
|
|
@@ -2226,6 +2390,16 @@ async function main() {
|
|
|
2226
2390
|
const lower = trimmed.toLowerCase();
|
|
2227
2391
|
if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
|
|
2228
2392
|
break;
|
|
2393
|
+
// Dev-only commands (backlog / self-review) must not run outside dev mode.
|
|
2394
|
+
// Hiding them from /help is not enough: a user could type the command by
|
|
2395
|
+
// hand, and it would rewrite their project's BACKLOG.md or snapshot src/.
|
|
2396
|
+
if (!devMode) {
|
|
2397
|
+
const devCmd = isDevOnlyCommand(lower);
|
|
2398
|
+
if (devCmd) {
|
|
2399
|
+
console.error(theme.warn(t('msg.dev_only', { v: devCmd })));
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2229
2403
|
// `!command` — run a shell command directly, without asking the model
|
|
2230
2404
|
// (like Claude Code's bash mode). The same sandbox guard as the Bash tool
|
|
2231
2405
|
// applies, so a direct command cannot leave the project either.
|
|
@@ -3069,6 +3243,9 @@ async function main() {
|
|
|
3069
3243
|
console.log(theme.system(t('status.undo', {
|
|
3070
3244
|
v: config.undo.enabled ? t('common.on') : t('common.off'),
|
|
3071
3245
|
})));
|
|
3246
|
+
console.log(theme.system(t('status.checkpoint', {
|
|
3247
|
+
v: config.checkpoint.enabled ? t('common.on') : t('common.off'),
|
|
3248
|
+
})));
|
|
3072
3249
|
console.log(theme.system(t('status.transcript', {
|
|
3073
3250
|
v: transcript.file || t('common.off'),
|
|
3074
3251
|
})));
|
|
@@ -3328,6 +3505,7 @@ async function main() {
|
|
|
3328
3505
|
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
3329
3506
|
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
3330
3507
|
fallbackLimit: COMPACT_FALLBACK_LIMIT,
|
|
3508
|
+
selfImprovement: devMode,
|
|
3331
3509
|
});
|
|
3332
3510
|
if (res.ok) {
|
|
3333
3511
|
currentChatId = res.chatId;
|
|
@@ -3382,12 +3560,80 @@ async function main() {
|
|
|
3382
3560
|
}
|
|
3383
3561
|
},
|
|
3384
3562
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3563
|
+
selfImprovement: devMode,
|
|
3385
3564
|
});
|
|
3386
3565
|
}
|
|
3387
3566
|
finally {
|
|
3388
3567
|
if (editor)
|
|
3389
3568
|
editor.busy = false;
|
|
3390
3569
|
}
|
|
3570
|
+
// Auto-prune: a finished item keeps its summary line but loses the
|
|
3571
|
+
// archived <details> copy of its original text, so BACKLOG.md does not
|
|
3572
|
+
// grow on every /improve. Best-effort — a read/write failure must not
|
|
3573
|
+
// break the command.
|
|
3574
|
+
try {
|
|
3575
|
+
const raw = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
|
|
3576
|
+
const col = mod.collapseBacklog(raw);
|
|
3577
|
+
if (col.changed) {
|
|
3578
|
+
await fs.writeFile(path.join(currentWorkdir, 'BACKLOG.md'), col.text, 'utf-8');
|
|
3579
|
+
console.log(theme.dim(t('improve.collapsed', { n: String(col.collapsed) })));
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
catch { }
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
if (lower === '/backlog' || lower.startsWith('/backlog ')) {
|
|
3586
|
+
// Record an improvement idea without implementing it. `/backlog
|
|
3587
|
+
// collapse` prunes the archived blocks instead. The LLM only PLACES
|
|
3588
|
+
// the item; the pure helpers in src/backlog.ts own the format.
|
|
3589
|
+
const note = trimmed.slice('/backlog'.length).trim();
|
|
3590
|
+
const backlogFile = path.join(currentWorkdir, 'BACKLOG.md');
|
|
3591
|
+
let backlogText;
|
|
3592
|
+
try {
|
|
3593
|
+
backlogText = await fs.readFile(backlogFile, 'utf-8');
|
|
3594
|
+
}
|
|
3595
|
+
catch {
|
|
3596
|
+
console.error(theme.warn(t('backlog.none')));
|
|
3597
|
+
continue;
|
|
3598
|
+
}
|
|
3599
|
+
if (!note) {
|
|
3600
|
+
console.log(theme.dim(t('backlog.usage')));
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (note === 'collapse') {
|
|
3604
|
+
const col = mod.collapseBacklog(backlogText);
|
|
3605
|
+
if (!col.changed) {
|
|
3606
|
+
console.log(theme.dim(t('backlog.empty')));
|
|
3607
|
+
}
|
|
3608
|
+
else {
|
|
3609
|
+
await fs.writeFile(backlogFile, col.text, 'utf-8');
|
|
3610
|
+
console.log(theme.system(t('improve.collapsed', { n: String(col.collapsed) })));
|
|
3611
|
+
}
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
// Deterministic append: no model round-trip is needed to record a
|
|
3615
|
+
// one-line note, and a pure helper cannot invent a different id than
|
|
3616
|
+
// the one the operator is shown. An optional leading `P0..P3` sets the
|
|
3617
|
+
// priority; the first line is the title, the rest becomes the body.
|
|
3618
|
+
let priority = 'P2';
|
|
3619
|
+
let body = note;
|
|
3620
|
+
for (const p of ['P0', 'P1', 'P2', 'P3']) {
|
|
3621
|
+
if (note.toUpperCase().startsWith(p + ' ')) {
|
|
3622
|
+
priority = p;
|
|
3623
|
+
body = note.slice(p.length + 1);
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
const bodyLines = body.split(String.fromCharCode(10));
|
|
3628
|
+
const title = bodyLines[0];
|
|
3629
|
+
const rest = bodyLines.slice(1).join(String.fromCharCode(10)).trim();
|
|
3630
|
+
const res = mod.appendBacklogItem(backlogText, {
|
|
3631
|
+
title,
|
|
3632
|
+
priority,
|
|
3633
|
+
note: rest || undefined,
|
|
3634
|
+
});
|
|
3635
|
+
await fs.writeFile(backlogFile, res.text, 'utf-8');
|
|
3636
|
+
console.log(theme.system(t('backlog.adding', { v: res.id })));
|
|
3391
3637
|
continue;
|
|
3392
3638
|
}
|
|
3393
3639
|
if (lower === '/review' || lower.startsWith('/review ')) {
|
|
@@ -3455,6 +3701,86 @@ async function main() {
|
|
|
3455
3701
|
}
|
|
3456
3702
|
continue;
|
|
3457
3703
|
}
|
|
3704
|
+
if (lower === '/rewind-list' || lower === '/checkpoints') {
|
|
3705
|
+
const list = await checkpoints.list(10);
|
|
3706
|
+
if (!list.length) {
|
|
3707
|
+
console.log(theme.system(t('rewind.empty')));
|
|
3708
|
+
}
|
|
3709
|
+
else {
|
|
3710
|
+
console.log(theme.system(t('rewind.list_title')));
|
|
3711
|
+
list.forEach((r, i) => {
|
|
3712
|
+
const stamp = new Date(r.stamp).toLocaleString();
|
|
3713
|
+
const label = r.label ? ' [' + t('rewind.label_backup') + ']' : '';
|
|
3714
|
+
console.log(theme.system(` ${i + 1}. ${stamp} ${r.workdir}${label}`));
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
if (lower === '/rewind' || lower.startsWith('/rewind ')) {
|
|
3720
|
+
const selector = trimmed.slice('/rewind'.length).trim() || '1';
|
|
3721
|
+
if (editor)
|
|
3722
|
+
editor.lock(t('msg.input_locked'));
|
|
3723
|
+
try {
|
|
3724
|
+
const list = await checkpoints.list(10);
|
|
3725
|
+
const target = pickCheckpoint(list, selector);
|
|
3726
|
+
if (!target) {
|
|
3727
|
+
console.error(theme.error(t('rewind.failed', { v: selector })));
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
// Confirmation: a rewind overwrites the working tree, so it must never
|
|
3731
|
+
// happen on a muscle-memory Enter.
|
|
3732
|
+
const stamp = new Date(target.stamp).toLocaleString();
|
|
3733
|
+
let answer = 'n';
|
|
3734
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3735
|
+
// promptOnce reads stdin itself. While the LineEditor owns the
|
|
3736
|
+
// terminal we must hand it over first (the same pause/resume the
|
|
3737
|
+
// /config menu uses), or both readers fight for the same keys.
|
|
3738
|
+
if (editor)
|
|
3739
|
+
editor.pause();
|
|
3740
|
+
try {
|
|
3741
|
+
answer = await promptOnce(theme.warn(t('rewind.confirm', { v: stamp })) + ' [y/N] ');
|
|
3742
|
+
}
|
|
3743
|
+
finally {
|
|
3744
|
+
if (editor)
|
|
3745
|
+
editor.resume();
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
else if (trimmed.indexOf('--yes') !== -1) {
|
|
3749
|
+
// Non-TTY (piped): do not guess. Require an explicit --yes.
|
|
3750
|
+
answer = 'y';
|
|
3751
|
+
}
|
|
3752
|
+
if (!/^(y|yes|д|да)$/i.test(answer.trim())) {
|
|
3753
|
+
console.log(theme.system(t('rewind.cancelled')));
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
const res = await checkpoints.restore(selector);
|
|
3757
|
+
if (res.ok && res.record) {
|
|
3758
|
+
console.log(theme.assistant(t('rewind.reverted', { v: String(res.record.stamp) })));
|
|
3759
|
+
if (res.backup) {
|
|
3760
|
+
console.log(theme.system(t('rewind.backup', { v: String(res.backup.stamp) })));
|
|
3761
|
+
}
|
|
3762
|
+
transcript.log('rewind', { stamp: res.record.stamp });
|
|
3763
|
+
}
|
|
3764
|
+
else {
|
|
3765
|
+
const reasonKey = res.reason === 'not_found'
|
|
3766
|
+
? 'rewind.reason_not_found'
|
|
3767
|
+
: res.reason === 'archive_missing'
|
|
3768
|
+
? 'rewind.reason_archive_missing'
|
|
3769
|
+
: res.reason === 'extract_failed'
|
|
3770
|
+
? 'rewind.reason_extract_failed'
|
|
3771
|
+
: res.reason === 'disabled'
|
|
3772
|
+
? 'rewind.reason_disabled'
|
|
3773
|
+
: null;
|
|
3774
|
+
const reason = reasonKey ? t(reasonKey) : String(res.reason ?? '');
|
|
3775
|
+
console.error(theme.error(t('rewind.failed', { v: reason })));
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
finally {
|
|
3779
|
+
if (editor)
|
|
3780
|
+
editor.unlock();
|
|
3781
|
+
}
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3458
3784
|
if (lower === '/debug-dom') {
|
|
3459
3785
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3460
3786
|
const file = path.join(config.transcript.dir, `dom-${stamp}.html`);
|
|
@@ -3510,10 +3836,21 @@ async function main() {
|
|
|
3510
3836
|
// task. Skills are instructions the agent follows; custom commands are
|
|
3511
3837
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3512
3838
|
let expandedTask = null;
|
|
3839
|
+
let missingArgs = null;
|
|
3513
3840
|
if (lower.startsWith('/')) {
|
|
3514
3841
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3515
3842
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3516
|
-
|
|
3843
|
+
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
3844
|
+
if (res && 'missing' in res)
|
|
3845
|
+
missingArgs = res.missing;
|
|
3846
|
+
else if (res)
|
|
3847
|
+
expandedTask = res.text;
|
|
3848
|
+
}
|
|
3849
|
+
// B7: a custom command with mandatory `arguments:` refuses to run until the
|
|
3850
|
+
// operator supplies them (by position).
|
|
3851
|
+
if (missingArgs) {
|
|
3852
|
+
console.error(theme.error(t('msg.missing_args', { v: missingArgs.join(', ') })));
|
|
3853
|
+
continue;
|
|
3517
3854
|
}
|
|
3518
3855
|
if (lower.startsWith('/') && expandedTask === null) {
|
|
3519
3856
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
@@ -3521,7 +3858,20 @@ async function main() {
|
|
|
3521
3858
|
}
|
|
3522
3859
|
// ---- Regular task (including in review mode) ----
|
|
3523
3860
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3524
|
-
|
|
3861
|
+
// B5: inline any `@path` references into the task so the agent does not
|
|
3862
|
+
// spend a round-trip reading a file the operator already pointed at. Only
|
|
3863
|
+
// existing files are inlined; a bare `@name` in prose is left untouched.
|
|
3864
|
+
let finalTaskText = taskText;
|
|
3865
|
+
{
|
|
3866
|
+
const refRes = await inlineAtRefs(currentWorkdir, taskText);
|
|
3867
|
+
if (refRes.inlined.length) {
|
|
3868
|
+
finalTaskText = refRes.text;
|
|
3869
|
+
console.log(theme.system(t('msg.at_refs', { n: refRes.inlined.length }) +
|
|
3870
|
+
' ' +
|
|
3871
|
+
refRes.inlined.join(', ')));
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
lastTaskText = finalTaskText;
|
|
3525
3875
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3526
3876
|
await autoReload();
|
|
3527
3877
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3530,7 +3880,19 @@ async function main() {
|
|
|
3530
3880
|
await refreshDynamicCommands(currentWorkdir);
|
|
3531
3881
|
if (editor)
|
|
3532
3882
|
editor.setCommands(buildSlashCommands());
|
|
3533
|
-
transcript.log('user_task', {
|
|
3883
|
+
transcript.log('user_task', {
|
|
3884
|
+
task: finalTaskText,
|
|
3885
|
+
workdir: currentWorkdir,
|
|
3886
|
+
});
|
|
3887
|
+
// BACKLOG B3: snapshot the working tree BEFORE the task so /rewind can
|
|
3888
|
+
// undo the whole task in one step. Best-effort: create() never throws, so
|
|
3889
|
+
// a missing tar cannot abort the task it was meant to protect. Skipped
|
|
3890
|
+
// while reviewing a snapshot (workdir is a throwaway copy).
|
|
3891
|
+
if (!reviewMode) {
|
|
3892
|
+
const cp = await checkpoints.create(currentWorkdir);
|
|
3893
|
+
if (cp)
|
|
3894
|
+
transcript.log('checkpoint', { stamp: cp.stamp });
|
|
3895
|
+
}
|
|
3534
3896
|
const tools = mod.createTools(currentWorkdir, {
|
|
3535
3897
|
undo,
|
|
3536
3898
|
todos: todoStore,
|
|
@@ -3541,7 +3903,7 @@ async function main() {
|
|
|
3541
3903
|
if (editor)
|
|
3542
3904
|
editor.busy = true;
|
|
3543
3905
|
try {
|
|
3544
|
-
await runTask(browser, tools,
|
|
3906
|
+
await runTask(browser, tools, finalTaskText, currentWorkdir, {
|
|
3545
3907
|
transcript,
|
|
3546
3908
|
freshChat: freshChatNext,
|
|
3547
3909
|
sendSystemPrompt: sendSystemPromptNext,
|
|
@@ -3567,7 +3929,7 @@ async function main() {
|
|
|
3567
3929
|
currentChatId,
|
|
3568
3930
|
workdir: currentWorkdir,
|
|
3569
3931
|
locale: currentLocale,
|
|
3570
|
-
task:
|
|
3932
|
+
task: finalTaskText,
|
|
3571
3933
|
transcript,
|
|
3572
3934
|
ui: editor,
|
|
3573
3935
|
buildSystemPrompt: mod.buildSystemPrompt,
|
|
@@ -3578,6 +3940,7 @@ async function main() {
|
|
|
3578
3940
|
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
3579
3941
|
quiet: true,
|
|
3580
3942
|
skipUiLock: true,
|
|
3943
|
+
selfImprovement: devMode,
|
|
3581
3944
|
});
|
|
3582
3945
|
if (res.ok) {
|
|
3583
3946
|
currentChatId = res.chatId;
|
|
@@ -3591,6 +3954,7 @@ async function main() {
|
|
|
3591
3954
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3592
3955
|
goal: sessionGoal,
|
|
3593
3956
|
todosQuery: () => todosSummary(),
|
|
3957
|
+
selfImprovement: devMode,
|
|
3594
3958
|
}, inputAttachments);
|
|
3595
3959
|
}
|
|
3596
3960
|
finally {
|