zames_pro 2.82.139 → 2.82.141
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 +57 -0
- package/README.md +68 -42
- package/SECURITY.md +6 -3
- package/dist/browser.js +8 -2
- package/dist/commands.js +5 -1
- package/dist/compact.js +6 -1
- package/dist/config-command.js +17 -1
- package/dist/config.js +63 -10
- package/dist/context.js +27 -9
- package/dist/i18n/common.js +16 -11
- package/dist/i18n/config.js +9 -2
- package/dist/i18n/deepseek.js +2 -2
- package/dist/i18n/doctor.js +9 -5
- package/dist/i18n/glm.js +2 -2
- package/dist/i18n/help.js +126 -117
- package/dist/i18n/mcp.js +11 -5
- package/dist/i18n/prompt.js +9 -3
- package/dist/i18n/qwen.js +2 -2
- package/dist/i18n/self-review.js +4 -0
- package/dist/i18n/spinner.js +13 -8
- package/dist/index.js +41 -7
- package/dist/mcp.js +4 -1
- package/dist/prompt.js +16 -1
- package/dist/status-command.js +6 -2
- package/dist/system-prompt.js +8 -0
- package/dist/tui/AskQuestion.js +1 -1
- package/dist/tui/TuiEditor.js +7 -0
- package/dist/tui/widgets.js +4 -2
- package/llms-full.txt +35 -22
- package/llms.txt +4 -2
- package/package.json +2 -2
- package/skills/systematic-debugging/SKILL.md +7 -0
- package/skills/test-driven-development/SKILL.md +5 -0
- package/skills/verification-before-completion/SKILL.md +5 -0
- package/skills/writing-plans/SKILL.md +12 -0
package/dist/i18n/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// MCP server messages.
|
|
2
2
|
export const mcpMessages = {
|
|
3
3
|
'help.cmd.mcp': {
|
|
4
|
-
ru: '/mcp
|
|
5
|
-
en: '/mcp
|
|
4
|
+
ru: '/mcp MCP-серверы: число инструментов, ошибки и предупреждения',
|
|
5
|
+
en: '/mcp MCP servers: tool count, errors and warnings',
|
|
6
6
|
},
|
|
7
7
|
'mcp.loaded': {
|
|
8
8
|
ru: 'MCP: подключено инструментов {n} ({servers})',
|
|
@@ -21,15 +21,21 @@ export const mcpMessages = {
|
|
|
21
21
|
en: 'No MCP servers configured.',
|
|
22
22
|
},
|
|
23
23
|
'mcp.hint': {
|
|
24
|
-
ru: 'Добавь ~/.zames/mcp.json
|
|
25
|
-
en: 'Add ~/.zames/mcp.json
|
|
24
|
+
ru: 'Добавь ~/.zames/mcp.json, <project>/.zames/mcp.json или <project>/.mcp.json.',
|
|
25
|
+
en: 'Add ~/.zames/mcp.json, <project>/.zames/mcp.json or <project>/.mcp.json.',
|
|
26
26
|
},
|
|
27
27
|
'mcp.title': {
|
|
28
28
|
ru: 'MCP-серверы (инструментов: {n}):',
|
|
29
29
|
en: 'MCP servers ({n} tools):',
|
|
30
30
|
},
|
|
31
|
+
'mcp.floating': {
|
|
32
|
+
ru: 'MCP-сервер «{v}» использует плавающую версию (@latest/@next); закрепите точную версию, чтобы набор инструментов не менялся между запусками.',
|
|
33
|
+
en: 'MCP server "{v}" uses a floating version (@latest/@next); pin an exact version so the tool set does not change between runs.',
|
|
34
|
+
},
|
|
31
35
|
'mcp.status_error': { ru: '(ошибка: {v})', en: '(error: {v})' },
|
|
32
|
-
|
|
36
|
+
// `{v}` is a COUNT (or 'unknown' when no pool is configured) — not a
|
|
37
|
+
// list of tool names, so the label must not read as one.
|
|
38
|
+
'status.mcp': { ru: 'MCP-инструментов: {v}', en: 'MCP tools: {v}' },
|
|
33
39
|
'status.toggles': {
|
|
34
40
|
ru: 'Размышление: {think} · Поиск: {search}',
|
|
35
41
|
en: 'Deep thinking: {think} · Search: {search}',
|
package/dist/i18n/prompt.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// System-prompt fragments.
|
|
2
2
|
export const promptMessages = {
|
|
3
|
+
// AGENT-FACING: the model reads this, so neither locale may carry Cyrillic
|
|
4
|
+
// prose (see AGENTS.md "Language policy"). Only the LANGUAGE NAME differs,
|
|
5
|
+
// which is what actually steers the answer language.
|
|
3
6
|
'prompt.answer_language': {
|
|
4
|
-
ru: '
|
|
5
|
-
en: 'IMPORTANT:
|
|
7
|
+
ru: 'IMPORTANT: answer the operator in Russian. All text in the message field of the respond tool, and any explanations, must be in Russian.',
|
|
8
|
+
en: 'IMPORTANT: answer the operator in English. All text in the message field of the respond tool, and any explanations, must be in English.',
|
|
6
9
|
},
|
|
7
10
|
// The hard "ONLY TOOL CALLS" block. All prose around a tool call is a
|
|
8
11
|
// protocol violation: the operator never sees it (only tool calls and the
|
|
@@ -10,7 +13,10 @@ export const promptMessages = {
|
|
|
10
13
|
// stop DeepSeek from generating it INSIDE its chat with code (that is the
|
|
11
14
|
// model's output); we can only forbid it by prompt and hide it here.
|
|
12
15
|
'prompt.only_tool_calls': {
|
|
13
|
-
|
|
16
|
+
// AGENT-FACING: the model reads this, so BOTH locales carry the English
|
|
17
|
+
// text (see AGENTS.md "Language policy"): a Russian operator still gets an
|
|
18
|
+
// English protocol block, and the answer LANGUAGE is set separately above.
|
|
19
|
+
ru: '## ONLY TOOL CALLS (hard rule)\n\nTalk to the operator ONLY through tool calls. Any plain text — explanations, plans, reasoning, comments, apologies, greetings, headings, lists, markdown, emoji — is FORBIDDEN. It is not read and counts as an error.\n\nYOUR ONLY OUTPUT is a tool call. Each turn contains exactly one JSON tool-call object (or an array of independent calls), with not a single word before or after.\n\nYou MUST NOT write: "I will now...", "let us look...", "let me check", plans, explanations, progress notes between steps.\n\nALLOWED: only a tool call and, at the very end, when the task is done, respond with the summary.\n\nThe only place where text is allowed is the message field inside respond, and only at the very end.',
|
|
14
20
|
en: '## ONLY TOOL CALLS (hard rule)\n\nTalk to the operator ONLY through tool calls. Any plain text — explanations, plans, reasoning, comments, apologies, greetings, headings, lists, markdown, emoji — is FORBIDDEN. It is not read and counts as an error.\n\nYOUR ONLY OUTPUT is a tool call. Each turn contains exactly one JSON tool-call object (or an array of independent calls), with not a single word before or after.\n\nYou MUST NOT write: "I will now...", "let us look...", "let me check", plans, explanations, progress notes between steps.\n\nALLOWED: only a tool call and, at the very end, when the task is done, respond with the summary.\n\nThe only place where text is allowed is the message field inside respond, and only at the very end.',
|
|
15
21
|
},
|
|
16
22
|
};
|
package/dist/i18n/qwen.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// so provider-specific wording has one home.
|
|
4
4
|
export const qwenMessages = {
|
|
5
5
|
'qwen.guest_mode': {
|
|
6
|
-
ru: 'Qwen: вход не выполнен, работаю в гостевом режиме (
|
|
7
|
-
en: 'Qwen: not signed in, running in guest mode (
|
|
6
|
+
ru: 'Qwen: вход не выполнен, работаю в гостевом режиме (суточная квота гостя ограничена, вложения недоступны). Войдите через /provider login (он сам откроет видимое окно) или запустите с --headed.',
|
|
7
|
+
en: 'Qwen: not signed in, running in guest mode (the guest daily quota is limited, attachments are unavailable). Log in via /provider login (it opens a visible window itself) or launch with --headed.',
|
|
8
8
|
},
|
|
9
9
|
'qwen.login_email_prompt': {
|
|
10
10
|
ru: 'Email для входа в Qwen{def}: ',
|
package/dist/i18n/self-review.js
CHANGED
|
@@ -177,6 +177,10 @@ export const selfReviewMessages = {
|
|
|
177
177
|
ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
|
|
178
178
|
en: 'No saved sessions. They appear after the first task/chat.',
|
|
179
179
|
},
|
|
180
|
+
'sessions.none_here': {
|
|
181
|
+
ru: 'Для этого каталога сохранённых сессий нет — сначала выполните здесь задачу.',
|
|
182
|
+
en: 'No saved session for this directory — run a task here first.',
|
|
183
|
+
},
|
|
180
184
|
'sessions.restore_hint': {
|
|
181
185
|
ru: 'Восстановить: /resume-id <id> (полный id) или выбери сессию в списке /sessions.',
|
|
182
186
|
en: 'Restore: /resume-id <id> (full id) or pick a session from the /sessions list.',
|
package/dist/i18n/spinner.js
CHANGED
|
@@ -25,26 +25,31 @@ export const spinnerMessages = {
|
|
|
25
25
|
},
|
|
26
26
|
'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
|
|
27
27
|
'editor.page_hint': {
|
|
28
|
-
ru: '(Ctrl+N/Ctrl+P —
|
|
29
|
-
en: '(Ctrl+N/Ctrl+P —
|
|
28
|
+
ru: '(Ctrl+N/Ctrl+P — выбор, Tab — вставить)',
|
|
29
|
+
en: '(Ctrl+N/Ctrl+P — select, Tab — insert)',
|
|
30
30
|
},
|
|
31
31
|
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
32
32
|
// Shown in the full-screen log viewport before the first turn, so a fresh
|
|
33
33
|
// chat is not a blank screen with no hint of what to do.
|
|
34
34
|
'editor.empty_hint': {
|
|
35
|
-
ru: 'Напишите задачу и нажмите Enter. /help — команды,
|
|
36
|
-
en: 'Type a task and press Enter. /help for commands,
|
|
35
|
+
ru: 'Напишите задачу и нажмите Enter. /help — команды, \\ + Enter — новая строка.',
|
|
36
|
+
en: 'Type a task and press Enter. /help for commands, \\ + Enter for a new line.',
|
|
37
37
|
},
|
|
38
38
|
// One-line key hint UNDER the input box (full-screen mode). Two variants:
|
|
39
39
|
// with in-app wheel scroll the mouse belongs to the app, so selection needs
|
|
40
40
|
// Shift; without it the terminal keeps the mouse and the wheel is inert.
|
|
41
|
+
// Kept SHORT: the row is truncated, and the long "up/down" / "top/bottom"
|
|
42
|
+
// tails pushed the MODE-specific part (wheel vs inert) off a narrow screen.
|
|
41
43
|
'editor.keys_hint': {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
// The wheel belongs to the app ONLY in this mode; in SELECT mode the
|
|
45
|
+
// terminal keeps it (see useMouseWheel.ts), so that hint never advertises
|
|
46
|
+
// a wheel scroll.
|
|
47
|
+
ru: 'колесо — скролл · выделение — Shift+клик · Esc — стоп · PgUp/PgDn · Home/End',
|
|
48
|
+
en: 'wheel — scroll · select — Shift+click · Esc — stop · PgUp/PgDn · Home/End',
|
|
44
49
|
},
|
|
45
50
|
'editor.keys_hint_select': {
|
|
46
|
-
ru: 'выделение — мышью ·
|
|
47
|
-
en: 'select — drag ·
|
|
51
|
+
ru: 'выделение — мышью · колесо инертно · Esc — стоп · PgUp/PgDn · Home/End',
|
|
52
|
+
en: 'select — drag · wheel inert · Esc — stop · PgUp/PgDn · Home/End',
|
|
48
53
|
},
|
|
49
54
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
50
55
|
'editor.search_prompt': {
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { LineEditor } from './input.js';
|
|
|
20
20
|
import { TuiEditor } from './tui/TuiEditor.js';
|
|
21
21
|
import { promptOnce } from './prompt.js';
|
|
22
22
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
23
|
-
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, } from './config.js';
|
|
23
|
+
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, shadowedConfigKeys, } from './config.js';
|
|
24
24
|
import { runConfigMenu } from './config-menu.js';
|
|
25
25
|
import { runConfigMenuInk } from './tui/run-config-menu.js';
|
|
26
26
|
import { formatConfigList } from './config-commands.js';
|
|
@@ -126,6 +126,17 @@ let currentLocale = isLocale(config.ui?.locale)
|
|
|
126
126
|
// Translation: reads currentLocale at call time, so a language change takes
|
|
127
127
|
// effect immediately, without a restart (for lines printed afterwards).
|
|
128
128
|
const t = (key, params) => translate(currentLocale)(key, params);
|
|
129
|
+
// A project `.zamesrc.json` wins over the HOME config, so a key it ALSO sets
|
|
130
|
+
// silently defeats `/config set` — the value looks saved and comes back stale
|
|
131
|
+
// after a restart (the reported "language resets to English" case). Say it at
|
|
132
|
+
// startup, before a task, instead of letting the operator hunt for it.
|
|
133
|
+
const shadowedAtStartup = shadowedConfigKeys();
|
|
134
|
+
if (shadowedAtStartup.length) {
|
|
135
|
+
console.error(theme.warn(t('cfg.shadowed', {
|
|
136
|
+
file: CONFIG_PATHS.PROJECT_CONFIG,
|
|
137
|
+
keys: shadowedAtStartup.join(', '),
|
|
138
|
+
})));
|
|
139
|
+
}
|
|
129
140
|
// Headless is the default. --headed forces a visible window (useful for the
|
|
130
141
|
// first sign-in or debugging the DOM). --headless keeps it explicit.
|
|
131
142
|
// DeepSeek/Qwen run headless by default; GLM CANNOT: z.ai answers a headless
|
|
@@ -474,9 +485,9 @@ ${theme.bold(t('help.while_working'))}
|
|
|
474
485
|
${t('help.key.search')}
|
|
475
486
|
${t('help.key.words')}
|
|
476
487
|
${t('help.key.undo')}
|
|
488
|
+
${t('help.key.clear_screen')}
|
|
477
489
|
${t('help.key.slash')}
|
|
478
490
|
${t('help.key.newline')}
|
|
479
|
-
${t('help.key.backslash')}
|
|
480
491
|
${t('help.key.attach')}
|
|
481
492
|
${t('help.key.esc')}
|
|
482
493
|
${t('help.key.bang')}
|
|
@@ -692,7 +703,7 @@ async function resolveWorkdir() {
|
|
|
692
703
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
693
704
|
const stat = await fs.stat(dir).catch(() => null);
|
|
694
705
|
if (!stat || !stat.isDirectory()) {
|
|
695
|
-
throw new Error(t('
|
|
706
|
+
throw new Error(t('cd.not_dir', { v: dir }));
|
|
696
707
|
}
|
|
697
708
|
return dir;
|
|
698
709
|
}
|
|
@@ -1052,7 +1063,7 @@ async function main() {
|
|
|
1052
1063
|
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
1053
1064
|
}
|
|
1054
1065
|
for (const w of st.warnings)
|
|
1055
|
-
console.error(theme.warn(w));
|
|
1066
|
+
console.error(theme.warn(t(w.key, w.params)));
|
|
1056
1067
|
}
|
|
1057
1068
|
catch (e) {
|
|
1058
1069
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
@@ -1213,6 +1224,7 @@ async function main() {
|
|
|
1213
1224
|
const recoverFromContextLimit = async () => {
|
|
1214
1225
|
const res = await performCompact({
|
|
1215
1226
|
browser,
|
|
1227
|
+
providerLabel: providerLabel(config.provider),
|
|
1216
1228
|
currentChatId,
|
|
1217
1229
|
workdir: currentWorkdir,
|
|
1218
1230
|
locale: currentLocale,
|
|
@@ -1349,10 +1361,23 @@ async function main() {
|
|
|
1349
1361
|
// never bypassing the send throttle). Started lazily on the first job.
|
|
1350
1362
|
const scheduler = new Scheduler();
|
|
1351
1363
|
let scheduleTimer = null;
|
|
1364
|
+
// Ctrl+C on an idle agent EXITS the process. Without this latch a /loop job
|
|
1365
|
+
// whose task was interrupted (and every later fire) would be dropped into
|
|
1366
|
+
// the queue while nobody waits for it, and each tick printed "fired" into a
|
|
1367
|
+
// dying terminal. A loop is meant to outlive a single task, not the agent.
|
|
1368
|
+
let shuttingDown = false;
|
|
1369
|
+
const stopScheduleTicker = () => {
|
|
1370
|
+
if (scheduleTimer) {
|
|
1371
|
+
clearInterval(scheduleTimer);
|
|
1372
|
+
scheduleTimer = null;
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1352
1375
|
const startScheduleTicker = () => {
|
|
1353
1376
|
if (scheduleTimer)
|
|
1354
1377
|
return;
|
|
1355
1378
|
scheduleTimer = setInterval(() => {
|
|
1379
|
+
if (shuttingDown)
|
|
1380
|
+
return;
|
|
1356
1381
|
const due = scheduler.due(Date.now());
|
|
1357
1382
|
for (const job of due) {
|
|
1358
1383
|
// Do not pile MANY copies of the same job while the agent is busy: if
|
|
@@ -1403,6 +1428,8 @@ async function main() {
|
|
|
1403
1428
|
let reviewMode = null;
|
|
1404
1429
|
process.on('SIGINT', async () => {
|
|
1405
1430
|
running = false;
|
|
1431
|
+
shuttingDown = true;
|
|
1432
|
+
stopScheduleTicker();
|
|
1406
1433
|
// Kill any background processes so no dev-server outlives the agent.
|
|
1407
1434
|
killAllBackground();
|
|
1408
1435
|
await browser.close().catch(() => { });
|
|
@@ -1827,8 +1854,12 @@ async function main() {
|
|
|
1827
1854
|
lastCtrlCAt = now;
|
|
1828
1855
|
}
|
|
1829
1856
|
else {
|
|
1830
|
-
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
1857
|
+
// Not busy — exit. We wake takeInput() so the loop finishes. The
|
|
1858
|
+
// scheduler must stop too: a live /loop ticker keeps firing into the
|
|
1859
|
+
// queue while the process is shutting down.
|
|
1831
1860
|
running = false;
|
|
1861
|
+
shuttingDown = true;
|
|
1862
|
+
stopScheduleTicker();
|
|
1832
1863
|
if (waiter) {
|
|
1833
1864
|
const r = waiter;
|
|
1834
1865
|
waiter = null;
|
|
@@ -2160,6 +2191,7 @@ async function main() {
|
|
|
2160
2191
|
schema: CONFIG_SCHEMA,
|
|
2161
2192
|
homeConfigPath: CONFIG_PATHS.HOME_CONFIG,
|
|
2162
2193
|
projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
|
|
2194
|
+
shadowedKeys: shadowedConfigKeys,
|
|
2163
2195
|
});
|
|
2164
2196
|
}
|
|
2165
2197
|
// Open a chat by its ChatInfo and print the restored dialogue. Shared by
|
|
@@ -2939,7 +2971,7 @@ async function main() {
|
|
|
2939
2971
|
// finding it meant /chats (a network fetch) or copying the id.
|
|
2940
2972
|
const last = loadLastSession(currentWorkdir, config.provider);
|
|
2941
2973
|
if (!last || !last.id) {
|
|
2942
|
-
console.error(theme.error(t('sessions.
|
|
2974
|
+
console.error(theme.error(t('sessions.none_here')));
|
|
2943
2975
|
continue;
|
|
2944
2976
|
}
|
|
2945
2977
|
if (editor)
|
|
@@ -3222,7 +3254,7 @@ async function main() {
|
|
|
3222
3254
|
}
|
|
3223
3255
|
}
|
|
3224
3256
|
for (const w of st.warnings)
|
|
3225
|
-
console.log(theme.warn(' ' + w));
|
|
3257
|
+
console.log(theme.warn(' ' + t(w.key, w.params)));
|
|
3226
3258
|
continue;
|
|
3227
3259
|
}
|
|
3228
3260
|
if (lower === '/skills') {
|
|
@@ -3704,6 +3736,7 @@ async function main() {
|
|
|
3704
3736
|
// paths stay identical (same retries, same fallback, same resend).
|
|
3705
3737
|
const res = await performCompact({
|
|
3706
3738
|
browser,
|
|
3739
|
+
providerLabel: providerLabel(config.provider),
|
|
3707
3740
|
currentChatId,
|
|
3708
3741
|
workdir: currentWorkdir,
|
|
3709
3742
|
locale: currentLocale,
|
|
@@ -4167,6 +4200,7 @@ async function main() {
|
|
|
4167
4200
|
? async () => {
|
|
4168
4201
|
const res = await performCompact({
|
|
4169
4202
|
browser,
|
|
4203
|
+
providerLabel: providerLabel(config.provider),
|
|
4170
4204
|
currentChatId,
|
|
4171
4205
|
workdir: currentWorkdir,
|
|
4172
4206
|
locale: currentLocale,
|
package/dist/mcp.js
CHANGED
|
@@ -486,7 +486,10 @@ export async function createMcpPool(opts) {
|
|
|
486
486
|
status: () => ({
|
|
487
487
|
servers: infos,
|
|
488
488
|
toolCount: tools.length,
|
|
489
|
-
warnings: floating.map((n) =>
|
|
489
|
+
warnings: floating.map((n) => ({
|
|
490
|
+
key: 'mcp.floating',
|
|
491
|
+
params: { v: n },
|
|
492
|
+
})),
|
|
490
493
|
}),
|
|
491
494
|
close: async () => {
|
|
492
495
|
if (closed)
|
package/dist/prompt.js
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
11
11
|
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
12
12
|
// treated as submission.
|
|
13
|
+
//
|
|
14
|
+
// A `data` chunk is a stream of UTF-8 BYTES and a terminal hands them over in
|
|
15
|
+
// whatever sizes the pty chose: an emoji (4 bytes) or any other non-ASCII
|
|
16
|
+
// character can be split between two reads. `Buffer#toString('utf-8')` decodes
|
|
17
|
+
// every read on its own, so a split character became a run of U+FFFD — the
|
|
18
|
+
// operator's answer (a login, a path, a Cyrillic task) arrived corrupted. The
|
|
19
|
+
// decoder below holds back an INCOMPLETE trailing sequence until the bytes
|
|
20
|
+
// that finish it arrive, and every read (keys, paste, the pasted tail) goes
|
|
21
|
+
// through it. The non-TTY path instead CONCATENATES the raw buffers and decodes
|
|
22
|
+
// once at EOF, so a split there heals by itself.
|
|
23
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
13
24
|
export async function promptOnce(question) {
|
|
14
25
|
const stdin = process.stdin;
|
|
15
26
|
const stdout = process.stdout;
|
|
@@ -71,8 +82,12 @@ export async function promptOnce(question) {
|
|
|
71
82
|
line = line.slice(0, cursor) + clean + line.slice(cursor);
|
|
72
83
|
cursor += clean.length;
|
|
73
84
|
};
|
|
85
|
+
// One decoder for the WHOLE read loop: it keeps the bytes of a character
|
|
86
|
+
// that was split between two `data` events and prepends them to the next
|
|
87
|
+
// read, instead of turning both halves into U+FFFD.
|
|
88
|
+
const decoder = new StringDecoder('utf8');
|
|
74
89
|
const onData = (buf) => {
|
|
75
|
-
let s =
|
|
90
|
+
let s = decoder.write(buf);
|
|
76
91
|
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
77
92
|
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
78
93
|
// If newlines came TOGETHER with other text in one chunk — that's a
|
package/dist/status-command.js
CHANGED
|
@@ -7,7 +7,7 @@ export function formatStatus(s, t = translate('en')) {
|
|
|
7
7
|
const onoff = (b) => (b ? t('common.on') : t('common.off'));
|
|
8
8
|
const lines = [];
|
|
9
9
|
lines.push(t('status.workdir', { v: s.workdir }));
|
|
10
|
-
lines.push(t('status.review_mode', { v: s.reviewMode ||
|
|
10
|
+
lines.push(t('status.review_mode', { v: s.reviewMode || no }));
|
|
11
11
|
if (s.reviewMode && s.origDir) {
|
|
12
12
|
lines.push(t('status.orig_dir', { v: s.origDir }));
|
|
13
13
|
}
|
|
@@ -36,7 +36,11 @@ export function formatStatus(s, t = translate('en')) {
|
|
|
36
36
|
// A NEGATIVE tool count is nonsense (a broken MCP server status, a
|
|
37
37
|
// hand-edited value): "MCP tools: -2" reads like a real measurement, the
|
|
38
38
|
// same rule the token counter below already follows.
|
|
39
|
-
|
|
39
|
+
// `none` ("(none)") would claim that the tools themselves are absent;
|
|
40
|
+
// with no MCP pool configured the COUNT is simply unknown.
|
|
41
|
+
const mcpCount = s.mcpToolCount === null || s.mcpToolCount < 0
|
|
42
|
+
? t('common.unknown')
|
|
43
|
+
: s.mcpToolCount;
|
|
40
44
|
lines.push(t('status.mcp', { v: mcpCount }));
|
|
41
45
|
// A NEGATIVE count is nonsense (a broken provider counter): show "unknown"
|
|
42
46
|
// rather than a number that reads like a real measurement.
|
package/dist/system-prompt.js
CHANGED
|
@@ -351,6 +351,11 @@ and the (MCP server: ...) note in its description tells you which server
|
|
|
351
351
|
it belongs to. The JSON Schema in the description lists the exact fields
|
|
352
352
|
(including their descriptions and enums) - follow it.
|
|
353
353
|
|
|
354
|
+
In plan (read-only) mode an MCP tool whose name looks mutating (click,
|
|
355
|
+
type, write, ...) is REMOVED from your tool set, exactly like the
|
|
356
|
+
built-in writers. The list you were given is the whole truth: if a tool
|
|
357
|
+
is not in it, it is not available in this run - do not try to call it.
|
|
358
|
+
|
|
354
359
|
## How to use tools
|
|
355
360
|
|
|
356
361
|
To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
|
|
@@ -420,6 +425,9 @@ reliably, pass the content in base64 instead of plain text:
|
|
|
420
425
|
|
|
421
426
|
- Write: use 'content_base64' instead of 'content'.
|
|
422
427
|
- Edit: use 'old_base64' / 'new_base64' instead of 'old_string' / 'new_string'.
|
|
428
|
+
- MultiEdit: use 'edits_base64' (base64 of the JSON array) instead of 'edits'.
|
|
429
|
+
- TodoWrite: use 'todos_base64' (base64 of the JSON array) instead of 'todos'.
|
|
430
|
+
- ApplyPatch: use 'patch_base64' instead of 'patch'.
|
|
423
431
|
|
|
424
432
|
base64 alphabet is [A-Za-z0-9+/=] and is not corrupted. Encode with the
|
|
425
433
|
base64 command or in Node via Buffer.from(text).toString('base64').
|
package/dist/tui/AskQuestion.js
CHANGED
|
@@ -91,7 +91,7 @@ export function AskQuestion({ question, options, recommended, allowText, locale
|
|
|
91
91
|
out.push(line);
|
|
92
92
|
return out.length ? out : [''];
|
|
93
93
|
};
|
|
94
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + t('askuser.title') }), wrap(question).map((line, i) => (_jsx(Text, { color: UI.text, children: line }, i))), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(innerCols) }), typing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('askuser.your_answer') }), _jsxs(Text, { color: UI.accent, children: ['❯ ', text, '▏'] })] })) : (rows.map((label, i) => {
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + t('askuser.title') }), wrap(question).map((line, i) => (_jsx(Text, { color: UI.text, children: line }, i))), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(innerCols) }), typing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('askuser.your_answer') }), _jsx(Text, { color: UI.dim, children: t('askuser.enter_send') }), _jsxs(Text, { color: UI.accent, children: ['❯ ', text, '▏'] })] })) : (rows.map((label, i) => {
|
|
95
95
|
const sel = i === cursor;
|
|
96
96
|
const isRec = i === recommended;
|
|
97
97
|
return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: label }), isRec ? (_jsx(Text, { color: "#6fd0b0", children: ' (' + t('askuser.recommended') + ')' })) : null] }, i));
|
package/dist/tui/TuiEditor.js
CHANGED
|
@@ -134,6 +134,13 @@ export class TuiEditor {
|
|
|
134
134
|
// printAbove), so Ink must not patch them: an unmount (clearScreen)
|
|
135
135
|
// would otherwise restore the native console and lose that routing.
|
|
136
136
|
patchConsole: false,
|
|
137
|
+
// Shift+Enter is INDISTINGUISHABLE from a plain Enter unless the
|
|
138
|
+
// terminal disambiguates escape codes: both arrive as CR. The kitty
|
|
139
|
+
// keyboard protocol makes the modified form a distinct CSI-u
|
|
140
|
+
// sequence, which is what `dispatchKey` maps to a newline.
|
|
141
|
+
// 'auto' probes the terminal (CSI ? u) and stays silent when the
|
|
142
|
+
// terminal does not answer, so an unsupporting one is unaffected.
|
|
143
|
+
kittyKeyboard: { mode: 'auto' },
|
|
137
144
|
alternateScreen: this.alternateScreen,
|
|
138
145
|
});
|
|
139
146
|
this.rendered = true;
|
package/dist/tui/widgets.js
CHANGED
|
@@ -45,8 +45,10 @@ export function HintBar({ hints, }) {
|
|
|
45
45
|
*
|
|
46
46
|
* The agent's answers are printed as pre-rendered ANSI (markdown.ts already
|
|
47
47
|
* highlights with cli-highlight), so the live log does NOT use this component —
|
|
48
|
-
* it exists for menus/panels that show a snippet inline
|
|
49
|
-
*
|
|
48
|
+
* it exists for menus/panels that show a snippet inline. No call site today:
|
|
49
|
+
* the Ctrl+Y / `/copy-code` feature that used it was removed, and plain `/copy`
|
|
50
|
+
* copies the whole answer, so `extractCodeBlocks` (src/code-blocks.ts) is only
|
|
51
|
+
* reached from its own tests.
|
|
50
52
|
*/
|
|
51
53
|
export function CodeBlock({ code, lang, highlighted, width, hint, focused, }) {
|
|
52
54
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? UI.accent : UI.border, paddingX: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.dim, children: lang || 'code' }), hint ? _jsx(Text, { color: UI.accent, children: hint }) : null] }), (highlighted || code).split(String.fromCharCode(10)).map((line, i) => (_jsx(Text, { children: line || ' ' }, i)))] }));
|
package/llms-full.txt
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# zames_pro — full documentation for LLMs
|
|
2
2
|
|
|
3
3
|
> Terminal coding agent that drives a real web chat LLM (DeepSeek, Qwen,
|
|
4
|
-
> GLM) through a
|
|
4
|
+
> GLM) through a Playwright browser (DeepSeek and Qwen headless, GLM in a
|
|
5
|
+
> visible window) — no API key required.
|
|
5
6
|
> Package: https://www.npmjs.com/package/zames_pro
|
|
6
7
|
> Source: https://github.com/Viqto0r/zames_pro
|
|
7
8
|
|
|
@@ -111,7 +112,9 @@ Key pieces:
|
|
|
111
112
|
- **Subagents** — the `Task` tool delegates a self-contained sub-task to an
|
|
112
113
|
agent in its own separate chat (isolated context), which returns only a
|
|
113
114
|
report, so heavy research does not flood the main context. On by default;
|
|
114
|
-
|
|
115
|
+
`browser.subagents` turns it off and `browser.maxSubagents` caps the runs
|
|
116
|
+
per TASK (default 3). Calls are sequential, not parallel, and the subtype
|
|
117
|
+
matters: `explore` is read-only, `general` may edit files.
|
|
115
118
|
- **MCP support** — plug in external tool servers (e.g. `@playwright/mcp`).
|
|
116
119
|
- **Scheduling** — `/loop`, `/cron` and `/jobs` repeat tasks on a timer.
|
|
117
120
|
- **Bilingual UI** — Russian / English (`/config lang`).
|
|
@@ -271,8 +274,12 @@ The prompt is a small line editor with persistent history:
|
|
|
271
274
|
- `Ctrl+_` — undo the last edit in the input line (a fat-fingered `Ctrl+U` /
|
|
272
275
|
`Ctrl+K` is recoverable).
|
|
273
276
|
- `Ctrl+U` — clear the line, `Ctrl+K` — delete to end of line, `Ctrl+W` —
|
|
274
|
-
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words
|
|
275
|
-
|
|
277
|
+
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words,
|
|
278
|
+
`Ctrl+Delete`/`Alt+Delete` — delete the word after the cursor.
|
|
279
|
+
- `\` + `Enter` and `Ctrl+J` — insert a newline; these two work in
|
|
280
|
+
every terminal. `Ctrl+Enter` and `Shift+Enter` do it too, but only where the
|
|
281
|
+
terminal implements the kitty keyboard protocol: without it the terminal
|
|
282
|
+
sends a PLAIN Enter for Shift+Enter and the message is sent.
|
|
276
283
|
- `/` + `Tab` — slash-command hints and completion.
|
|
277
284
|
- `!command` — run a shell command directly, bypassing the model (like
|
|
278
285
|
Claude Code's bash mode). The same sandbox guard as the `Bash` tool applies,
|
|
@@ -286,9 +293,10 @@ selected without leaving the terminal:
|
|
|
286
293
|
wheel never scrolls the input history — history is arrow keys only.
|
|
287
294
|
- `ui.mouseScroll=false` — the terminal owns the mouse again: normal text
|
|
288
295
|
selection works and the wheel is inert.
|
|
289
|
-
- `PgUp` / `PgDn`
|
|
290
|
-
|
|
291
|
-
input line is empty
|
|
296
|
+
- the mouse wheel, `PgUp` / `PgDn` (half a screen) and `Home` / `End` (the very
|
|
297
|
+
top / bottom) scroll the dialog; the wheel needs `ui.mouseScroll=true` (the default), and
|
|
298
|
+
Home/End jump only while the input line is empty (with text typed they
|
|
299
|
+
move the caret as usual).
|
|
292
300
|
- To copy a LARGE block, use `/export` (writes the whole session as Markdown)
|
|
293
301
|
or the transcript file (`/transcript` shows the path) — the terminal's own
|
|
294
302
|
drag-select cannot reach past one screen.
|
|
@@ -297,8 +305,8 @@ Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
|
297
305
|
|
|
298
306
|
### Images and files
|
|
299
307
|
|
|
300
|
-
You can paste an image or a file into the input line (
|
|
301
|
-
|
|
308
|
+
You can paste an image or a file into the input line (Shift+Insert or the
|
|
309
|
+
terminal's own paste; for a FILE from the clipboard press Ctrl+V). zames saves it under `<project>/tmp` and shows a
|
|
302
310
|
marker in the line - `[image#1]` for images, `[file#1]` for other files - then
|
|
303
311
|
uploads the real file to the chat together with your message. These are the
|
|
304
312
|
same formats the DeepSeek / Qwen / GLM web chat accepts (PNG, JPEG, GIF, WEBP, BMP,
|
|
@@ -368,6 +376,8 @@ The agent has the same style of tools as Claude Code / Codex CLI:
|
|
|
368
376
|
- **Git** — `GitStatus`, `GitDiff`, `GitLog`, `GitShow`, `GitBranchList`,
|
|
369
377
|
`GitAdd`, `GitCommit`, `GitPush`.
|
|
370
378
|
- **Web** — `WebFetch`, `WebSearch`.
|
|
379
|
+
- **Subagents** — `Task` (delegate a self-contained sub-task to an agent in
|
|
380
|
+
its own chat; `explore` is read-only, `general` may edit; capped per task).
|
|
371
381
|
- **Operator** — `AskUser` (a modal question with options; skipped in plan
|
|
372
382
|
mode so a plan is never interrupted).
|
|
373
383
|
- **Service** — `respond` (final answer to the operator, ends the task).
|
|
@@ -415,8 +425,9 @@ commands (/new or /clear, /sessions, /chats, /resume-id, /last, /chat, /cd,
|
|
|
415
425
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
416
426
|
and report findings (no code changes).
|
|
417
427
|
- /plan [on|off] — plan (read-only) mode. While it is on, the mutating tools
|
|
418
|
-
(Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush
|
|
419
|
-
from the tool set, so
|
|
428
|
+
(Write/Edit/MultiEdit/ApplyPatch/Bash/BashOutput, GitAdd/GitCommit/GitPush,
|
|
429
|
+
AskUser) are removed from the tool set, and so are MCP tools with a mutating
|
|
430
|
+
name, so the agent can investigate without touching the tree.
|
|
420
431
|
Start in it with `--plan`.
|
|
421
432
|
- /compact — ask the active backend to compress the current chat into a handover
|
|
422
433
|
summary, then open a NEW chat, resend the system prompt and post the summary
|
|
@@ -424,9 +435,10 @@ commands (/new or /clear, /sessions, /chats, /resume-id, /last, /chat, /cd,
|
|
|
424
435
|
- /goal <condition> — set a **completion condition** (Claude Code parity). The
|
|
425
436
|
agent starts working toward it at once and keeps starting new turns until a
|
|
426
437
|
separate evaluator model judges the condition met. The goal clears itself
|
|
427
|
-
automatically when it is met (or judged impossible), or with `/goal clear
|
|
428
|
-
The condition is stored in `<project>/.zames-goal`
|
|
429
|
-
on the next launch. `/goal` with no argument
|
|
438
|
+
automatically when it is met (or judged impossible), or with `/goal clear`
|
|
439
|
+
(or its synonym `/goal off`). The condition is stored in `<project>/.zames-goal`
|
|
440
|
+
(git-ignored) and restored on the next launch. `/goal` with no argument — or
|
|
441
|
+
`/goal on` — shows the current state.
|
|
430
442
|
- /loop <interval> <task> — repeat a task periodically (e.g. `/loop 10m run the
|
|
431
443
|
tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> — run on a schedule.
|
|
432
444
|
/jobs [rm <id>|clear] lists and stops them. A fired job is put into the same
|
|
@@ -440,8 +452,9 @@ tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> — run on a schedule.
|
|
|
440
452
|
"Web search" item in the "+" menu.
|
|
441
453
|
These (and /queue, /jobs, /goal, `/config <sub>`) can be used WHILE the agent
|
|
442
454
|
is working — they do not touch the in-flight generation.
|
|
443
|
-
- /queue [clear] — list or clear
|
|
444
|
-
current
|
|
455
|
+
- /queue [list|ls|clear|c|clean] — list (`list`/`ls`) or clear
|
|
456
|
+
(`clear`/`c`/`clean`) the messages waiting to be sent after the current
|
|
457
|
+
task.
|
|
445
458
|
|
|
446
459
|
The token context is also shown live: the status line above the input has the
|
|
447
460
|
spinner/text on the left and the context on the right (e.g. `125k · 13%`). The
|
|
@@ -547,8 +560,7 @@ too):
|
|
|
547
560
|
|
|
548
561
|
- **Memory** (`@modelcontextprotocol/server-memory`) — a local knowledge graph
|
|
549
562
|
(9 tools) for durable facts across chats. NOTE: zames already has `MEMORY.md`
|
|
550
|
-
|
|
551
|
-
* `AGENTS.md`; add this only if you want a queryable graph.
|
|
563
|
+
+ `AGENTS.md`; add this only if you want a queryable graph.
|
|
552
564
|
|
|
553
565
|
Keep the stack SMALL: every server loads its tool definitions into the model's
|
|
554
566
|
context. Add one when you feel its absence, not before.
|
|
@@ -564,13 +576,14 @@ You can view and change settings without leaving the agent — use the
|
|
|
564
576
|
toggle in place; numbers and strings open an input prompt.
|
|
565
577
|
|
|
566
578
|
```
|
|
567
|
-
/config interactive settings menu
|
|
568
|
-
/config list print all editable settings
|
|
579
|
+
/config interactive settings menu (alias: menu)
|
|
580
|
+
/config list print all editable settings (aliases: show, ls)
|
|
569
581
|
/config get <path> show a setting
|
|
570
582
|
/config set <path> <value> change a setting
|
|
571
|
-
/config reset <path> reset a setting to its default
|
|
572
|
-
/config path show config file paths
|
|
583
|
+
/config reset <path> reset a setting to its default (alias: unset)
|
|
584
|
+
/config path show config file paths (alias: paths)
|
|
573
585
|
/config lang <ru|en> switch interface and agent language
|
|
586
|
+
(aliases: language, язык)
|
|
574
587
|
```
|
|
575
588
|
|
|
576
589
|
Examples:
|
package/llms.txt
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> zames_pro (`zames_pro` on npm, binary `zames`) is a free, open-source
|
|
4
4
|
> terminal coding agent that drives a real web chat LLM — DeepSeek
|
|
5
5
|
> (chat.deepseek.com), Qwen (chat.qwen.ai) or GLM (chat.z.ai) — through a
|
|
6
|
-
>
|
|
7
|
-
> token. It behaves like Claude Code / Codex CLI in the terminal: reads and
|
|
6
|
+
> Playwright browser (DeepSeek and Qwen headless, GLM in a visible window), so
|
|
7
|
+
> it needs NO API key and bills nothing per token. It behaves like Claude Code / Codex CLI in the terminal: reads and
|
|
8
8
|
> edits files, runs shell commands, greps the repo, and commits to git.
|
|
9
9
|
|
|
10
10
|
This file follows the llms.txt convention (https://llmstxt.org): a short,
|
|
@@ -20,6 +20,8 @@ LLM-friendly map of the project. Everything below is plain Markdown.
|
|
|
20
20
|
- **Supported backends:** DeepSeek (`chat.deepseek.com`, default), Qwen
|
|
21
21
|
(`chat.qwen.ai`), GLM (`chat.z.ai`). Live switch with
|
|
22
22
|
`/provider <deepseek|qwen|glm>`; each backend keeps its own browser profile.
|
|
23
|
+
DeepSeek and Qwen run headless; GLM runs with a VISIBLE window by default,
|
|
24
|
+
because z.ai answers every headless send with a slider captcha.
|
|
23
25
|
- **Language / stack:** TypeScript (strict), Node.js >= 22, Playwright,
|
|
24
26
|
Ink/React TUI, MCP client.
|
|
25
27
|
- **License:** MIT. **Not affiliated** with DeepSeek, Alibaba or Zhipu AI.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.82.
|
|
4
|
-
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen
|
|
3
|
+
"version": "2.82.141",
|
|
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": {
|
|
7
7
|
"zames": "dist/index.js"
|
|
@@ -27,6 +27,13 @@ Shrink the failure to the smallest unit that still fails.
|
|
|
27
27
|
remember writing.
|
|
28
28
|
- Check the obvious causes FIRST: wrong file, stale build, cached state,
|
|
29
29
|
different config, an earlier error swallowed by a catch.
|
|
30
|
+
- In zames the evidence is already on disk: the transcript at
|
|
31
|
+
`~/.zames/logs/*.jsonl` (`/transcript` prints the exact path of THIS run),
|
|
32
|
+
the raw answer bodies under `~/.zames/net-log` when started with
|
|
33
|
+
`ZAMES_NET_DEBUG=1` (`npm run dev` sets it), and the per-send telemetry at
|
|
34
|
+
`~/.zames/logs/sends-<provider>.jsonl` (written in dev mode and when
|
|
35
|
+
`browser.sendTelemetry` is on). Read them BEFORE guessing — most "it hangs
|
|
36
|
+
/ it stopped" reports are visible there.
|
|
30
37
|
|
|
31
38
|
## Phase 3 — Hypothesize (one at a time)
|
|
32
39
|
|
|
@@ -32,3 +32,8 @@ RED → GREEN → REFACTOR. The order is the point, not a formality.
|
|
|
32
32
|
CODE was wrong; do not just move the assertion.
|
|
33
33
|
- If the code was written before its test, delete and rewrite it driven by
|
|
34
34
|
the test — otherwise the test is shaped around the implementation.
|
|
35
|
+
- A test that writes a temp dir or file cleans it up itself: in this repo
|
|
36
|
+
always go through `test/tmp-dir.ts` (`makeTmpDir(t, prefix)` /
|
|
37
|
+
`makeTmpDirSync`), which registers the `t.after` cleanup. On WSL a leaked
|
|
38
|
+
`os.tmpdir()` entry lives inside ext4.vhdx and never returns its space to
|
|
39
|
+
the Windows host, so an abandoned temp dir fragments the disk permanently.
|
|
@@ -24,6 +24,11 @@ from the real artifact — not from your intent.
|
|
|
24
24
|
5. **State the evidence** in the final report — the exact command and its
|
|
25
25
|
result — so the operator can reproduce it. "Tests pass" without the
|
|
26
26
|
command is not evidence.
|
|
27
|
+
6. **In zames the report is ONE `respond` call.** A long task may have
|
|
28
|
+
produced several findings along the way; the operator only ever reads the
|
|
29
|
+
LAST `respond` before the run stops, so put the complete evidence there —
|
|
30
|
+
never split it across intermediate messages, and never end a turn on a
|
|
31
|
+
bare text update with work still pending.
|
|
27
32
|
|
|
28
33
|
## The two failure modes this prevents
|
|
29
34
|
|