zames_pro 2.10.9 → 2.12.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/README.md +30 -0
- package/dist/agent-loop.js +34 -1
- package/dist/config.js +2 -2
- package/dist/context.js +270 -0
- package/dist/i18n.js +16 -0
- package/dist/index.js +216 -3
- package/dist/input.js +6 -0
- package/dist/system-prompt.js +51 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,36 @@ zames --version
|
|
|
76
76
|
zames --help
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Project context, skills and memory
|
|
80
|
+
|
|
81
|
+
Like Codex / Claude Code, zames reads project instructions and reusable
|
|
82
|
+
workflows from your repository and from `~/.zames`.
|
|
83
|
+
|
|
84
|
+
- **AGENTS.md** — project instructions. Put one in the repo root (or in any
|
|
85
|
+
parent folder of the working directory). Global instructions live in
|
|
86
|
+
`~/.zames/AGENTS.md` (and `~/.claude/CLAUDE.md`). Run `/init` and the agent
|
|
87
|
+
will explore the project and write an AGENTS.md based on the real build/test
|
|
88
|
+
commands and conventions (use `/init --force` to overwrite an existing file).
|
|
89
|
+
- **MEMORY.md** — durable notes that persist between sessions. The agent
|
|
90
|
+
appends useful facts here; you can edit it by hand.
|
|
91
|
+
- **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
|
|
92
|
+
`description`, optional `allowed-tools`, `user-invokable`) plus any helper
|
|
93
|
+
files. Discovered under `.zames/skills/`, `.claude/skills/`,
|
|
94
|
+
`.agents/skills/`, `skills/`, and `~/.zames/skills/`. The agent reads the
|
|
95
|
+
body only when a task matches the description. List them with `/skills`;
|
|
96
|
+
invoke one with `/<skill-name>`.
|
|
97
|
+
- **Custom commands** — `.md` files under `.zames/commands/` (or
|
|
98
|
+
`.claude/commands/`). They support `$ARGUMENTS` / `{{args}}` placeholders and
|
|
99
|
+
are invoked with `/<command-name>`.
|
|
100
|
+
|
|
101
|
+
Skills and custom commands show up in the «/» completion list and in `/help`.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
/skills list discovered skills
|
|
105
|
+
/memory show AGENTS.md / MEMORY.md in effect
|
|
106
|
+
/init [--force] analyze the project and create AGENTS.md
|
|
107
|
+
```
|
|
108
|
+
|
|
79
109
|
## Configuration
|
|
80
110
|
|
|
81
111
|
Global config: `~/.zames/config.json`
|
package/dist/agent-loop.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
2
|
+
import { loadProjectContext } from './context.js';
|
|
2
3
|
import { getGitContext, formatGitContext } from './gitTools.js';
|
|
3
4
|
import { parseXmlToolCalls } from './xml-toolcall.js';
|
|
4
5
|
import { translate } from './i18n.js';
|
|
5
6
|
import { normText } from './browser.js';
|
|
6
|
-
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations =
|
|
7
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 200, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', }) {
|
|
7
8
|
// UI callbacks must NEVER break the agent loop. A rendering error (a huge
|
|
8
9
|
// tool result, a broken markdown frame, a closed terminal) used to throw
|
|
9
10
|
// out of the loop right after a tool call — the session looked "stopped
|
|
@@ -59,15 +60,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
59
60
|
catch (e) {
|
|
60
61
|
gitText = `(git context error: ${e.message})`;
|
|
61
62
|
}
|
|
63
|
+
let context = null;
|
|
64
|
+
try {
|
|
65
|
+
context = await loadProjectContext(workdir);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
context = null;
|
|
69
|
+
}
|
|
62
70
|
const systemPrompt = buildSystemPrompt({
|
|
63
71
|
workdir,
|
|
64
72
|
tools,
|
|
65
73
|
gitContext: gitText,
|
|
66
74
|
locale,
|
|
75
|
+
context,
|
|
67
76
|
});
|
|
68
77
|
transcript?.log('system_prompt', {
|
|
69
78
|
length: systemPrompt.length,
|
|
70
79
|
gitContext: gitText,
|
|
80
|
+
agents: context?.agents.map((f) => f.path) ?? [],
|
|
81
|
+
memory: context?.memory.map((f) => f.path) ?? [],
|
|
82
|
+
skills: context?.skills.map((s) => s.name) ?? [],
|
|
83
|
+
commands: context?.commands.map((c) => c.name) ?? [],
|
|
71
84
|
});
|
|
72
85
|
safeThinking();
|
|
73
86
|
// system-prompt is an agent send: throttled (agent: true).
|
|
@@ -919,6 +932,26 @@ function parseToolCallPermissive(text) {
|
|
|
919
932
|
if (!toolMatch)
|
|
920
933
|
return null;
|
|
921
934
|
const tool = toolMatch[1];
|
|
935
|
+
// The model occasionally puts the argument keys INLINE with "tool" —
|
|
936
|
+
// {"tool": "Bash", "command": "..."} — with no "args" wrapper at all.
|
|
937
|
+
// The strict parser rejects it (no obj.args), and the permissive parser
|
|
938
|
+
// used to bail out too (indexOf('"args"') === -1). Such a call was
|
|
939
|
+
// reported as malformed and re-asked up to MAX_MALFORMED_RETRIES times;
|
|
940
|
+
// after the budget ran out the run stopped with the model's text as the
|
|
941
|
+
// final answer. Flatten the inline keys into args when "args" is absent.
|
|
942
|
+
if (text.indexOf('"args"') === -1) {
|
|
943
|
+
const braceIdx = text.indexOf('{');
|
|
944
|
+
if (braceIdx === -1)
|
|
945
|
+
return null;
|
|
946
|
+
const endBrace = findMatching(text, braceIdx, '{', '}');
|
|
947
|
+
const objText = endBrace === -1 ? text.slice(braceIdx) : text.slice(braceIdx, endBrace + 1);
|
|
948
|
+
const inlineArgs = parseArgsPermissive(objText) || parseArgsGreedy(objText);
|
|
949
|
+
if (!inlineArgs)
|
|
950
|
+
return null;
|
|
951
|
+
delete inlineArgs.tool;
|
|
952
|
+
// The value may be a string ("command": "...") or a number/bool.
|
|
953
|
+
return { tool, args: inlineArgs };
|
|
954
|
+
}
|
|
922
955
|
const argsIdx = text.indexOf('"args"');
|
|
923
956
|
if (argsIdx === -1)
|
|
924
957
|
return null;
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ const ZAMES_HOME = path.join(os.homedir(), '.zames');
|
|
|
6
6
|
const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
|
|
7
7
|
const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json');
|
|
8
8
|
export const DEFAULTS = {
|
|
9
|
-
maxIterations:
|
|
9
|
+
maxIterations: 200,
|
|
10
10
|
headless: false,
|
|
11
11
|
debug: false,
|
|
12
12
|
browserChannel: null,
|
|
@@ -77,7 +77,7 @@ function deepMerge(target, source) {
|
|
|
77
77
|
}
|
|
78
78
|
export const CONFIG_SCHEMA = [
|
|
79
79
|
{ path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
|
|
80
|
-
{ path: 'maxIterations', type: 'number', min: 1, max:
|
|
80
|
+
{ path: 'maxIterations', type: 'number', min: 1, max: 100000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
|
|
81
81
|
{ path: 'headless', type: 'boolean', labelKey: 'cfg.f.headless', groupKey: 'cfg.group.agent' },
|
|
82
82
|
{ path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
|
|
83
83
|
{ path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const MAX_FILE_BASE = 60000;
|
|
5
|
+
const MAX_TOTAL = 240000;
|
|
6
|
+
const CRLF_RE = /\r\n/g;
|
|
7
|
+
const NL = String.fromCharCode(10);
|
|
8
|
+
function clipIf(name, content, budget) {
|
|
9
|
+
const cleaned = content.replace(CRLF_RE, NL).trim();
|
|
10
|
+
if (cleaned.length <= budget)
|
|
11
|
+
return cleaned;
|
|
12
|
+
const keep = Math.max(0, budget - 120);
|
|
13
|
+
return cleaned.slice(0, keep) + NL + NL + '[... ' + name + ' truncated (' + (cleaned.length - keep) + ' chars omitted) ...]';
|
|
14
|
+
}
|
|
15
|
+
async function readIfFile(p, budget) {
|
|
16
|
+
try {
|
|
17
|
+
const st = await fs.stat(p);
|
|
18
|
+
if (!st.isFile())
|
|
19
|
+
return null;
|
|
20
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
21
|
+
const content = clipIf(path.basename(p), raw, budget);
|
|
22
|
+
if (!content)
|
|
23
|
+
return null;
|
|
24
|
+
return { path: p, content };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const AGENT_NAMES = ['AGENTS.md', 'agents.md', 'Agents.md'];
|
|
31
|
+
const MEMORY_NAMES = ['MEMORY.md', 'memory.md', 'Memory.md'];
|
|
32
|
+
async function findNamedFile(dir, names) {
|
|
33
|
+
for (const n of names) {
|
|
34
|
+
const p = path.join(dir, n);
|
|
35
|
+
const st = await fs.stat(p).catch(() => null);
|
|
36
|
+
if (st && st.isFile())
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function dirChainFromRoot(from) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let cur = path.resolve(from);
|
|
44
|
+
const root = path.parse(cur).root;
|
|
45
|
+
while (true) {
|
|
46
|
+
out.unshift(cur);
|
|
47
|
+
if (cur === root)
|
|
48
|
+
break;
|
|
49
|
+
const parent = path.dirname(cur);
|
|
50
|
+
if (parent === cur)
|
|
51
|
+
break;
|
|
52
|
+
cur = parent;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
const PROJECT_SKILL_DIRS = ['.zames/skills', '.claude/skills', '.agents/skills', 'skills'];
|
|
57
|
+
function parseFrontmatter(raw) {
|
|
58
|
+
const norm = raw.replace(CRLF_RE, NL);
|
|
59
|
+
if (!norm.startsWith('---'))
|
|
60
|
+
return {};
|
|
61
|
+
const end = norm.indexOf(NL + '---', 3);
|
|
62
|
+
if (end === -1)
|
|
63
|
+
return {};
|
|
64
|
+
const block = norm.slice(3, end);
|
|
65
|
+
const out = {};
|
|
66
|
+
let lastKey = null;
|
|
67
|
+
for (const rawLine of block.split(NL)) {
|
|
68
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
69
|
+
if (!line || /^\s*#/.test(line))
|
|
70
|
+
continue;
|
|
71
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
72
|
+
if (m && !/^\s/.test(line)) {
|
|
73
|
+
const key = m[1];
|
|
74
|
+
let val = (m[2] || '').trim();
|
|
75
|
+
if (val === '|' || val === '>') {
|
|
76
|
+
out[key] = '';
|
|
77
|
+
lastKey = key;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
81
|
+
val = val.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
out[key] = val;
|
|
84
|
+
lastKey = key;
|
|
85
|
+
}
|
|
86
|
+
else if (lastKey && /^\s+/.test(line)) {
|
|
87
|
+
out[lastKey] = ((out[lastKey] || '') + ' ' + line.trim()).trim();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const allowedTools = typeof out['allowed-tools'] === 'string'
|
|
91
|
+
? out['allowed-tools'].split(/,/).map((s) => s.trim()).filter(Boolean)
|
|
92
|
+
: undefined;
|
|
93
|
+
const userInvokable = typeof out['user-invokable'] === 'string'
|
|
94
|
+
? out['user-invokable'].toLowerCase() !== 'false'
|
|
95
|
+
: undefined;
|
|
96
|
+
return { name: out.name, description: out.description, allowedTools, userInvokable };
|
|
97
|
+
}
|
|
98
|
+
export function skillBody(raw) {
|
|
99
|
+
const norm = raw.replace(CRLF_RE, NL);
|
|
100
|
+
if (!norm.startsWith('---'))
|
|
101
|
+
return norm.trim();
|
|
102
|
+
const end = norm.indexOf(NL + '---', 3);
|
|
103
|
+
if (end === -1)
|
|
104
|
+
return norm.trim();
|
|
105
|
+
return norm.slice(norm.indexOf(NL, end + 1) + 1).trim();
|
|
106
|
+
}
|
|
107
|
+
async function findSkillFiles(root, depth = 3) {
|
|
108
|
+
const out = [];
|
|
109
|
+
const walk = async (dir, level) => {
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
const full = path.join(dir, e.name);
|
|
119
|
+
if (e.isFile() && e.name === 'SKILL.md')
|
|
120
|
+
out.push(full);
|
|
121
|
+
else if (e.isDirectory() && level < depth)
|
|
122
|
+
await walk(full, level + 1);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
await walk(root, 1);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
export async function loadSkills(workdir) {
|
|
129
|
+
const found = [];
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const roots = [];
|
|
132
|
+
for (const rel of PROJECT_SKILL_DIRS) {
|
|
133
|
+
roots.push({ dir: path.join(workdir, rel), source: 'project' });
|
|
134
|
+
}
|
|
135
|
+
roots.push({ dir: path.join(os.homedir(), '.zames', 'skills'), source: 'global' });
|
|
136
|
+
roots.push({ dir: path.join(os.homedir(), '.claude', 'skills'), source: 'global' });
|
|
137
|
+
for (const root of roots) {
|
|
138
|
+
const files = await findSkillFiles(root.dir);
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const raw = await fs.readFile(f, 'utf-8').catch(() => null);
|
|
141
|
+
if (raw === null)
|
|
142
|
+
continue;
|
|
143
|
+
const fm = parseFrontmatter(raw);
|
|
144
|
+
const dir = path.dirname(f);
|
|
145
|
+
const name = (fm.name || path.basename(dir)).trim();
|
|
146
|
+
if (!name)
|
|
147
|
+
continue;
|
|
148
|
+
const key = name.toLowerCase();
|
|
149
|
+
if (seen.has(key))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(key);
|
|
152
|
+
const description = (fm.description || '').replace(/\s+/g, ' ').trim();
|
|
153
|
+
found.push({ name, description, path: f, dir, allowedTools: fm.allowedTools, source: root.source, userInvokable: fm.userInvokable !== false });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
found.sort((a, b) => a.name.localeCompare(b.name));
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
const PROJECT_COMMAND_DIRS = ['.zames/commands', '.claude/commands', '.agents/commands'];
|
|
160
|
+
async function loadCommandsFromDir(dir) {
|
|
161
|
+
const out = [];
|
|
162
|
+
let entries;
|
|
163
|
+
try {
|
|
164
|
+
entries = await fs.readdir(dir);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
for (const e of entries) {
|
|
170
|
+
if (!/\.md$/i.test(e))
|
|
171
|
+
continue;
|
|
172
|
+
const full = path.join(dir, e);
|
|
173
|
+
const st = await fs.stat(full).catch(() => null);
|
|
174
|
+
if (!st || !st.isFile())
|
|
175
|
+
continue;
|
|
176
|
+
const raw = await fs.readFile(full, 'utf-8').catch(() => null);
|
|
177
|
+
if (raw === null)
|
|
178
|
+
continue;
|
|
179
|
+
const fm = parseFrontmatter(raw);
|
|
180
|
+
const name = path.basename(e, '.md').trim();
|
|
181
|
+
if (!name)
|
|
182
|
+
continue;
|
|
183
|
+
out.push({ name, description: (fm.description || '').replace(/\s+/g, ' ').trim(), path: full, body: skillBody(raw) });
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
export async function loadCommands(workdir) {
|
|
188
|
+
const out = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const dirs = PROJECT_COMMAND_DIRS.map((d) => path.join(workdir, d));
|
|
191
|
+
dirs.push(path.join(os.homedir(), '.zames', 'commands'));
|
|
192
|
+
dirs.push(path.join(os.homedir(), '.claude', 'commands'));
|
|
193
|
+
for (const dir of dirs) {
|
|
194
|
+
for (const c of await loadCommandsFromDir(dir)) {
|
|
195
|
+
const key = c.name.toLowerCase();
|
|
196
|
+
if (seen.has(key))
|
|
197
|
+
continue;
|
|
198
|
+
seen.add(key);
|
|
199
|
+
out.push(c);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
async function loadAgentsChain(workdir) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
for (const p of [path.join(os.homedir(), '.zames', 'AGENTS.md'), path.join(os.homedir(), '.claude', 'CLAUDE.md')]) {
|
|
209
|
+
const f = await readIfFile(p, MAX_FILE_BASE);
|
|
210
|
+
if (f && !seen.has(f.path)) {
|
|
211
|
+
seen.add(f.path);
|
|
212
|
+
out.push(f);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const chain = dirChainFromRoot(workdir);
|
|
216
|
+
for (const dir of chain) {
|
|
217
|
+
const p = await findNamedFile(dir, AGENT_NAMES);
|
|
218
|
+
if (!p || seen.has(p))
|
|
219
|
+
continue;
|
|
220
|
+
const f = await readIfFile(p, MAX_FILE_BASE);
|
|
221
|
+
if (f) {
|
|
222
|
+
seen.add(p);
|
|
223
|
+
out.push(f);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
async function loadMemoryChain(workdir) {
|
|
229
|
+
const out = [];
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
for (const p of [path.join(os.homedir(), '.zames', 'MEMORY.md'), path.join(os.homedir(), '.claude', 'MEMORY.md')]) {
|
|
232
|
+
const f = await readIfFile(p, MAX_FILE_BASE);
|
|
233
|
+
if (f && !seen.has(f.path)) {
|
|
234
|
+
seen.add(f.path);
|
|
235
|
+
out.push(f);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const chain = dirChainFromRoot(workdir);
|
|
239
|
+
for (const dir of chain) {
|
|
240
|
+
const p = await findNamedFile(dir, MEMORY_NAMES);
|
|
241
|
+
if (!p || seen.has(p))
|
|
242
|
+
continue;
|
|
243
|
+
const f = await readIfFile(p, MAX_FILE_BASE);
|
|
244
|
+
if (f) {
|
|
245
|
+
seen.add(p);
|
|
246
|
+
out.push(f);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
export async function loadProjectContext(workdir) {
|
|
252
|
+
const [agents, memory, skills, commands] = await Promise.all([
|
|
253
|
+
loadAgentsChain(workdir).catch(() => []),
|
|
254
|
+
loadMemoryChain(workdir).catch(() => []),
|
|
255
|
+
loadSkills(workdir).catch(() => []),
|
|
256
|
+
loadCommands(workdir).catch(() => []),
|
|
257
|
+
]);
|
|
258
|
+
let used = 0;
|
|
259
|
+
const withinBudget = (files) => {
|
|
260
|
+
const kept = [];
|
|
261
|
+
for (const f of files) {
|
|
262
|
+
if (used + f.content.length > MAX_TOTAL)
|
|
263
|
+
break;
|
|
264
|
+
used += f.content.length;
|
|
265
|
+
kept.push(f);
|
|
266
|
+
}
|
|
267
|
+
return kept;
|
|
268
|
+
};
|
|
269
|
+
return { agents: withinBudget(agents), memory: withinBudget(memory), skills, commands };
|
|
270
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -83,6 +83,22 @@ const CATALOG = {
|
|
|
83
83
|
'help.cmd.debug_dom': { ru: '/debug-dom сохранить HTML страницы (для отладки)', en: '/debug-dom dump page HTML (debug)' },
|
|
84
84
|
'help.cmd.help': { ru: '/help, help справка', en: '/help, help this help' },
|
|
85
85
|
'help.cmd.exit': { ru: '/exit, /quit, exit выход', en: '/exit, /quit, exit exit' },
|
|
86
|
+
'skills.none': { ru: 'Навыки не найдены. Добавьте SKILL.md в .zames/skills/<имя>/ или ~/.zames/skills/.', en: 'No skills found. Add SKILL.md under .zames/skills/<name>/ or ~/.zames/skills/.' },
|
|
87
|
+
'skills.title': { ru: 'Найдено навыков: {n}', en: 'Skills found: {n}' },
|
|
88
|
+
'memory.agents': { ru: 'Инструкции (AGENTS.md):', en: 'Instructions (AGENTS.md):' },
|
|
89
|
+
'memory.memory': { ru: 'Память (MEMORY.md):', en: 'Memory (MEMORY.md):' },
|
|
90
|
+
'init.exists': { ru: 'AGENTS.md уже существует: {v}', en: 'AGENTS.md already exists: {v}' },
|
|
91
|
+
'init.created': { ru: 'Создан {v}', en: 'Created {v}' },
|
|
92
|
+
'init.analyzing': { ru: 'Изучаю проект и готовлю AGENTS.md...', en: 'Analyzing the project and preparing AGENTS.md...' },
|
|
93
|
+
'init.done': { ru: 'AGENTS.md создан агентом: {v}', en: 'AGENTS.md created by the agent: {v}' },
|
|
94
|
+
'init.failed': { ru: 'Не удалось создать AGENTS.md автоматически, записан базовый шаблон.', en: 'Could not create AGENTS.md automatically, wrote a basic template.' },
|
|
95
|
+
'init.overwrite': { ru: 'AGENTS.md уже существует: {v} (используйте /init --force для перезаписи)', en: 'AGENTS.md already exists: {v} (use /init --force to overwrite)' },
|
|
96
|
+
'help.cmd.skill': { ru: 'навык (SKILL.md)', en: 'skill (SKILL.md)' },
|
|
97
|
+
'help.cmd.custom': { ru: 'пользовательская команда', en: 'custom command' },
|
|
98
|
+
'help.cmd.skills': { ru: '/skills список найденных навыков', en: '/skills list discovered skills' },
|
|
99
|
+
'help.cmd.memory': { ru: '/memory показать MEMORY.md и AGENTS.md', en: '/memory show MEMORY.md and AGENTS.md' },
|
|
100
|
+
'help.cmd.init': { ru: '/init [--force] изучить проект и создать AGENTS.md', en: '/init [--force] analyze the project and create AGENTS.md' },
|
|
101
|
+
'help.skills': { ru: 'Навыки и свои команды:', en: 'Skills and custom commands:' },
|
|
86
102
|
'help.self_review': { ru: 'Самообзор (отладка агента):', en: 'Self-review (agent debugging):' },
|
|
87
103
|
'help.self.review': { ru: '/self-review [фокус] снять снапшот src/ и запустить ревью', en: '/self-review [focus] snapshot src/ and run review' },
|
|
88
104
|
'help.self.fix': { ru: '/self-fix <name> [фокус] вернуться в существующий снапшот и продолжить', en: '/self-fix <name> [focus] enter an existing snapshot and continue' },
|
package/dist/index.js
CHANGED
|
@@ -226,6 +226,7 @@ ${theme.bold(t('help.self_review'))}
|
|
|
226
226
|
${t('help.self.list')}
|
|
227
227
|
${t('help.self.diff')}
|
|
228
228
|
${t('help.self.apply')}
|
|
229
|
+
${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + d.name.padEnd(24) + ' ' + d.description).join('\n') : ''}
|
|
229
230
|
|
|
230
231
|
${theme.bold(t('help.files'))}
|
|
231
232
|
${t('help.files.logs')} ${config.transcript.dir}
|
|
@@ -261,6 +262,9 @@ const SLASH_COMMANDS = [
|
|
|
261
262
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
262
263
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
263
264
|
{ name: '/config', key: 'help.cmd.config' },
|
|
265
|
+
{ name: '/skills', key: 'help.cmd.skills' },
|
|
266
|
+
{ name: '/memory', key: 'help.cmd.memory' },
|
|
267
|
+
{ name: '/init', key: 'help.cmd.init' },
|
|
264
268
|
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
265
269
|
{ name: '/self-review', key: 'help.self.review' },
|
|
266
270
|
{ name: '/self-fix', key: 'help.self.fix' },
|
|
@@ -271,12 +275,82 @@ const SLASH_COMMANDS = [
|
|
|
271
275
|
{ name: '/exit', key: 'help.cmd.exit' },
|
|
272
276
|
{ name: '/quit', key: 'help.cmd.exit' },
|
|
273
277
|
];
|
|
278
|
+
// Dynamically discovered skills and custom commands for the current workdir.
|
|
279
|
+
// Filled in by refreshDynamicCommands() and shown in the «/» hint list and
|
|
280
|
+
// help. Skills are surfaced as /skill-name entries (user-invokable only).
|
|
281
|
+
let dynamicCommands = [];
|
|
282
|
+
async function refreshDynamicCommands(workdir) {
|
|
283
|
+
const out = [];
|
|
284
|
+
try {
|
|
285
|
+
const { loadSkills, loadCommands } = await import('./context.js');
|
|
286
|
+
const skills = await loadSkills(workdir);
|
|
287
|
+
for (const s of skills) {
|
|
288
|
+
if (!s.userInvokable)
|
|
289
|
+
continue;
|
|
290
|
+
out.push({
|
|
291
|
+
name: '/' + s.name,
|
|
292
|
+
description: s.description || t('help.cmd.skill'),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const cmds = await loadCommands(workdir);
|
|
296
|
+
for (const c of cmds) {
|
|
297
|
+
out.push({
|
|
298
|
+
name: '/' + c.name,
|
|
299
|
+
description: c.description || t('help.cmd.custom'),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// best-effort: a broken skill/command must not break the CLI
|
|
305
|
+
}
|
|
306
|
+
dynamicCommands = out;
|
|
307
|
+
}
|
|
308
|
+
// Expand a slash target (skill or custom command) into a task string.
|
|
309
|
+
// Returns null when the name is neither a skill nor a custom command.
|
|
310
|
+
async function expandSlashTarget(workdir, name, rest) {
|
|
311
|
+
try {
|
|
312
|
+
const { loadSkills, loadCommands, skillBody } = await import('./context.js');
|
|
313
|
+
const commands = await loadCommands(workdir);
|
|
314
|
+
const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
315
|
+
if (cmd) {
|
|
316
|
+
let body = cmd.body.replace(/\{\{args\}\}/g, rest);
|
|
317
|
+
body = body.replace(/\$ARGUMENTS/g, rest);
|
|
318
|
+
return body.trim() || rest;
|
|
319
|
+
}
|
|
320
|
+
const skills = await loadSkills(workdir);
|
|
321
|
+
const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
|
|
322
|
+
if (skill) {
|
|
323
|
+
let body = '';
|
|
324
|
+
try {
|
|
325
|
+
const raw = await fs.readFile(skill.path, 'utf-8');
|
|
326
|
+
body = skillBody(raw);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
body = '';
|
|
330
|
+
}
|
|
331
|
+
const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
|
|
332
|
+
const extra = rest ? '\n\nAdditional instructions from the operator: ' + rest : '';
|
|
333
|
+
return header + '\n\n' + body + extra;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// fall through: treated as an unknown command
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
274
341
|
// Slash-command descriptions in the current language (for LineEditor hints).
|
|
275
342
|
function buildSlashCommands() {
|
|
276
|
-
|
|
343
|
+
const base = SLASH_COMMANDS.map((c) => ({
|
|
277
344
|
name: c.name,
|
|
278
345
|
description: t(c.key),
|
|
279
346
|
}));
|
|
347
|
+
const known = new Set(base.map((c) => c.name.toLowerCase()));
|
|
348
|
+
for (const d of dynamicCommands) {
|
|
349
|
+
if (known.has(d.name.toLowerCase()))
|
|
350
|
+
continue;
|
|
351
|
+
base.push(d);
|
|
352
|
+
}
|
|
353
|
+
return base;
|
|
280
354
|
}
|
|
281
355
|
// We put the agent's temporary files (one-off scripts, etc.) in
|
|
282
356
|
// <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
|
|
@@ -973,6 +1047,7 @@ async function main() {
|
|
|
973
1047
|
}
|
|
974
1048
|
return theme.prompt('❯ ') + tail + theme.dim(' › ');
|
|
975
1049
|
};
|
|
1050
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
976
1051
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
977
1052
|
const ed = new LineEditor({
|
|
978
1053
|
prompt: buildPrompt(),
|
|
@@ -1677,6 +1752,127 @@ async function main() {
|
|
|
1677
1752
|
console.log(theme.system(currentWorkdir));
|
|
1678
1753
|
continue;
|
|
1679
1754
|
}
|
|
1755
|
+
if (lower === '/skills') {
|
|
1756
|
+
const { loadSkills } = await import('./context.js');
|
|
1757
|
+
const skills = await loadSkills(currentWorkdir);
|
|
1758
|
+
if (!skills.length) {
|
|
1759
|
+
console.log(theme.dim(t('skills.none')));
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
console.log(theme.system(t('skills.title', { n: String(skills.length) })));
|
|
1763
|
+
for (const s of skills) {
|
|
1764
|
+
console.log(' ' +
|
|
1765
|
+
theme.user('/' + s.name) +
|
|
1766
|
+
theme.dim(' [' + s.source + '] ') +
|
|
1767
|
+
(s.description || theme.dim(t('common.none'))));
|
|
1768
|
+
console.log(theme.dim(' ' + s.path));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (lower === '/memory') {
|
|
1774
|
+
const { loadProjectContext } = await import('./context.js');
|
|
1775
|
+
const ctx = await loadProjectContext(currentWorkdir);
|
|
1776
|
+
const show = (title, files) => {
|
|
1777
|
+
console.log(theme.system(title));
|
|
1778
|
+
if (!files.length) {
|
|
1779
|
+
console.log(theme.dim(' ' + t('common.none')));
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
for (const f of files)
|
|
1783
|
+
console.log(' ' + f.path);
|
|
1784
|
+
};
|
|
1785
|
+
show(t('memory.agents'), ctx.agents);
|
|
1786
|
+
show(t('memory.memory'), ctx.memory);
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (lower === '/init' || lower.startsWith('/init ')) {
|
|
1790
|
+
const force = lower.includes('--force');
|
|
1791
|
+
const target = path.join(currentWorkdir, 'AGENTS.md');
|
|
1792
|
+
const exists = await fs.stat(target).catch(() => null);
|
|
1793
|
+
if (exists && !force) {
|
|
1794
|
+
console.log(theme.warn(t('init.overwrite', { v: target })));
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
// Like Codex: let the agent explore the project and write AGENTS.md via
|
|
1798
|
+
// the Write tool, so the file reflects the real build/test commands and
|
|
1799
|
+
// conventions instead of a static template.
|
|
1800
|
+
const initTask = 'The user ran /init. Analyze this repository and create an AGENTS.md ' +
|
|
1801
|
+
'file at the project root that onboards future coding agents. Explore ' +
|
|
1802
|
+
'the project first (read the README, package.json and other manifests, ' +
|
|
1803
|
+
'configs, CI, and a sample of source files). Then write AGENTS.md with: ' +
|
|
1804
|
+
'an overview of what the project is; build/typecheck/test/lint commands ' +
|
|
1805
|
+
'(use the real scripts you find); code style and conventions; the repo ' +
|
|
1806
|
+
'layout; and any gotchas or rules an agent must follow. Keep it concise ' +
|
|
1807
|
+
'and factual, based only on what you find. Write the file with the Write ' +
|
|
1808
|
+
'tool, then reply via respond with a one-line summary.';
|
|
1809
|
+
const initTools = mod.createTools(currentWorkdir, { undo });
|
|
1810
|
+
if (editor)
|
|
1811
|
+
editor.busy = true;
|
|
1812
|
+
console.log(theme.system(t('init.analyzing')));
|
|
1813
|
+
try {
|
|
1814
|
+
await mod.runAgentLoop({
|
|
1815
|
+
browser,
|
|
1816
|
+
tools: initTools,
|
|
1817
|
+
task: initTask,
|
|
1818
|
+
workdir: currentWorkdir,
|
|
1819
|
+
maxIterations: maxIter,
|
|
1820
|
+
freshChat: freshChatNext,
|
|
1821
|
+
sendSystemPrompt: sendSystemPromptNext,
|
|
1822
|
+
transcript,
|
|
1823
|
+
onThinking: () => { },
|
|
1824
|
+
onToolCall: (name, toolArgs) => {
|
|
1825
|
+
if (editor)
|
|
1826
|
+
editor.toolCall(name, toolArgs);
|
|
1827
|
+
},
|
|
1828
|
+
onToolResult: (r) => {
|
|
1829
|
+
if (editor)
|
|
1830
|
+
editor.toolResult(r);
|
|
1831
|
+
},
|
|
1832
|
+
onAssistantMessage: (m) => {
|
|
1833
|
+
if (editor)
|
|
1834
|
+
editor.assistant(m);
|
|
1835
|
+
},
|
|
1836
|
+
onWarning: (m) => {
|
|
1837
|
+
if (editor)
|
|
1838
|
+
editor.warning(m);
|
|
1839
|
+
},
|
|
1840
|
+
locale: currentLocale,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
catch (e) {
|
|
1844
|
+
console.error(theme.error(t('init.failed')), e.message);
|
|
1845
|
+
}
|
|
1846
|
+
finally {
|
|
1847
|
+
if (editor)
|
|
1848
|
+
editor.busy = false;
|
|
1849
|
+
}
|
|
1850
|
+
freshChatNext = false;
|
|
1851
|
+
sendSystemPromptNext = false;
|
|
1852
|
+
if (!currentChatId) {
|
|
1853
|
+
currentChatId = await browser.getCurrentChatId();
|
|
1854
|
+
}
|
|
1855
|
+
saveLastChat(currentChatId, currentWorkdir);
|
|
1856
|
+
const created = await fs.stat(target).catch(() => null);
|
|
1857
|
+
if (created) {
|
|
1858
|
+
console.log(theme.assistant(t('init.done', { v: target })));
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
const stub = '# AGENTS.md\n\n' +
|
|
1862
|
+
'Project instructions for the coding agent. Describe the build/test commands, ' +
|
|
1863
|
+
'conventions, and any rules the agent must follow in this repository.\n\n' +
|
|
1864
|
+
'## Commands\n\n' +
|
|
1865
|
+
'- build: `...`\n' +
|
|
1866
|
+
'- test: `...`\n' +
|
|
1867
|
+
'- lint: `...`\n\n' +
|
|
1868
|
+
'## Conventions\n\n' +
|
|
1869
|
+
'- ...\n';
|
|
1870
|
+
await fs.writeFile(target, stub, 'utf-8');
|
|
1871
|
+
console.log(theme.warn(t('init.failed')));
|
|
1872
|
+
console.log(theme.assistant(t('init.created', { v: target })));
|
|
1873
|
+
}
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1680
1876
|
if (lower === '/reload') {
|
|
1681
1877
|
console.log(theme.system(t('msg.reload_start')));
|
|
1682
1878
|
try {
|
|
@@ -1693,6 +1889,10 @@ async function main() {
|
|
|
1693
1889
|
catch (e) {
|
|
1694
1890
|
console.error(theme.error(t('msg.reload_error')), e.message);
|
|
1695
1891
|
}
|
|
1892
|
+
await autoReload();
|
|
1893
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
1894
|
+
if (editor)
|
|
1895
|
+
editor.setCommands(buildSlashCommands());
|
|
1696
1896
|
continue;
|
|
1697
1897
|
}
|
|
1698
1898
|
if (lower === '/status') {
|
|
@@ -1803,6 +2003,9 @@ async function main() {
|
|
|
1803
2003
|
currentWorkdir = newDir;
|
|
1804
2004
|
freshChatNext = true;
|
|
1805
2005
|
sendSystemPromptNext = true;
|
|
2006
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
2007
|
+
if (editor)
|
|
2008
|
+
editor.setCommands(buildSlashCommands());
|
|
1806
2009
|
console.log(theme.system(t('cd.changed', { v: newDir })));
|
|
1807
2010
|
}
|
|
1808
2011
|
catch (e) {
|
|
@@ -1810,19 +2013,29 @@ async function main() {
|
|
|
1810
2013
|
}
|
|
1811
2014
|
continue;
|
|
1812
2015
|
}
|
|
2016
|
+
// Custom command or skill invoked as a slash command? Expand it into a
|
|
2017
|
+
// task. Skills are instructions the agent follows; custom commands are
|
|
2018
|
+
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
2019
|
+
let expandedTask = null;
|
|
1813
2020
|
if (lower.startsWith('/')) {
|
|
2021
|
+
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
2022
|
+
const rest = trimmed.slice(1 + name.length).trim();
|
|
2023
|
+
expandedTask = await expandSlashTarget(currentWorkdir, name, rest);
|
|
2024
|
+
}
|
|
2025
|
+
if (lower.startsWith('/') && expandedTask === null) {
|
|
1814
2026
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
1815
2027
|
continue;
|
|
1816
2028
|
}
|
|
1817
2029
|
// ---- Regular task (including in review mode) ----
|
|
2030
|
+
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
1818
2031
|
// Dev mode: pick up fresh logic modules before the task.
|
|
1819
2032
|
await autoReload();
|
|
1820
|
-
transcript.log('user_task', { task:
|
|
2033
|
+
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
1821
2034
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
1822
2035
|
if (editor)
|
|
1823
2036
|
editor.busy = true;
|
|
1824
2037
|
try {
|
|
1825
|
-
await runTask(browser, tools,
|
|
2038
|
+
await runTask(browser, tools, taskText, currentWorkdir, {
|
|
1826
2039
|
transcript,
|
|
1827
2040
|
freshChat: freshChatNext,
|
|
1828
2041
|
sendSystemPrompt: sendSystemPromptNext,
|
package/dist/input.js
CHANGED
|
@@ -351,6 +351,12 @@ export class LineEditor {
|
|
|
351
351
|
}
|
|
352
352
|
unlock() {
|
|
353
353
|
this.locked = false;
|
|
354
|
+
// A lock may have set the status hint ("operation in progress, input is
|
|
355
|
+
// temporarily locked"). If nothing else replaced it (no spinner, no
|
|
356
|
+
// pending text), clear it here — otherwise the hint stays on screen
|
|
357
|
+
// forever after a fast operation like /new.
|
|
358
|
+
if (this.statusText)
|
|
359
|
+
this.setStatus('');
|
|
354
360
|
}
|
|
355
361
|
setPrompt(str) {
|
|
356
362
|
this.promptStr = str;
|
package/dist/system-prompt.js
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import { translate } from './i18n.js';
|
|
2
|
-
|
|
2
|
+
/** Render the AGENTS.md / MEMORY / skills / commands blocks of the prompt. */
|
|
3
|
+
export function renderContextSection(context) {
|
|
4
|
+
if (!context)
|
|
5
|
+
return '';
|
|
6
|
+
let out = '';
|
|
7
|
+
for (const f of context.agents) {
|
|
8
|
+
out +=
|
|
9
|
+
'\n## Project instructions (' + f.path + ')\n\n' +
|
|
10
|
+
'The following come from ' + f.path + '. Follow them for this project unless the operator explicitly overrides them.\n\n' +
|
|
11
|
+
f.content +
|
|
12
|
+
'\n';
|
|
13
|
+
}
|
|
14
|
+
for (const f of context.memory) {
|
|
15
|
+
out +=
|
|
16
|
+
'\n## Memory (' + f.path + ')\n\n' +
|
|
17
|
+
'Durable notes accumulated across sessions. Treat as background knowledge; update this file when you learn something worth remembering.\n\n' +
|
|
18
|
+
f.content +
|
|
19
|
+
'\n';
|
|
20
|
+
}
|
|
21
|
+
if (context.memory.length) {
|
|
22
|
+
out +=
|
|
23
|
+
'\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
|
|
24
|
+
'conventions, gotchas), append a short bullet to ' + context.memory[0].path + ' via Edit/Write so it survives ' +
|
|
25
|
+
'into future sessions. Keep entries concise.\n';
|
|
26
|
+
}
|
|
27
|
+
if (context.skills.length) {
|
|
28
|
+
const lines = context.skills.map((s) => {
|
|
29
|
+
const tools = s.allowedTools && s.allowedTools.length
|
|
30
|
+
? ' [tools: ' + s.allowedTools.join(', ') + ']'
|
|
31
|
+
: '';
|
|
32
|
+
return '- ' + s.name + tools + ': ' + (s.description || '(no description)') + '\n file: ' + s.path;
|
|
33
|
+
});
|
|
34
|
+
out +=
|
|
35
|
+
'\n## Skills\n\n' +
|
|
36
|
+
'Specialized workflows available as SKILL.md files. When a task matches a skill description, read its file and follow its instructions. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
|
|
37
|
+
lines.join('\n') +
|
|
38
|
+
'\n';
|
|
39
|
+
}
|
|
40
|
+
if (context.commands.length) {
|
|
41
|
+
const lines = context.commands.map((c) => '- /' + c.name + ': ' + (c.description || '(no description)'));
|
|
42
|
+
out +=
|
|
43
|
+
'\n## Custom commands\n\n' +
|
|
44
|
+
'Reusable prompts the operator can invoke with a slash command. If the operator runs one, its body arrives as the task text.\n\n' +
|
|
45
|
+
lines.join('\n') +
|
|
46
|
+
'\n';
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, }) {
|
|
3
51
|
const t = translate(locale);
|
|
4
52
|
const toolDescriptions = tools
|
|
5
53
|
.map((t2) => `### ${t2.name}\n${t2.description}\nПараметры: ${JSON.stringify(t2.parameters)}`)
|
|
@@ -11,6 +59,7 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
|
|
|
11
59
|
? `\n## Attachments\n\nThe user attached files to this task: ${attachments.join(', ')}.\n` +
|
|
12
60
|
`Each [image#N] / [file#N] marker corresponds to a file the user pasted into the terminal; the file was uploaded to the chat and also saved under <project>/tmp. Look at the images in the chat; for files, read the copy in tmp if you need the contents.\n`
|
|
13
61
|
: '';
|
|
62
|
+
const contextSection = renderContextSection(context);
|
|
14
63
|
return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
|
|
15
64
|
|
|
16
65
|
You work in the directory: ${workdir}
|
|
@@ -63,7 +112,7 @@ task is done (or when you must ask the operator), not between tool calls.
|
|
|
63
112
|
You have access to the following tools:
|
|
64
113
|
|
|
65
114
|
${toolDescriptions}
|
|
66
|
-
${gitSection}${attachSection}
|
|
115
|
+
${gitSection}${attachSection}${contextSection}
|
|
67
116
|
## How to use tools
|
|
68
117
|
|
|
69
118
|
To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
|