zames_pro 2.70.0 → 2.72.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 +32 -0
- package/README.md +8 -0
- package/dist/agent-loop.js +258 -857
- package/dist/answer-parse.js +765 -0
- package/dist/browser-chats.js +161 -0
- package/dist/browser-pure.js +238 -0
- package/dist/browser.js +45 -390
- package/dist/changelog.js +29 -0
- package/dist/cli-args.js +48 -0
- package/dist/compact.js +9 -0
- package/dist/config.js +24 -0
- package/dist/diagnostics.js +71 -0
- package/dist/hooks.js +30 -2
- package/dist/i18n/common.js +4 -0
- package/dist/i18n/config.js +8 -0
- package/dist/index.js +81 -24
- package/dist/input.js +25 -1
- package/dist/run-task.js +33 -1
- package/dist/shell.js +102 -1
- package/dist/statusline.js +52 -0
- package/dist/subagent.js +156 -0
- package/dist/system-prompt.js +38 -1
- package/dist/tools.js +105 -9
- package/package.json +10 -8
- package/scripts/changelog-draft.mts +38 -1
- package/scripts/{eslint-ts6.mjs → eslint-ts6.mts} +11 -4
- package/scripts/release.mts +36 -7
- package/scripts/{render-demo.mjs → render-demo.mts} +12 -11
- package/scripts/{self-smoke.mjs → self-smoke.mts} +86 -5
- /package/scripts/{clean-dist.mjs → clean-dist.mts} +0 -0
- /package/scripts/{coverage-gate.mjs → coverage-gate.mts} +0 -0
package/dist/subagent.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
2
|
+
// Subagent runner (BACKLOG N36). A `Task` tool call from the model is handled
|
|
3
|
+
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
4
|
+
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
5
|
+
//
|
|
6
|
+
// Why a SEPARATE chat: in zames "context" IS the DeepSeek chat, so context
|
|
7
|
+
// isolation can only be achieved by opening a new chat, running a nested
|
|
8
|
+
// agent loop there, and restoring the parent chat afterwards. The nested loop
|
|
9
|
+
// returns ONLY the subagent's final `respond` report; the parent never sees
|
|
10
|
+
// the subagent's raw turns, which is the whole point.
|
|
11
|
+
//
|
|
12
|
+
// This is deliberately SEQUENTIAL: one browser, one send slot. Parallel
|
|
13
|
+
// subagents would need a second page and a shared send scheduler (a change to
|
|
14
|
+
// browser.ts, which owns the live Playwright context) — out of scope here.
|
|
15
|
+
/** Hard cap on iterations for one subagent run. */
|
|
16
|
+
export const SUBAGENT_MAX_ITER = 30;
|
|
17
|
+
// The report is fed back into the PARENT chat as a tool result, so a runaway
|
|
18
|
+
// subagent must not be able to blow up the parent's context with one answer.
|
|
19
|
+
// The tail is trimmed with a marker, not silently dropped.
|
|
20
|
+
export const SUBAGENT_REPORT_CAP = 20_000;
|
|
21
|
+
/** Cap a subagent report before it becomes a parent-context tool result. */
|
|
22
|
+
export function capReport(text, cap = SUBAGENT_REPORT_CAP) {
|
|
23
|
+
if (text.length <= cap)
|
|
24
|
+
return text;
|
|
25
|
+
return (text.slice(0, cap) +
|
|
26
|
+
'\n… [report truncated: ' +
|
|
27
|
+
(text.length - cap) +
|
|
28
|
+
' chars omitted]');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The subagent persona, prepended to the task message. It is deliberately NOT
|
|
32
|
+
* injected as a replacement system prompt: the nested runAgentLoop builds the
|
|
33
|
+
* standard system prompt itself (tool list + call format), and the model needs
|
|
34
|
+
* exactly that to work. The persona only adds role + output contract.
|
|
35
|
+
*/
|
|
36
|
+
export function subagentPersona(type) {
|
|
37
|
+
const base = 'You are a SUBAGENT launched by a main coding agent to complete ONE ' +
|
|
38
|
+
'self-contained sub-task in an ISOLATED chat. You do NOT see the main ' +
|
|
39
|
+
"conversation's history, so rely only on the instructions below and on " +
|
|
40
|
+
'the files/tools available. You cannot talk to the operator, so do not ' +
|
|
41
|
+
'ask questions. When the sub-task is done, call respond with a concise, ' +
|
|
42
|
+
'information-dense report: findings, exact file paths, relevant code ' +
|
|
43
|
+
'snippets and a clear conclusion. Do NOT call respond until the work is ' +
|
|
44
|
+
'complete.';
|
|
45
|
+
if (type === 'explore') {
|
|
46
|
+
return (base +
|
|
47
|
+
'\n\nYou are in EXPLORE (read-only) mode: you may search and read files ' +
|
|
48
|
+
'but you CANNOT modify anything or run mutating commands. Investigate ' +
|
|
49
|
+
'and report your findings.');
|
|
50
|
+
}
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `onSubagent` callback for runAgentLoop. It enforces the per-task
|
|
55
|
+
* budget, opens a fresh chat, runs a nested loop (with the subagent's own
|
|
56
|
+
* task text and a restricted or full tool set), restores the parent chat and
|
|
57
|
+
* returns only the report.
|
|
58
|
+
*/
|
|
59
|
+
export function createSubagentRunner(opts) {
|
|
60
|
+
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries, onAskPermission, } = opts;
|
|
61
|
+
let used = 0;
|
|
62
|
+
return async (req) => {
|
|
63
|
+
if (used >= maxSubagents) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
text: 'Subagent budget exhausted (' +
|
|
67
|
+
maxSubagents +
|
|
68
|
+
' per task). Do the remaining work yourself with the other tools.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
used++;
|
|
72
|
+
const parentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
73
|
+
const subTools = buildTools(req.type === 'explore');
|
|
74
|
+
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
75
|
+
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
|
76
|
+
// browser, and leaving the nested (no-op) callbacks in place would kill
|
|
77
|
+
// the parent's spinner/status for the rest of the task.
|
|
78
|
+
const savedHooks = {
|
|
79
|
+
onSendStart: browser.onSendStart ?? null,
|
|
80
|
+
onSendPause: browser.onSendPause ?? null,
|
|
81
|
+
onSendState: browser.onSendState ?? null,
|
|
82
|
+
onNotice: browser.onNotice ?? null,
|
|
83
|
+
};
|
|
84
|
+
let report = '';
|
|
85
|
+
let failed = '';
|
|
86
|
+
try {
|
|
87
|
+
report = await runAgentLoop({
|
|
88
|
+
browser,
|
|
89
|
+
tools: subTools,
|
|
90
|
+
task: taskText,
|
|
91
|
+
workdir,
|
|
92
|
+
maxIterations: SUBAGENT_MAX_ITER,
|
|
93
|
+
// A fresh chat gives the subagent an isolated context; the nested loop
|
|
94
|
+
// opens it AND sends the standard system prompt itself.
|
|
95
|
+
freshChat: true,
|
|
96
|
+
sendSystemPrompt: true,
|
|
97
|
+
transcript,
|
|
98
|
+
locale,
|
|
99
|
+
askDeadlineMs,
|
|
100
|
+
maxAfterToolRetries,
|
|
101
|
+
// A subagent NEVER touches BACKLOG.md, even in the operator's dev
|
|
102
|
+
// mode: its sub-task is scoped, and a general subagent appending
|
|
103
|
+
// "improvement" notes would be unrelated, unsupervised edits.
|
|
104
|
+
selfImprovement: false,
|
|
105
|
+
// No recursion: the subagent cannot spawn its own subagents.
|
|
106
|
+
onSubagent: null,
|
|
107
|
+
// Do not persist the subagent chat as a session.
|
|
108
|
+
onChatReady: () => { },
|
|
109
|
+
// Keep the PARENT's UI alive during the nested run. The nested loop
|
|
110
|
+
// OVERWRITES browser.onSendStart/… with its own callbacks; without
|
|
111
|
+
// forwarding the parent's hooks the spinner and the throttle countdown
|
|
112
|
+
// would go blank for the whole subagent run (a long subagent looked
|
|
113
|
+
// like a frozen, dead agent). The nested loop, not this runner, owns
|
|
114
|
+
// the assignment, so we hand it the functions it should install.
|
|
115
|
+
onThinking: savedHooks.onSendStart ?? undefined,
|
|
116
|
+
onSendPause: savedHooks.onSendPause ?? undefined,
|
|
117
|
+
onSendState: savedHooks.onSendState ?? undefined,
|
|
118
|
+
onNotice: savedHooks.onNotice ?? undefined,
|
|
119
|
+
onAskPermission: onAskPermission
|
|
120
|
+
? (info) => onAskPermission({
|
|
121
|
+
tool: info.tool,
|
|
122
|
+
reason: info.reason,
|
|
123
|
+
action: info.action,
|
|
124
|
+
})
|
|
125
|
+
: undefined,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
failed = e.message;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
// Restore the parent chat, then the hooks, so a failure in openChat
|
|
133
|
+
// still leaves the hooks correct.
|
|
134
|
+
if (parentChatId) {
|
|
135
|
+
await browser.openChat(parentChatId).catch(() => false);
|
|
136
|
+
}
|
|
137
|
+
browser.onSendStart = savedHooks.onSendStart;
|
|
138
|
+
browser.onSendPause = savedHooks.onSendPause;
|
|
139
|
+
browser.onSendState = savedHooks.onSendState;
|
|
140
|
+
browser.onNotice = savedHooks.onNotice;
|
|
141
|
+
}
|
|
142
|
+
// SubagentStop hook (N32): fires after the nested loop finishes, so a hook
|
|
143
|
+
// can clean up or notify. Best-effort; its stdout is appended to the report.
|
|
144
|
+
let hookOut = '';
|
|
145
|
+
try {
|
|
146
|
+
hookOut = await runLifecycleHooks(loadHooks(workdir), 'SubagentStop', workdir, { type: req.type, ok: failed ? 'false' : 'true' });
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
if (failed)
|
|
150
|
+
return { ok: false, text: failed };
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
text: capReport(hookOut ? report + '\n\n' + hookOut : report),
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -21,6 +21,35 @@ Rules for that note:
|
|
|
21
21
|
- this is the ONE exception to "do not touch unrelated files". If nothing
|
|
22
22
|
worth noting, add nothing.
|
|
23
23
|
`;
|
|
24
|
+
// Guidance for the `Task` tool (subagents), injected only when that tool is in
|
|
25
|
+
// the model's set. Without it the model under-uses subagents (it does every
|
|
26
|
+
// broad search itself and floods the main context) or misuses them (delegates
|
|
27
|
+
// work that needs THIS conversation's history, which the isolated subagent
|
|
28
|
+
// cannot see). Kept short: it competes with the tool list for space.
|
|
29
|
+
const SUBAGENT_SECTION = `
|
|
30
|
+
## Task tool (subagents)
|
|
31
|
+
|
|
32
|
+
The Task tool delegates a self-contained sub-task to a subagent that works in
|
|
33
|
+
its OWN separate chat with an ISOLATED context and returns ONLY a final
|
|
34
|
+
report. The subagent does NOT see this conversation, so the prompt you pass
|
|
35
|
+
must be fully self-contained (goal, exact questions, file paths, context).
|
|
36
|
+
|
|
37
|
+
Use it to PROTECT this chat's context and to parallelize independent work:
|
|
38
|
+
|
|
39
|
+
- Broad exploration whose raw hits would flood this chat ("find every use of
|
|
40
|
+
X", "map how module Y works end to end"): delegate it with
|
|
41
|
+
subagent_type "explore" (read-only) and keep only the report.
|
|
42
|
+
- A self-contained research or verification sub-task you can specify in a
|
|
43
|
+
paragraph: delegate it and continue the main line meanwhile.
|
|
44
|
+
- Several INDEPENDENT investigations: emit several Task calls in ONE batch.
|
|
45
|
+
|
|
46
|
+
Do NOT use it for:
|
|
47
|
+
- work that depends on this conversation's history (the subagent cannot see
|
|
48
|
+
it) - pass everything needed in the prompt, or do the work yourself;
|
|
49
|
+
- a trivial single read/edit you can do directly (a subagent costs a full
|
|
50
|
+
extra chat with its own system prompt and sends);
|
|
51
|
+
- edits that must run strictly in order with the rest of this chat.
|
|
52
|
+
`;
|
|
24
53
|
/** Render the AGENTS.md / MEMORY / skills / commands blocks of the prompt. */
|
|
25
54
|
export function renderContextSection(context) {
|
|
26
55
|
if (!context)
|
|
@@ -112,6 +141,12 @@ export function renderToolSection(tools) {
|
|
|
112
141
|
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
|
|
113
142
|
const t = translate(locale);
|
|
114
143
|
const toolDescriptions = renderToolSection(tools);
|
|
144
|
+
// Emit the subagent guidance ONLY when the Task tool is actually in the set
|
|
145
|
+
// (subagents enabled and not in plan mode), so the prompt never tells the
|
|
146
|
+
// model about a tool it does not have.
|
|
147
|
+
const subagentSection = tools.some((x) => x.name === 'Task')
|
|
148
|
+
? SUBAGENT_SECTION
|
|
149
|
+
: '';
|
|
115
150
|
const gitSection = gitContext
|
|
116
151
|
? `\n## Git context\n\n${gitContext}\n`
|
|
117
152
|
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
|
@@ -186,7 +221,9 @@ Prefer the most specific tool; a wrong choice wastes a turn:
|
|
|
186
221
|
directory -> LS.
|
|
187
222
|
- Git operations -> the Git* tools, not Bash.
|
|
188
223
|
- Reading a file whose path you already know -> Read, not Bash cat.
|
|
189
|
-
|
|
224
|
+
- A broad search/exploration that would flood this chat -> the Task tool
|
|
225
|
+
(a subagent in its own chat) if it is available, then work from its report.
|
|
226
|
+
${subagentSection}
|
|
190
227
|
## MCP tools
|
|
191
228
|
|
|
192
229
|
Some tools are provided by external MCP servers and their names look
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { assertCommandInsideRoot, runFile, runShell } from './shell.js';
|
|
3
|
+
import { assertCommandInsideRoot, runFile, runShell, startBackground, pollBackground, killBackground, } from './shell.js';
|
|
4
|
+
import { isImageName, guessMime, formatSize } from './attachments.js';
|
|
4
5
|
import { safePath } from './sandbox.js';
|
|
5
6
|
import { createGitTools } from './gitTools.js';
|
|
6
7
|
import { createWebTools } from './web.js';
|
|
@@ -9,7 +10,7 @@ import { createExtraTools } from './extraTools.js';
|
|
|
9
10
|
// user-facing. They must be in ENGLISH (like Claude Code / Codex). Only the
|
|
10
11
|
// strings the OPERATOR sees (help, spinner, service messages) are localized —
|
|
11
12
|
// those live in src/i18n.ts (CATALOG) and are picked by the locale setting.
|
|
12
|
-
export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
13
|
+
export function createTools(workdir, { undo, todos, readOnly = false, subagents = false, } = {}) {
|
|
13
14
|
const root = path.resolve(workdir);
|
|
14
15
|
const safe = (p) => safePath(root, p);
|
|
15
16
|
// Extracts text from content/content_base64. base64 is needed because the
|
|
@@ -26,6 +27,17 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
26
27
|
// that omits `path` (or sends it as null) becomes String(undefined) ===
|
|
27
28
|
// "undefined" and the tool silently creates a file literally named
|
|
28
29
|
// "undefined" in the working directory. `req()` rejects that up front.
|
|
30
|
+
// A NUL byte is the classic binary signal (text files essentially never
|
|
31
|
+
// contain one). We only scan the head: a huge archive must not be walked
|
|
32
|
+
// byte-by-byte just to decide it is not text.
|
|
33
|
+
const isBinaryBuffer = (buf) => {
|
|
34
|
+
const n = Math.min(buf.length, 8000);
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
if (buf[i] === 0)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
29
41
|
const req = (v, name) => {
|
|
30
42
|
if (v === undefined || v === null) {
|
|
31
43
|
throw new Error(`Missing required argument: ${name}`);
|
|
@@ -42,7 +54,10 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
42
54
|
description: 'Read a file. Optional: offset and limit (lines). ' +
|
|
43
55
|
'numbered=true adds cat -n style line numbers (1-based) for reference ' +
|
|
44
56
|
'only — do NOT copy the numbers into Edit old_string. ' +
|
|
45
|
-
'Lines longer than 2000 chars are truncated.'
|
|
57
|
+
'Lines longer than 2000 chars are truncated. ' +
|
|
58
|
+
'Binary files (images, archives) are not decoded: an image returns its ' +
|
|
59
|
+
'path/size as a marker (the operator can paste it to attach it), and ' +
|
|
60
|
+
'other binaries return a short hex header instead of mojibake.',
|
|
46
61
|
parameters: {
|
|
47
62
|
path: 'string',
|
|
48
63
|
offset: 'number?',
|
|
@@ -51,7 +66,34 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
51
66
|
},
|
|
52
67
|
fn: async ({ path: p, offset, limit, numbered }) => {
|
|
53
68
|
const file = safe(req(p, 'path'));
|
|
54
|
-
|
|
69
|
+
// Binary guard (N35): reading an image/archive as utf-8 produced
|
|
70
|
+
// mojibake the model tried to reason about. Detect a binary buffer
|
|
71
|
+
// BEFORE decoding and answer with useful metadata instead.
|
|
72
|
+
const buf = await fs.readFile(file);
|
|
73
|
+
if (isImageName(file)) {
|
|
74
|
+
return ('[image: ' +
|
|
75
|
+
p +
|
|
76
|
+
'] (' +
|
|
77
|
+
guessMime(file) +
|
|
78
|
+
', ' +
|
|
79
|
+
formatSize(buf.length) +
|
|
80
|
+
') — this is an image; its pixels cannot be read as text. To have ' +
|
|
81
|
+
'the model SEE it, the operator can paste/drag the file into the ' +
|
|
82
|
+
'chat (it becomes an [image#N] attachment).');
|
|
83
|
+
}
|
|
84
|
+
if (isBinaryBuffer(buf)) {
|
|
85
|
+
const head = buf.subarray(0, 16);
|
|
86
|
+
return ('(binary file: ' +
|
|
87
|
+
p +
|
|
88
|
+
', ' +
|
|
89
|
+
formatSize(buf.length) +
|
|
90
|
+
', ' +
|
|
91
|
+
guessMime(file) +
|
|
92
|
+
')\nFirst bytes (hex): ' +
|
|
93
|
+
Buffer.from(head).toString('hex') +
|
|
94
|
+
'\nUse Bash (e.g. `file`, `xxd`, `unzip -l`) to inspect it.');
|
|
95
|
+
}
|
|
96
|
+
const content = buf.toString('utf-8');
|
|
55
97
|
if (content === '')
|
|
56
98
|
return '(file is empty)';
|
|
57
99
|
const lines = content.split('\n');
|
|
@@ -126,13 +168,43 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
126
168
|
{
|
|
127
169
|
name: 'Bash',
|
|
128
170
|
description: 'Run a shell command in the working directory (cmd.exe on Windows, sh on Linux/macOS). ' +
|
|
129
|
-
'Do not use for long-running processes (servers) — they will hit the timeout. ' +
|
|
130
171
|
'Do not use for commands that require interactive input. ' +
|
|
172
|
+
'For a long-running process (dev server, watch, long build) set ' +
|
|
173
|
+
'run_in_background=true: it starts the command and returns an id ' +
|
|
174
|
+
'immediately; poll its output with BashOutput and stop it with BashOutput kill. ' +
|
|
131
175
|
'For git use the Git* tools. For the web use WebFetch / WebSearch.',
|
|
132
|
-
parameters: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
parameters: {
|
|
177
|
+
command: 'string',
|
|
178
|
+
timeout: 'number?',
|
|
179
|
+
run_in_background: 'boolean?',
|
|
180
|
+
},
|
|
181
|
+
fn: async ({ command, timeout, run_in_background }, ctx) => {
|
|
182
|
+
const cmd = req(command, 'command');
|
|
183
|
+
assertCommandInsideRoot(root, cmd);
|
|
184
|
+
if (run_in_background) {
|
|
185
|
+
const id = startBackground(workdir, cmd);
|
|
186
|
+
return ('Started in background with id ' +
|
|
187
|
+
id +
|
|
188
|
+
'. Poll it with BashOutput (id="' +
|
|
189
|
+
id +
|
|
190
|
+
'") and stop it with BashOutput (id="' +
|
|
191
|
+
id +
|
|
192
|
+
'", kill=true).');
|
|
193
|
+
}
|
|
194
|
+
return runShell(workdir, cmd, timeout, ctx?.signal);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'BashOutput',
|
|
199
|
+
description: 'Read the accumulated stdout+stderr of a process started with Bash ' +
|
|
200
|
+
'run_in_background=true, and optionally kill it. id — the id returned ' +
|
|
201
|
+
'by Bash. kill=true terminates the process (and its children).',
|
|
202
|
+
parameters: { id: 'string', kill: 'boolean?' },
|
|
203
|
+
fn: async ({ id, kill }) => {
|
|
204
|
+
const pid = req(id, 'id');
|
|
205
|
+
if (kill)
|
|
206
|
+
return killBackground(pid);
|
|
207
|
+
return pollBackground(pid);
|
|
136
208
|
},
|
|
137
209
|
},
|
|
138
210
|
{
|
|
@@ -200,11 +272,34 @@ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
|
|
|
200
272
|
parameters: { message: 'string' },
|
|
201
273
|
fn: async ({ message }) => message,
|
|
202
274
|
};
|
|
275
|
+
// `Task` is a SEAM handled by the agent loop (onSubagent), not an ordinary
|
|
276
|
+
// fn. Its body here is only a fallback for a direct unit-test call; the live
|
|
277
|
+
// path never reaches it because agent-loop intercepts the name first.
|
|
278
|
+
const taskTool = {
|
|
279
|
+
name: 'Task',
|
|
280
|
+
description: 'Delegate a self-contained sub-task to a SUBAGENT that works in its own ' +
|
|
281
|
+
'separate chat with an isolated context, then returns ONLY a final ' +
|
|
282
|
+
'report. Use it to keep heavy research out of the main context. ' +
|
|
283
|
+
'subagent_type: "explore" (read-only: search/read, cannot modify) or ' +
|
|
284
|
+
'"general" (full tools). The subagent does NOT see this conversation, so ' +
|
|
285
|
+
'`prompt` must be fully self-contained: state the goal, the exact ' +
|
|
286
|
+
'questions, and any file paths/context it needs. Returns the subagent ' +
|
|
287
|
+
'report as text.',
|
|
288
|
+
parameters: {
|
|
289
|
+
description: 'string',
|
|
290
|
+
prompt: 'string',
|
|
291
|
+
subagent_type: 'string?',
|
|
292
|
+
},
|
|
293
|
+
fn: async () => 'Subagents are not available in this run. Do the work yourself.',
|
|
294
|
+
};
|
|
295
|
+
// In plan (read-only) mode `Task` is dropped too: a `general` subagent could
|
|
296
|
+
// otherwise write through it and defeat the read-only promise.
|
|
203
297
|
const all = [
|
|
204
298
|
...baseTools,
|
|
205
299
|
...extraTools,
|
|
206
300
|
...gitTools,
|
|
207
301
|
...webTools,
|
|
302
|
+
...(subagents && !readOnly ? [taskTool] : []),
|
|
208
303
|
respondTool,
|
|
209
304
|
];
|
|
210
305
|
return readOnly ? filterToolsForReadOnly(all) : all;
|
|
@@ -220,6 +315,7 @@ export const MUTATING_TOOLS = new Set([
|
|
|
220
315
|
'MultiEdit',
|
|
221
316
|
'ApplyPatch',
|
|
222
317
|
'Bash',
|
|
318
|
+
'BashOutput',
|
|
223
319
|
'GitAdd',
|
|
224
320
|
'GitCommit',
|
|
225
321
|
'GitPush',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.0",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — 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": {
|
|
@@ -17,21 +17,23 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node ./dist/index.js",
|
|
19
19
|
"dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev --headed",
|
|
20
|
-
"clean": "
|
|
21
|
-
"build": "
|
|
22
|
-
"coverage:gate": "
|
|
20
|
+
"clean": "tsx scripts/clean-dist.mts",
|
|
21
|
+
"build": "tsx scripts/clean-dist.mts && tsc -p tsconfig.build.json",
|
|
22
|
+
"coverage:gate": "tsx scripts/coverage-gate.mts",
|
|
23
23
|
"changelog:draft": "tsx scripts/changelog-draft.mts",
|
|
24
|
+
"changelog:sync": "tsx scripts/changelog-draft.mts --write",
|
|
25
|
+
"changelog:check": "tsx scripts/changelog-draft.mts --check",
|
|
24
26
|
"release": "tsx scripts/release.mts",
|
|
25
27
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"lint": "
|
|
27
|
-
"lint:fix": "
|
|
28
|
+
"lint": "tsx scripts/eslint-ts6.mts",
|
|
29
|
+
"lint:fix": "tsx scripts/eslint-ts6.mts --fix",
|
|
28
30
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json,md}\"",
|
|
29
31
|
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
30
32
|
"prepublishOnly": "npm run build:readme && npm run build",
|
|
31
33
|
"test": "tsx --test test/*.test.ts",
|
|
32
34
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
33
|
-
"self-smoke": "tsx scripts/self-smoke.
|
|
34
|
-
"render-demo": "
|
|
35
|
+
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
|
+
"render-demo": "tsx scripts/render-demo.mts",
|
|
35
37
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
36
38
|
"postinstall": "node scripts/postinstall.mjs",
|
|
37
39
|
"prepare": "husky"
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
2
5
|
import {
|
|
3
6
|
parseConventionalCommit,
|
|
4
7
|
groupCommits,
|
|
5
8
|
renderChangelogDraft,
|
|
9
|
+
syncUnreleased,
|
|
6
10
|
} from '../src/changelog.js'
|
|
7
11
|
|
|
8
12
|
// Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
|
|
@@ -13,6 +17,8 @@ import {
|
|
|
13
17
|
// Usage:
|
|
14
18
|
// npm run changelog:draft # since the latest tag, or an explicit ref
|
|
15
19
|
// npm run changelog:draft -- v2.63.0
|
|
20
|
+
// npm run changelog:draft -- --write # update the Unreleased section in place
|
|
21
|
+
// npm run changelog:draft -- --check # exit 1 if Unreleased is out of date
|
|
16
22
|
|
|
17
23
|
function git(args: string[]): string {
|
|
18
24
|
const r = spawnSync('git', args, { encoding: 'utf-8' })
|
|
@@ -20,7 +26,10 @@ function git(args: string[]): string {
|
|
|
20
26
|
return (r.stdout || '').toString()
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
const
|
|
29
|
+
const argv = process.argv.slice(2)
|
|
30
|
+
const write = argv.includes('--write')
|
|
31
|
+
const check = argv.includes('--check')
|
|
32
|
+
const argRef = argv.find((a) => !a.startsWith('-'))
|
|
24
33
|
|
|
25
34
|
// Resolve the start ref: an explicit arg, otherwise the latest tag,
|
|
26
35
|
// otherwise the root commit.
|
|
@@ -46,6 +55,34 @@ const parsed = subjects
|
|
|
46
55
|
.filter((c): c is NonNullable<typeof c> => c !== null)
|
|
47
56
|
const body = renderChangelogDraft(groupCommits(parsed))
|
|
48
57
|
|
|
58
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
59
|
+
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
60
|
+
|
|
61
|
+
// D2: sync the `## [Unreleased]` section in CHANGELOG.md from the conventional
|
|
62
|
+
// commits since the last tag. `--write` edits the file; `--check` fails when
|
|
63
|
+
// the committed section is out of date (for CI); neither just prints the body.
|
|
64
|
+
if (write || check) {
|
|
65
|
+
const onDisk = fs.readFileSync(changelogPath, 'utf-8')
|
|
66
|
+
const nextText = syncUnreleased(onDisk, body)
|
|
67
|
+
if (write) {
|
|
68
|
+
if (nextText !== onDisk) {
|
|
69
|
+
fs.writeFileSync(changelogPath, nextText)
|
|
70
|
+
console.log('changelog: updated the Unreleased section')
|
|
71
|
+
} else {
|
|
72
|
+
console.log('changelog: Unreleased already up to date')
|
|
73
|
+
}
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
if (nextText !== onDisk) {
|
|
77
|
+
console.error(
|
|
78
|
+
'changelog: the Unreleased section is out of date — run `npm run changelog:sync`',
|
|
79
|
+
)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
console.log('changelog: Unreleased is up to date')
|
|
83
|
+
process.exit(0)
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
if (!body) {
|
|
50
87
|
console.error('no conventional commits since ' + ref)
|
|
51
88
|
process.exit(0)
|
|
@@ -11,6 +11,12 @@ import Module from 'module'
|
|
|
11
11
|
import path from 'path'
|
|
12
12
|
import { fileURLToPath } from 'url'
|
|
13
13
|
|
|
14
|
+
// Module._resolveFilename is a private Node field (not in @types/node), so it
|
|
15
|
+
// is reached through a small structural type instead of `any`.
|
|
16
|
+
type ModuleWithResolve = typeof Module & {
|
|
17
|
+
_resolveFilename: (request: string, ...rest: unknown[]) => string
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
const require = createRequire(import.meta.url)
|
|
15
21
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
16
22
|
const ts6 = path.join(here, '..', 'node_modules', 'typescript-6')
|
|
@@ -26,16 +32,17 @@ try {
|
|
|
26
32
|
|
|
27
33
|
// Patch Module._resolveFilename so any require('typescript') resolves to the
|
|
28
34
|
// TS 6 package instead of the TS 7 the project depends on.
|
|
29
|
-
const
|
|
30
|
-
|
|
35
|
+
const M = Module as ModuleWithResolve
|
|
36
|
+
const origResolve = M._resolveFilename
|
|
37
|
+
M._resolveFilename = (request: string, ...rest: unknown[]) => {
|
|
31
38
|
if (request === 'typescript') {
|
|
32
39
|
try {
|
|
33
40
|
return require.resolve(path.join(ts6, 'lib', 'typescript.js'))
|
|
34
41
|
} catch {
|
|
35
|
-
return origResolve
|
|
42
|
+
return origResolve(request, ...rest)
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
return origResolve
|
|
45
|
+
return origResolve(request, ...rest)
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
const { ESLint } = await import('eslint')
|
package/scripts/release.mts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
// npm run release patch --dry-run # print the plan, change nothing
|
|
24
24
|
//
|
|
25
25
|
// Run via tsx so it can reuse src/changelog.ts (the conventional-commit
|
|
26
|
-
// parser) — a plain .mjs cannot import the TypeScript sources.
|
|
26
|
+
// parser) — a plain .mjs cannot import the TypeScript sources. The other
|
|
27
|
+
// helper scripts are .mts for the same reason; postinstall stays .mjs because
|
|
28
|
+
// it runs before tsx is guaranteed to be installed.
|
|
27
29
|
|
|
28
30
|
const NL = String.fromCharCode(10)
|
|
29
31
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -43,7 +45,11 @@ function fail(msg: string): never {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function run(cmd: string, args: string[]): void {
|
|
46
|
-
const r = spawnSync(cmd, args, {
|
|
48
|
+
const r = spawnSync(cmd, args, {
|
|
49
|
+
cwd: root,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
shell: process.platform === 'win32',
|
|
52
|
+
})
|
|
47
53
|
if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -100,8 +106,16 @@ if (git(['tag', '--list', tag]).trim()) {
|
|
|
100
106
|
// last tag. English commit subjects become the section body; the operator can
|
|
101
107
|
// still reword the section afterwards.
|
|
102
108
|
const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
|
|
103
|
-
const ref =
|
|
104
|
-
|
|
109
|
+
const ref =
|
|
110
|
+
latestTag ||
|
|
111
|
+
git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() ||
|
|
112
|
+
'HEAD'
|
|
113
|
+
const subjects = git([
|
|
114
|
+
'log',
|
|
115
|
+
'--no-merges',
|
|
116
|
+
'--pretty=format:%s',
|
|
117
|
+
ref + '..HEAD',
|
|
118
|
+
])
|
|
105
119
|
.split(NL)
|
|
106
120
|
.map((s) => s.trim())
|
|
107
121
|
.filter(Boolean)
|
|
@@ -129,7 +143,14 @@ const sectionBody = body
|
|
|
129
143
|
.join(NL)
|
|
130
144
|
const date = new Date().toISOString().slice(0, 10)
|
|
131
145
|
const section =
|
|
132
|
-
'## [' +
|
|
146
|
+
'## [' +
|
|
147
|
+
next +
|
|
148
|
+
'] - ' +
|
|
149
|
+
date +
|
|
150
|
+
NL +
|
|
151
|
+
NL +
|
|
152
|
+
(sectionBody || '### Changed' + NL + NL + '- Internal improvements.') +
|
|
153
|
+
NL
|
|
133
154
|
|
|
134
155
|
if (dryRun) {
|
|
135
156
|
console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
|
|
@@ -148,14 +169,22 @@ if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
|
148
169
|
const afterAnchor = idx + anchor.length
|
|
149
170
|
const updated =
|
|
150
171
|
changelog.slice(0, afterAnchor) +
|
|
151
|
-
NL +
|
|
172
|
+
NL +
|
|
173
|
+
NL +
|
|
174
|
+
section +
|
|
152
175
|
changelog.slice(afterAnchor).replace(/^\n+/, NL)
|
|
153
176
|
fs.writeFileSync(changelogPath, updated)
|
|
154
177
|
|
|
155
178
|
// 6. Bump package.json, preserving its exact formatting (single-line replace
|
|
156
179
|
// of the version value only).
|
|
157
180
|
const pkgText = fs.readFileSync(pkgPath, 'utf-8')
|
|
158
|
-
fs.writeFileSync(
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
pkgPath,
|
|
183
|
+
pkgText.replace(
|
|
184
|
+
'"version": "' + pkg.version + '"',
|
|
185
|
+
'"version": "' + next + '"',
|
|
186
|
+
),
|
|
187
|
+
)
|
|
159
188
|
|
|
160
189
|
// 7. Commit + tag. The push is intentionally NOT done here.
|
|
161
190
|
git(['add', 'package.json', 'CHANGELOG.md'])
|