zames_pro 2.58.0 → 2.60.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 +25 -1
- package/README.md +4 -0
- package/dist/agent-loop.js +33 -1
- package/dist/commands.js +73 -0
- package/dist/hooks.js +154 -0
- package/dist/i18n.js +20 -0
- package/dist/index.js +55 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.60.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- PreToolUse / PostToolUse hooks (see `src/hooks.ts`): a project policy can
|
|
15
|
+
attach an external script to every tool call via `.zames/hooks.json`
|
|
16
|
+
(`{ "PreToolUse": [{matcher, command}], "PostToolUse": [...] }`). A
|
|
17
|
+
PreToolUse hook that exits non-zero BLOCKS the call (its output becomes the
|
|
18
|
+
tool result); a PostToolUse hook's stdout is appended to the result.
|
|
19
|
+
Hooks are best-effort: a missing/malformed config, a crash or a 10s timeout
|
|
20
|
+
never fails the agent loop.
|
|
21
|
+
|
|
22
|
+
## [2.59.0]
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `/improve [id]` — a backlog-driven self-improvement loop. It picks the
|
|
27
|
+
next open item from `BACKLOG.md` (highest priority, or an explicit id),
|
|
28
|
+
runs the standard task loop on it (implement, typecheck/lint/test, mark
|
|
29
|
+
the item done, add a CHANGELOG entry) and leaves the changes uncommitted
|
|
30
|
+
for review. This is the reproducible hand-off path for a fresh agent.
|
|
31
|
+
|
|
10
32
|
## [2.58.0]
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -198,7 +220,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
198
220
|
- Session banner warns when no saved DeepSeek session exists.
|
|
199
221
|
- `--no-color` flag (explicit `NO_COLOR`).
|
|
200
222
|
|
|
201
|
-
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.
|
|
223
|
+
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.60.0...HEAD
|
|
224
|
+
[2.60.0]: https://github.com/Viqto0r/zames_pro/compare/v2.59.0...v2.60.0
|
|
225
|
+
[2.59.0]: https://github.com/Viqto0r/zames_pro/compare/v2.58.0...v2.59.0
|
|
202
226
|
[2.58.0]: https://github.com/Viqto0r/zames_pro/compare/v2.57.1...v2.58.0
|
|
203
227
|
[2.57.1]: https://github.com/Viqto0r/zames_pro/compare/v2.57.0...v2.57.1
|
|
204
228
|
[2.57.0]: https://github.com/Viqto0r/zames_pro/compare/v2.56.0...v2.57.0
|
package/README.md
CHANGED
|
@@ -247,6 +247,10 @@ Codex CLI:
|
|
|
247
247
|
instead of the whole list.
|
|
248
248
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
249
249
|
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.
|
|
250
254
|
- /plan [on|off] — plan (read-only) mode. While it is on, the mutating tools
|
|
251
255
|
(Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush) are removed
|
|
252
256
|
from the tool set, so the agent can investigate without touching the tree.
|
package/dist/agent-loop.js
CHANGED
|
@@ -5,7 +5,13 @@ import { parseXmlToolCalls } from './xml-toolcall.js';
|
|
|
5
5
|
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, }) {
|
|
10
|
+
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
11
|
+
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
12
|
+
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
13
|
+
// hooks entirely.
|
|
14
|
+
const hookConfig = hooks === undefined ? loadHooks(workdir) : (hooks ?? {});
|
|
9
15
|
// UI callbacks must NEVER break the agent loop. A rendering error (a huge
|
|
10
16
|
// tool result, a broken markdown frame, a closed terminal) used to throw
|
|
11
17
|
// out of the loop right after a tool call — the session looked "stopped
|
|
@@ -705,6 +711,21 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
705
711
|
syncToolAbort();
|
|
706
712
|
safeToolCall(call.tool, call.args);
|
|
707
713
|
transcript?.log('tool_call', { tool: call.tool, args: call.args });
|
|
714
|
+
// PreToolUse hooks run BEFORE the tool. A non-zero exit BLOCKS the call:
|
|
715
|
+
// its output becomes the tool result and the tool itself never runs.
|
|
716
|
+
// Best-effort by design — hooks must not be able to kill the loop.
|
|
717
|
+
const denial = await runPreToolUse(hookConfig, call.tool, call.args, workdir);
|
|
718
|
+
if (denial !== null) {
|
|
719
|
+
const blocked = `Blocked by PreToolUse hook: ${denial}`;
|
|
720
|
+
transcript?.log('hook_pre_deny', { tool: call.tool, reason: denial });
|
|
721
|
+
safeToolResult(blocked);
|
|
722
|
+
transcript?.log('tool_result', {
|
|
723
|
+
tool: call.tool,
|
|
724
|
+
result: blocked,
|
|
725
|
+
});
|
|
726
|
+
results.push({ tool: call.tool, result: blocked });
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
708
729
|
let result;
|
|
709
730
|
// While the tool runs, poll for an Esc/Ctrl+C: the abort flag is a plain
|
|
710
731
|
// boolean set by stopGeneration(), so the only way to turn it into a
|
|
@@ -722,6 +743,17 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
722
743
|
finally {
|
|
723
744
|
clearInterval(poll);
|
|
724
745
|
}
|
|
746
|
+
// PostToolUse hooks run AFTER the tool; their stdout is appended to the
|
|
747
|
+
// result (e.g. `prettier` output) before it is fed back to the model.
|
|
748
|
+
// Best-effort: a hook failure is ignored, the tool result still stands.
|
|
749
|
+
const post = await runPostToolUse(hookConfig, call.tool, call.args, String(result), workdir);
|
|
750
|
+
if (post) {
|
|
751
|
+
transcript?.log('hook_post_output', { tool: call.tool, output: post });
|
|
752
|
+
result = `${String(result)}
|
|
753
|
+
|
|
754
|
+
[PostToolUse hook]
|
|
755
|
+
${post}`;
|
|
756
|
+
}
|
|
725
757
|
safeToolResult(result);
|
|
726
758
|
transcript?.log('tool_result', {
|
|
727
759
|
tool: call.tool,
|
package/dist/commands.js
CHANGED
|
@@ -707,6 +707,79 @@ export function mergeMessages(msgs) {
|
|
|
707
707
|
const text = header + NL + NL + parts.join(NL + NL + '---' + NL + NL);
|
|
708
708
|
return { text, attachments };
|
|
709
709
|
}
|
|
710
|
+
// Parse BACKLOG.md into actionable items. Recognizes headings of the form
|
|
711
|
+
// `### A1. ...`, `### D7. ...` and marks an item done when its heading carries
|
|
712
|
+
// a `[x]` marker. Pure; unit-tested. We deliberately do NOT parse the whole
|
|
713
|
+
// markdown structure — only the headings the file contract promises.
|
|
714
|
+
export function parseBacklogItems(text) {
|
|
715
|
+
const out = [];
|
|
716
|
+
let priority = '';
|
|
717
|
+
for (const raw of String(text ?? '').split(NL)) {
|
|
718
|
+
const line = raw.trimEnd();
|
|
719
|
+
const p = /^##\s+(P[0-3])\b/.exec(line);
|
|
720
|
+
if (p) {
|
|
721
|
+
priority = p[1];
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const m = /^###\s+([A-Z]\d+)\.\s+(.*)$/.exec(line);
|
|
725
|
+
if (!m)
|
|
726
|
+
continue;
|
|
727
|
+
const id = m[1];
|
|
728
|
+
const rest = m[2];
|
|
729
|
+
// The `[x]` rewrite in BACKLOG keeps the ORIGINAL heading under a
|
|
730
|
+
// <details> block wrapped in `~~`. That archived copy must not be counted
|
|
731
|
+
// as a second, open item — skip a heading whose text starts with `~~`.
|
|
732
|
+
if (rest.trim().startsWith('~~'))
|
|
733
|
+
continue;
|
|
734
|
+
const done = /^\[x\]/i.test(rest.trim());
|
|
735
|
+
// The title is the heading text with the [x] marker and the `~~` strike
|
|
736
|
+
// wrappers removed, so a done item still has a clean label.
|
|
737
|
+
const title = rest
|
|
738
|
+
.replace(/^\[x\]\s*/i, '')
|
|
739
|
+
.replace(/~~/g, '')
|
|
740
|
+
.trim();
|
|
741
|
+
out.push({ id, title, status: done ? 'done' : 'open', priority });
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
// The next item to work on: the highest-priority OPEN item, in file order.
|
|
746
|
+
// P0 first, then P1..P3. An explicit id overrides the priority search.
|
|
747
|
+
export function nextBacklogItem(items, wanted) {
|
|
748
|
+
const open = items.filter((i) => i.status === 'open');
|
|
749
|
+
if (wanted) {
|
|
750
|
+
const w = wanted.trim().toUpperCase();
|
|
751
|
+
return items.find((i) => i.id.toUpperCase() === w) || null;
|
|
752
|
+
}
|
|
753
|
+
if (!open.length)
|
|
754
|
+
return null;
|
|
755
|
+
const rank = (p) => {
|
|
756
|
+
const m = /P(\d)/.exec(p || '');
|
|
757
|
+
return m ? Number(m[1]) : 9;
|
|
758
|
+
};
|
|
759
|
+
return open.slice().sort((a, b) => rank(a.priority) - rank(b.priority))[0];
|
|
760
|
+
}
|
|
761
|
+
// The task text sent to the agent for a chosen item. Agent-facing English,
|
|
762
|
+
// like the rest of the loop. Keeps the working rules explicit so a fresh agent
|
|
763
|
+
// does not need any external context.
|
|
764
|
+
export function buildImprovePrompt(item) {
|
|
765
|
+
return ('Implement BACKLOG item ' +
|
|
766
|
+
item.id +
|
|
767
|
+
' (priority ' +
|
|
768
|
+
(item.priority || '?') +
|
|
769
|
+
'): ' +
|
|
770
|
+
item.title +
|
|
771
|
+
NL +
|
|
772
|
+
NL +
|
|
773
|
+
'Read the full item text in BACKLOG.md for the details and the rationale. ' +
|
|
774
|
+
'Then: (1) implement it with the smallest reasonable change; ' +
|
|
775
|
+
'(2) run `npm run typecheck`, `npm run lint`, `npm run format:check` and ' +
|
|
776
|
+
'`npm test` and fix any failure; (3) mark the item done in BACKLOG.md by ' +
|
|
777
|
+
'prefixing its heading with `[x]` and wrapping the original heading text ' +
|
|
778
|
+
'in `~~ ~~`; (4) add a CHANGELOG entry if the change is user-visible; ' +
|
|
779
|
+
'(5) do NOT commit or push — leave the changes staged in the working tree ' +
|
|
780
|
+
'for the operator to review. When done, call respond with a short report ' +
|
|
781
|
+
'(files changed, tests run, whether BACKLOG/CHANGELOG were updated).');
|
|
782
|
+
}
|
|
710
783
|
// ---------- /review ----------
|
|
711
784
|
export function buildReviewPrompt(focus, hasStaged = false) {
|
|
712
785
|
const scope = hasStaged ? 'staged' : 'uncommitted';
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
// A hook is a helper, not a task: if it has not finished in ten seconds the
|
|
5
|
+
// tool call must not be held hostage by it.
|
|
6
|
+
const HOOK_TIMEOUT_MS = 10_000;
|
|
7
|
+
const HOOK_MAX_BUFFER = 1024 * 1024;
|
|
8
|
+
export function hooksConfigPath(workdir) {
|
|
9
|
+
return path.join(workdir, '.zames', 'hooks.json');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads `.zames/hooks.json`. A missing, unreadable or malformed file yields an
|
|
13
|
+
* empty config: a broken hooks file must not stop the agent from starting.
|
|
14
|
+
*/
|
|
15
|
+
export function loadHooks(workdir) {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = fs.readFileSync(hooksConfigPath(workdir), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
let data;
|
|
24
|
+
try {
|
|
25
|
+
data = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
if (!data || typeof data !== 'object')
|
|
31
|
+
return {};
|
|
32
|
+
const obj = data;
|
|
33
|
+
return {
|
|
34
|
+
PreToolUse: normalizeEntries(obj['PreToolUse']),
|
|
35
|
+
PostToolUse: normalizeEntries(obj['PostToolUse']),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function normalizeEntries(value) {
|
|
39
|
+
if (!Array.isArray(value))
|
|
40
|
+
return undefined;
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const item of value) {
|
|
43
|
+
if (!item || typeof item !== 'object')
|
|
44
|
+
continue;
|
|
45
|
+
const entry = item;
|
|
46
|
+
const command = entry['command'];
|
|
47
|
+
if (typeof command !== 'string' || command.trim() === '')
|
|
48
|
+
continue;
|
|
49
|
+
const matcher = entry['matcher'];
|
|
50
|
+
out.push({
|
|
51
|
+
command,
|
|
52
|
+
matcher: typeof matcher === 'string' ? matcher : undefined,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out.length ? out : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** True when the entry applies to the tool. A bad regex matches nothing. */
|
|
58
|
+
export function matchesHook(entry, tool) {
|
|
59
|
+
if (!entry.matcher)
|
|
60
|
+
return true;
|
|
61
|
+
try {
|
|
62
|
+
return new RegExp(entry.matcher).test(tool);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function runHook(entry, event, tool, args, workdir, result) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const payload = {
|
|
71
|
+
event,
|
|
72
|
+
tool,
|
|
73
|
+
args: args ?? {},
|
|
74
|
+
...(result === undefined ? {} : { result }),
|
|
75
|
+
};
|
|
76
|
+
const env = {
|
|
77
|
+
...process.env,
|
|
78
|
+
ZAMES_HOOK_EVENT: event,
|
|
79
|
+
ZAMES_TOOL_NAME: tool,
|
|
80
|
+
ZAMES_TOOL_ARGS: JSON.stringify(args ?? {}),
|
|
81
|
+
...(result === undefined ? {} : { ZAMES_TOOL_RESULT: result }),
|
|
82
|
+
};
|
|
83
|
+
let child;
|
|
84
|
+
try {
|
|
85
|
+
child = exec(entry.command, {
|
|
86
|
+
cwd: workdir,
|
|
87
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
88
|
+
maxBuffer: HOOK_MAX_BUFFER,
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
env,
|
|
91
|
+
shell: process.platform === 'win32'
|
|
92
|
+
? process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe'
|
|
93
|
+
: '/bin/sh',
|
|
94
|
+
}, (err, stdout, stderr) => {
|
|
95
|
+
let code = 0;
|
|
96
|
+
if (err) {
|
|
97
|
+
const c = err.code;
|
|
98
|
+
code = typeof c === 'number' ? c : 1;
|
|
99
|
+
}
|
|
100
|
+
resolve({
|
|
101
|
+
code,
|
|
102
|
+
stdout: (stdout || '').toString(),
|
|
103
|
+
stderr: (stderr || '').toString(),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Spawning the shell itself failed — treat it as a hook error, not a
|
|
109
|
+
// loop error.
|
|
110
|
+
resolve({ code: 1, stdout: '', stderr: 'hook could not be started' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// The call is also fed on stdin so a hook can parse the full JSON without
|
|
114
|
+
// depending on env size limits. A hook that ignores stdin is fine.
|
|
115
|
+
if (child.stdin) {
|
|
116
|
+
child.stdin.on('error', () => { });
|
|
117
|
+
child.stdin.end(JSON.stringify(payload) + String.fromCharCode(10));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Runs every matching PreToolUse hook in order. Returns a denial reason when a
|
|
123
|
+
* hook exits non-zero (its stderr/stdout becomes the tool result and the tool
|
|
124
|
+
* itself does NOT run), or null to allow the call.
|
|
125
|
+
*/
|
|
126
|
+
export async function runPreToolUse(hooks, tool, args, workdir) {
|
|
127
|
+
for (const entry of hooks?.PreToolUse ?? []) {
|
|
128
|
+
if (!matchesHook(entry, tool))
|
|
129
|
+
continue;
|
|
130
|
+
const r = await runHook(entry, 'PreToolUse', tool, args, workdir);
|
|
131
|
+
if (r.code !== 0) {
|
|
132
|
+
const reason = (r.stderr || r.stdout).trim();
|
|
133
|
+
return reason || `hook exited with code ${r.code}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Runs every matching PostToolUse hook and returns their stdout, ready to be
|
|
140
|
+
* appended to the tool result. Errors are ignored: the tool already ran, and
|
|
141
|
+
* its result must be reported.
|
|
142
|
+
*/
|
|
143
|
+
export async function runPostToolUse(hooks, tool, args, result, workdir) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
for (const entry of hooks?.PostToolUse ?? []) {
|
|
146
|
+
if (!matchesHook(entry, tool))
|
|
147
|
+
continue;
|
|
148
|
+
const r = await runHook(entry, 'PostToolUse', tool, args, workdir, result);
|
|
149
|
+
const out = r.stdout.trim();
|
|
150
|
+
if (out)
|
|
151
|
+
parts.push(out);
|
|
152
|
+
}
|
|
153
|
+
return parts.join(String.fromCharCode(10));
|
|
154
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -239,6 +239,26 @@ export const CATALOG = {
|
|
|
239
239
|
ru: '/review [focus] ревью незакоммиченных изменений',
|
|
240
240
|
en: '/review [focus] review uncommitted changes',
|
|
241
241
|
},
|
|
242
|
+
'help.cmd.improve': {
|
|
243
|
+
ru: '/improve [id] взять следующий пункт BACKLOG и довести до тестов',
|
|
244
|
+
en: '/improve [id] take the next BACKLOG item and drive it to green tests',
|
|
245
|
+
},
|
|
246
|
+
'improve.no_backlog': {
|
|
247
|
+
ru: 'BACKLOG.md не найден в рабочей директории.',
|
|
248
|
+
en: 'BACKLOG.md was not found in the working directory.',
|
|
249
|
+
},
|
|
250
|
+
'improve.all_done': {
|
|
251
|
+
ru: 'Открытых пунктов в BACKLOG нет.',
|
|
252
|
+
en: 'There are no open BACKLOG items.',
|
|
253
|
+
},
|
|
254
|
+
'improve.not_found': {
|
|
255
|
+
ru: 'Пункт {v} не найден в BACKLOG.',
|
|
256
|
+
en: 'Item {v} was not found in BACKLOG.',
|
|
257
|
+
},
|
|
258
|
+
'improve.start': {
|
|
259
|
+
ru: 'Улучшение {id}: {title}',
|
|
260
|
+
en: 'Improving {id}: {title}',
|
|
261
|
+
},
|
|
242
262
|
'help.cmd.compact': {
|
|
243
263
|
ru: '/compact сжать историю и открыть новый чат с резюме',
|
|
244
264
|
en: '/compact compact the history and open a new chat with the summary',
|
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, 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, 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';
|
|
@@ -388,6 +388,7 @@ ${theme.bold(t('help.sec.files'))}
|
|
|
388
388
|
${t('help.cmd.doctor')}
|
|
389
389
|
${t('help.cmd.add_dir')}
|
|
390
390
|
${t('help.cmd.review')}
|
|
391
|
+
${t('help.cmd.improve')}
|
|
391
392
|
${t('help.cmd.debug_dom')}
|
|
392
393
|
${t('help.cmd.help')}
|
|
393
394
|
${t('help.cmd.exit')}
|
|
@@ -448,6 +449,7 @@ const SLASH_COMMANDS = [
|
|
|
448
449
|
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
449
450
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
450
451
|
{ name: '/review', key: 'help.cmd.review' },
|
|
452
|
+
{ name: '/improve', key: 'help.cmd.improve' },
|
|
451
453
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
452
454
|
{ name: '/loop', key: 'help.cmd.loop' },
|
|
453
455
|
{ name: '/cron', key: 'help.cmd.cron' },
|
|
@@ -3336,6 +3338,58 @@ async function main() {
|
|
|
3336
3338
|
}
|
|
3337
3339
|
continue;
|
|
3338
3340
|
}
|
|
3341
|
+
if (lower === '/improve' || lower.startsWith('/improve ')) {
|
|
3342
|
+
// Backlog-driven self-improvement: pick the next open BACKLOG item (or a
|
|
3343
|
+
// specific id) and run the standard task loop on it. The agent reads
|
|
3344
|
+
// BACKLOG.md itself for the details, so a FRESH agent needs no external
|
|
3345
|
+
// context — this is the reproducible hand-off path.
|
|
3346
|
+
const arg = trimmed.slice('/improve'.length).trim();
|
|
3347
|
+
let backlogText;
|
|
3348
|
+
try {
|
|
3349
|
+
backlogText = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
|
|
3350
|
+
}
|
|
3351
|
+
catch {
|
|
3352
|
+
console.error(theme.warn(t('improve.no_backlog')));
|
|
3353
|
+
continue;
|
|
3354
|
+
}
|
|
3355
|
+
const items = parseBacklogItems(backlogText);
|
|
3356
|
+
const item = nextBacklogItem(items, arg || undefined);
|
|
3357
|
+
if (!item) {
|
|
3358
|
+
console.log(theme.dim(arg ? t('improve.not_found', { v: arg }) : t('improve.all_done')));
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
3361
|
+
console.log(theme.system(t('improve.start', { id: item.id, title: item.title })));
|
|
3362
|
+
const improveTools = mod.createTools(currentWorkdir, {
|
|
3363
|
+
undo,
|
|
3364
|
+
todos: todoStore,
|
|
3365
|
+
readOnly: planMode,
|
|
3366
|
+
});
|
|
3367
|
+
if (mcpPool)
|
|
3368
|
+
improveTools.push(...mcpPool.tools);
|
|
3369
|
+
if (editor)
|
|
3370
|
+
editor.busy = true;
|
|
3371
|
+
try {
|
|
3372
|
+
await runTask(browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
|
|
3373
|
+
transcript,
|
|
3374
|
+
freshChat: false,
|
|
3375
|
+
sendSystemPrompt: false,
|
|
3376
|
+
queue: pendingQueue,
|
|
3377
|
+
ui: editor || null,
|
|
3378
|
+
onChatReady: (chatId) => {
|
|
3379
|
+
if (chatId) {
|
|
3380
|
+
currentChatId = chatId;
|
|
3381
|
+
saveLastChat(chatId, currentWorkdir);
|
|
3382
|
+
}
|
|
3383
|
+
},
|
|
3384
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
finally {
|
|
3388
|
+
if (editor)
|
|
3389
|
+
editor.busy = false;
|
|
3390
|
+
}
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3339
3393
|
if (lower === '/review' || lower.startsWith('/review ')) {
|
|
3340
3394
|
const rest = trimmed.slice('/review'.length).trim();
|
|
3341
3395
|
const staged = rest.indexOf('--staged') !== -1;
|
package/package.json
CHANGED