zames_pro 2.60.0 → 2.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -2
- package/README.md +64 -23
- package/dist/agent-loop.js +55 -2
- package/dist/backlog.js +166 -0
- package/dist/checkpoint.js +197 -0
- package/dist/commands.js +157 -0
- package/dist/compact.js +2 -2
- package/dist/config.js +20 -0
- package/dist/context.js +88 -1
- package/dist/i18n.js +121 -0
- package/dist/index.js +390 -26
- package/dist/input.js +3 -2
- package/dist/path-token.js +75 -0
- package/dist/permissions.js +98 -0
- package/dist/system-prompt.js +39 -2
- package/logo-small.jpg +0 -0
- package/package.json +3 -2
- package/scripts/coverage-gate.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,96 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [2.63.0] - 2026-10-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Реальные подтверждения действий (BACKLOG C1): политика approval в
|
|
13
|
+
`.zames/permissions.json` (`default` + `rules` с regexp по `tool`/`command`/
|
|
14
|
+
`path` и действием `allow`|`deny`|`ask`). Вызывается в `agent-loop.ts` ПЕРЕД
|
|
15
|
+
каждым инструментом: `deny` блокирует вызов, `ask` спрашивает оператора через
|
|
16
|
+
`onAskPermission` (в TTY — интерактивный промпт над инпут-линией, в non-TTY —
|
|
17
|
+
запрет). Новый чистый модуль `src/permissions.ts` + тесты
|
|
18
|
+
(test/permissions.test.ts, test/permissions-loop.test.ts).
|
|
19
|
+
|
|
20
|
+
- Кастомные команды: аргументы и подсказки (BACKLOG B7). Frontmatter
|
|
21
|
+
`argument-hint:` показывается в списке «/» и в `/help` (не вставляется в
|
|
22
|
+
строку ввода), а `arguments:` объявляет обязательные позиционные аргументы.
|
|
23
|
+
В теле команды подставляются `$1 $2`, именованные `$name`, а также прежние
|
|
24
|
+
`{{args}}`/`$ARGUMENTS`. Если обязательные аргументы не переданы — команда
|
|
25
|
+
не уходит в чат, печатается подсказка. Новые чистые хелперы
|
|
26
|
+
`splitCommandArgs()` / `expandCommandArgs()` / `missingCommandArgs()`
|
|
27
|
+
(src/commands.ts, покрыты тестами).
|
|
28
|
+
|
|
29
|
+
- Path-scoped правила (BACKLOG B6): вложенные `AGENTS.md`/`MEMORY.md` из
|
|
30
|
+
подпапок, которых касается задача, подтягиваются автоматически. Текст задачи
|
|
31
|
+
сканируется на path-токены, для найденных директорий (и их предков ниже
|
|
32
|
+
рабочей) читаются ближайшие инструкции и рендерятся отдельной секцией
|
|
33
|
+
`## Scoped instructions (...)` — явно помечены как действующие только для
|
|
34
|
+
этих файлов. `loadProjectContext(workdir, touchPaths?)` и
|
|
35
|
+
`renderContextSection()` (src/context.ts, src/system-prompt.ts; покрыто
|
|
36
|
+
тестами).
|
|
37
|
+
|
|
38
|
+
- `@file`-ссылки в задаче (BACKLOG B5): `реши задачу @src/browser.ts`
|
|
39
|
+
подставляет содержимое указанного файла прямо в задачу, экономя отдельный
|
|
40
|
+
ход агента на чтение. Распознаётся `@path` на границе слова (в начале строки
|
|
41
|
+
или после пробела/скобки/кавычки), с расширением файла; `user@host` и
|
|
42
|
+
декораторы (`@Component`) не трогаются. Существующие файлы инлайнятся
|
|
43
|
+
(лимит 60 КБ на файл, 200 КБ суммарно — сверх этого усечение с пометкой),
|
|
44
|
+
несуществующие остаются как есть. Хелпер `extractAtFileRefs()` в
|
|
45
|
+
`src/path-token.ts` (чистый, покрыт тестами).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `AGENTS.md` уменьшен (BACKLOG C2): глубокие root-cause разборы («агент
|
|
50
|
+
остановился», чтение ответа из DOM, send-хуки, `LineEditor`, вложения) и
|
|
51
|
+
терминальная механика вынесены в `docs/DESIGN-NOTES.md` (progressive
|
|
52
|
+
disclosure — не грузится в каждую задачу). В AGENTS.md остались действующие
|
|
53
|
+
правила и краткая выжимка со ссылкой.
|
|
54
|
+
|
|
55
|
+
## [2.62.0]
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Checkpoints / rewind (BACKLOG B3): at the start of every task the working
|
|
60
|
+
tree is snapshotted into a tarball under `~/.zames/checkpoints/`, and
|
|
61
|
+
`/rewind [n]` rolls it back in one step (with a confirmation prompt and an
|
|
62
|
+
automatic `pre-rewind` backup of the current state, so the rewind itself is
|
|
63
|
+
reversible). `/rewind-list` shows the recent checkpoints. `node_modules`,
|
|
64
|
+
`.git`, `dist` and `tmp` are never snapshotted or deleted. Unlike a manual
|
|
65
|
+
git stash, this works in a non-git directory and never touches the
|
|
66
|
+
operator's index. New module `src/checkpoint.ts` (pure helpers + `CheckpointStore`,
|
|
67
|
+
unit-tested); configurable via `checkpoint.enabled` / `checkpoint.maxBackups`
|
|
68
|
+
(default 50) in `/config`.
|
|
69
|
+
|
|
70
|
+
## [2.61.0]
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- `/backlog <text>` — record an improvement idea in `BACKLOG.md` without
|
|
75
|
+
implementing it. Deterministic: a fresh `N<n>` id under the matching
|
|
76
|
+
`P0..P3` section; an optional leading `P0..P3` picks the section, the first
|
|
77
|
+
line is the title, the rest the body. `/backlog collapse` prunes the
|
|
78
|
+
archived blocks.
|
|
79
|
+
- BACKLOG.md maintenance (`src/backlog.ts`, pure/tested): `/improve` now
|
|
80
|
+
auto-prunes a finished item's archived `<details>` copy after a successful
|
|
81
|
+
run (the `### X. [x] ... done` summary line is kept), and a startup warning
|
|
82
|
+
fires when BACKLOG.md exceeds 500 lines / 60 KB. The collapser tolerates an
|
|
83
|
+
UNCLOSED `<details>` (a real file had one).
|
|
84
|
+
- Dev-mode self-improvement note: in `--dev` the system prompt tells the model
|
|
85
|
+
it may append ONE short BACKLOG.md bullet when it spots an improvement
|
|
86
|
+
outside the current task (off in a normal run).
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- Self-development commands (`/improve`, `/backlog`, `/self-review`,
|
|
91
|
+
`/self-fix`, `/self-done`, `/self-list`, `/self-diff`, `/self-apply`) are now
|
|
92
|
+
DEV-ONLY: they are hidden from `/help` and the «/» hints, and rejected by the
|
|
93
|
+
main loop, unless the operator runs in dev mode (`--dev` or
|
|
94
|
+
`config.hotReload`). A regular package install no longer advertises them, and
|
|
95
|
+
a hand-typed `/improve` can no longer edit an unrelated project's
|
|
96
|
+
BACKLOG.md. The list is `DEV_ONLY_COMMANDS` / `isDevOnlyCommand()` in
|
|
97
|
+
`src/commands.ts` (pure, tested).
|
|
9
98
|
|
|
10
99
|
## [2.60.0]
|
|
11
100
|
|
|
@@ -220,7 +309,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
220
309
|
- Session banner warns when no saved DeepSeek session exists.
|
|
221
310
|
- `--no-color` flag (explicit `NO_COLOR`).
|
|
222
311
|
|
|
223
|
-
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.
|
|
312
|
+
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.63.0...HEAD
|
|
313
|
+
[2.63.0]: https://github.com/Viqto0r/zames_pro/compare/v2.61.0...v2.63.0
|
|
314
|
+
[2.61.0]: https://github.com/Viqto0r/zames_pro/compare/v2.60.0...v2.61.0
|
|
224
315
|
[2.60.0]: https://github.com/Viqto0r/zames_pro/compare/v2.59.0...v2.60.0
|
|
225
316
|
[2.59.0]: https://github.com/Viqto0r/zames_pro/compare/v2.58.0...v2.59.0
|
|
226
317
|
[2.58.0]: https://github.com/Viqto0r/zames_pro/compare/v2.57.1...v2.58.0
|
package/README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/logo-small.jpg" alt="zames logo" width="180">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/zames_pro)
|
|
5
|
-
[](https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
[](package.json)
|
|
8
|
-
[](CONTRIBUTING.md)
|
|
5
|
+
<h1 align="center">zames_pro</h1>
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A terminal coding agent that drives chat.deepseek.com through Playwright — no API key required.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/zames_pro"><img src="https://img.shields.io/npm/v/zames_pro.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/zames_pro"><img src="https://img.shields.io/npm/dm/zames_pro.svg" alt="npm downloads"></a>
|
|
14
|
+
<a href="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml"><img src="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml/badge.svg" alt="tests"></a>
|
|
15
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
16
|
+
<a href="package.json"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js"></a>
|
|
17
|
+
</p>
|
|
11
18
|
|
|
12
19
|
A terminal coding agent that works on top of [chat.deepseek.com](https://chat.deepseek.com/) through Playwright.
|
|
13
20
|
In spirit it is similar to Claude Code / Codex CLI: it starts in the current
|
|
@@ -16,6 +23,44 @@ directory, reads and edits files, runs commands, and commits to git.
|
|
|
16
23
|
> No API key required — it drives the DeepSeek web chat like a regular user
|
|
17
24
|
> through a real (headless) browser.
|
|
18
25
|
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
zames does not call the model API. It launches a headless Chromium with a
|
|
29
|
+
persistent profile, signs in to `chat.deepseek.com` like a human, types the task
|
|
30
|
+
into the chat box, and reads the answer back.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
terminal (you) zames chat.deepseek.com
|
|
34
|
+
─────────────── ───── ─────────────────
|
|
35
|
+
task ──────────────────────▶ system prompt + task ────────▶ browser types it
|
|
36
|
+
▲ │
|
|
37
|
+
│ model answers
|
|
38
|
+
│ │
|
|
39
|
+
answer ◀──── render ◀───── parse tool-call ◀──── raw answer ◀────┘
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
run tool (Read/Edit/Bash/…) → feed result back
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Key pieces:
|
|
46
|
+
|
|
47
|
+
- **The answer is read from the raw network stream** (SSE), not the rendered
|
|
48
|
+
DOM, so tool-call JSON with template strings and escapes survives intact.
|
|
49
|
+
- **A persistent profile** (`~/.zames/profile`) keeps you signed in; the
|
|
50
|
+
headless User-Agent is patched so DeepSeek's CDN does not 403 the login.
|
|
51
|
+
- **A send throttle** (15 s by default) keeps the web chat's rate limit happy
|
|
52
|
+
during long tool-heavy runs.
|
|
53
|
+
- **The agent is sandboxed** to the directory it was started in — no tool can
|
|
54
|
+
read or write above it.
|
|
55
|
+
|
|
56
|
+
## Table of contents
|
|
57
|
+
|
|
58
|
+
- [Features](#features) · [Why zames?](#why-zames) · [Requirements](#requirements)
|
|
59
|
+
- [How it works](#how-it-works) · [Installation](#installation) · [Signing in](#signing-in) · [Usage](#usage)
|
|
60
|
+
- [Tools](#tools) · [Slash commands](#slash-commands)
|
|
61
|
+
- [Project context, skills and memory](#project-context-skills-and-memory) · [MCP (external tools)](#mcp-external-tools) · [Configuration](#configuration)
|
|
62
|
+
- [FAQ](#faq) · [Links](#links) · [License](#license)
|
|
63
|
+
|
|
19
64
|
## Features
|
|
20
65
|
|
|
21
66
|
- **Tools like Claude Code / Codex** — `Read`, `Write`, `Edit`, `Bash`,
|
|
@@ -29,8 +74,6 @@ directory, reads and edits files, runs commands, and commits to git.
|
|
|
29
74
|
custom commands from the repo and `~/.zames`, the same idea as Codex / Claude
|
|
30
75
|
Code.
|
|
31
76
|
- **MCP support** — plug in external tool servers (e.g. `@playwright/mcp`).
|
|
32
|
-
- **Self-review** — `/self-review` snapshots `src/` so the agent can review and
|
|
33
|
-
fix itself in a sandbox (`/self-fix`, `/self-apply`).
|
|
34
77
|
- **Scheduling** — `/loop`, `/cron` and `/jobs` repeat tasks on a timer.
|
|
35
78
|
- **Bilingual UI** — Russian / English (`/config lang`).
|
|
36
79
|
|
|
@@ -47,7 +90,7 @@ directory, reads and edits files, runs commands, and commits to git.
|
|
|
47
90
|
|
|
48
91
|
## Requirements
|
|
49
92
|
|
|
50
|
-
- Node.js >= 20
|
|
93
|
+
- Node.js >= 20
|
|
51
94
|
- A DeepSeek account. On first launch zames asks for your DeepSeek
|
|
52
95
|
login/password in the terminal (and stores them in `~/.zames/config.json`
|
|
53
96
|
after a successful sign-in, so a later logout is handled automatically
|
|
@@ -85,13 +128,6 @@ version). You do not need `--headed` just to log in.
|
|
|
85
128
|
Credentials and toggles can also be edited from `/config`
|
|
86
129
|
(`browser.auth.username`, `browser.auth.password`, `browser.auth.saveSession`).
|
|
87
130
|
|
|
88
|
-
## Links
|
|
89
|
-
|
|
90
|
-
- npm: <https://www.npmjs.com/package/zames_pro>
|
|
91
|
-
- Changelog: [`CHANGELOG.md`](CHANGELOG.md)
|
|
92
|
-
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
93
|
-
- Security policy: [`SECURITY.md`](SECURITY.md)
|
|
94
|
-
|
|
95
131
|
## Installation
|
|
96
132
|
|
|
97
133
|
```bash
|
|
@@ -247,10 +283,6 @@ Codex CLI:
|
|
|
247
283
|
instead of the whole list.
|
|
248
284
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
249
285
|
and report findings (no code changes).
|
|
250
|
-
- /improve [id] — self-improvement loop: take the next open item from
|
|
251
|
-
`BACKLOG.md` (or a specific id, e.g. `/improve B3`), implement it, run the
|
|
252
|
-
typecheck/lint/tests, mark it done and add a CHANGELOG entry. Nothing is
|
|
253
|
-
committed — the changes stay in the working tree for review.
|
|
254
286
|
- /plan [on|off] — plan (read-only) mode. While it is on, the mutating tools
|
|
255
287
|
(Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush) are removed
|
|
256
288
|
from the tool set, so the agent can investigate without touching the tree.
|
|
@@ -384,7 +416,7 @@ Changes are written to the project `.zamesrc.json` and applied right away
|
|
|
384
416
|
(help, messages, spinner) and the language the agent answers you in. The
|
|
385
417
|
locale lives in `ui.locale` in the config file.
|
|
386
418
|
|
|
387
|
-
Agent data is stored in `~/.zames`: browser profile, logs, undo history,
|
|
419
|
+
Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
|
|
388
420
|
|
|
389
421
|
## FAQ
|
|
390
422
|
|
|
@@ -414,6 +446,15 @@ Same shape (tools, `AGENTS.md`, skills, MCP, slash commands) but it runs on your
|
|
|
414
446
|
DeepSeek account instead of an API, as a browser automation rather than a
|
|
415
447
|
first-party API client.
|
|
416
448
|
|
|
449
|
+
## Links
|
|
450
|
+
|
|
451
|
+
- **npm:** <https://www.npmjs.com/package/zames_pro>
|
|
452
|
+
- **GitHub:** <https://github.com/Viqto0r/zames_pro>
|
|
453
|
+
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
|
|
454
|
+
- **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
455
|
+
- **Security policy:** [`SECURITY.md`](SECURITY.md)
|
|
456
|
+
- **Code of conduct:** [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
|
457
|
+
|
|
417
458
|
## License
|
|
418
459
|
|
|
419
460
|
MIT
|
package/dist/agent-loop.js
CHANGED
|
@@ -6,12 +6,15 @@ import { translate } from './i18n.js';
|
|
|
6
6
|
import { substituteAttachmentMarkers } from './commands.js';
|
|
7
7
|
import { normText } from './browser.js';
|
|
8
8
|
import { loadHooks, runPreToolUse, runPostToolUse, } from './hooks.js';
|
|
9
|
-
|
|
9
|
+
import { loadPermissions, decidePermission, } from './permissions.js';
|
|
10
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, onAskPermission = undefined, selfImprovement = false, }) {
|
|
10
11
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
11
12
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
12
13
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
13
14
|
// hooks entirely.
|
|
14
15
|
const hookConfig = hooks === undefined ? loadHooks(workdir) : (hooks ?? {});
|
|
16
|
+
// Same for the approval policy (C1): read `.zames/permissions.json` once.
|
|
17
|
+
const permissionPolicy = permissions === undefined ? loadPermissions(workdir) : permissions;
|
|
15
18
|
// UI callbacks must NEVER break the agent loop. A rendering error (a huge
|
|
16
19
|
// tool result, a broken markdown frame, a closed terminal) used to throw
|
|
17
20
|
// out of the loop right after a tool call — the session looked "stopped
|
|
@@ -91,7 +94,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
91
94
|
}
|
|
92
95
|
let context;
|
|
93
96
|
try {
|
|
94
|
-
|
|
97
|
+
// Pass the task text so nested AGENTS.md/MEMORY.md for the directories
|
|
98
|
+
// this task actually touches are pulled in (B6, path-scoped rules).
|
|
99
|
+
context = await loadProjectContext(workdir, [task]);
|
|
95
100
|
}
|
|
96
101
|
catch {
|
|
97
102
|
context = null;
|
|
@@ -102,11 +107,13 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
102
107
|
gitContext: gitText,
|
|
103
108
|
locale,
|
|
104
109
|
context,
|
|
110
|
+
selfImprovement,
|
|
105
111
|
});
|
|
106
112
|
transcript?.log('system_prompt', {
|
|
107
113
|
length: systemPrompt.length,
|
|
108
114
|
gitContext: gitText,
|
|
109
115
|
agents: context?.agents.map((f) => f.path) ?? [],
|
|
116
|
+
scopedAgents: context?.scopedAgents?.map((f) => f.path) ?? [],
|
|
110
117
|
memory: context?.memory.map((f) => f.path) ?? [],
|
|
111
118
|
skills: context?.skills.map((s) => s.name) ?? [],
|
|
112
119
|
commands: context?.commands.map((c) => c.name) ?? [],
|
|
@@ -726,6 +733,47 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
726
733
|
results.push({ tool: call.tool, result: blocked });
|
|
727
734
|
continue;
|
|
728
735
|
}
|
|
736
|
+
// Approval policy (C1): a `deny` rule blocks the call (like a PreToolUse
|
|
737
|
+
// denial); an `ask` rule prompts the operator. Hooks stay authoritative
|
|
738
|
+
// for programmatic guards — this is the human-in-the-loop layer.
|
|
739
|
+
const decision = decidePermission(permissionPolicy, call.tool, call.args);
|
|
740
|
+
if (decision.action === 'deny') {
|
|
741
|
+
const blocked = `Blocked by permission policy: ${decision.reason}`;
|
|
742
|
+
transcript?.log('permission_deny', {
|
|
743
|
+
tool: call.tool,
|
|
744
|
+
reason: decision.reason,
|
|
745
|
+
});
|
|
746
|
+
safeToolResult(blocked);
|
|
747
|
+
transcript?.log('tool_result', {
|
|
748
|
+
tool: call.tool,
|
|
749
|
+
result: blocked,
|
|
750
|
+
});
|
|
751
|
+
results.push({ tool: call.tool, result: blocked });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (decision.action === 'ask' && onAskPermission) {
|
|
755
|
+
let allowed;
|
|
756
|
+
try {
|
|
757
|
+
allowed = await onAskPermission({ ...decision, tool: call.tool });
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
allowed = false;
|
|
761
|
+
}
|
|
762
|
+
if (!allowed) {
|
|
763
|
+
const blocked = `Denied by operator: ${decision.reason}`;
|
|
764
|
+
transcript?.log('permission_denied', {
|
|
765
|
+
tool: call.tool,
|
|
766
|
+
reason: decision.reason,
|
|
767
|
+
});
|
|
768
|
+
safeToolResult(blocked);
|
|
769
|
+
transcript?.log('tool_result', {
|
|
770
|
+
tool: call.tool,
|
|
771
|
+
result: blocked,
|
|
772
|
+
});
|
|
773
|
+
results.push({ tool: call.tool, result: blocked });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
729
777
|
let result;
|
|
730
778
|
// While the tool runs, poll for an Esc/Ctrl+C: the abort flag is a plain
|
|
731
779
|
// boolean set by stopGeneration(), so the only way to turn it into a
|
|
@@ -734,6 +782,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
734
782
|
const poll = setInterval(syncToolAbort, 100);
|
|
735
783
|
if (typeof poll.unref === 'function')
|
|
736
784
|
poll.unref();
|
|
785
|
+
// Time the tool (T-D3): the transcript carries durationMs so /cost can
|
|
786
|
+
// show where the time goes (frequent Read→Edit cycles vs slow Bash).
|
|
787
|
+
const toolStart = Date.now();
|
|
737
788
|
try {
|
|
738
789
|
result = await tool.fn(call.args, { signal: toolAbort.signal });
|
|
739
790
|
}
|
|
@@ -743,6 +794,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
743
794
|
finally {
|
|
744
795
|
clearInterval(poll);
|
|
745
796
|
}
|
|
797
|
+
const toolMs = Date.now() - toolStart;
|
|
746
798
|
// PostToolUse hooks run AFTER the tool; their stdout is appended to the
|
|
747
799
|
// result (e.g. `prettier` output) before it is fed back to the model.
|
|
748
800
|
// Best-effort: a hook failure is ignored, the tool result still stands.
|
|
@@ -758,6 +810,7 @@ ${post}`;
|
|
|
758
810
|
transcript?.log('tool_result', {
|
|
759
811
|
tool: call.tool,
|
|
760
812
|
result: String(result),
|
|
813
|
+
durationMs: toolMs,
|
|
761
814
|
});
|
|
762
815
|
results.push({ tool: call.tool, result });
|
|
763
816
|
// The operator pressed Esc/Ctrl+C while the tool was running. The tool
|
package/dist/backlog.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { parseBacklogItems } from './commands.js';
|
|
2
|
+
// BACKLOG.md maintenance — the pure half of /backlog and the automatic pruning
|
|
3
|
+
// that runs after /improve. BACKLOG.md is the agent's own improvement-notes
|
|
4
|
+
// file (gitignored); it is READ by /improve, so keeping it small matters —
|
|
5
|
+
// every finished item used to keep a full copy of its original text inside a
|
|
6
|
+
// <details> block, and the file grew without bound.
|
|
7
|
+
//
|
|
8
|
+
// Everything here is pure and unit-tested; index.ts only reads/writes the file.
|
|
9
|
+
const NL = String.fromCharCode(10);
|
|
10
|
+
// Agent-appended items use this id prefix. A distinct letter keeps them apart
|
|
11
|
+
// from the hand-numbered A/B/C/D/E entries, so it is obvious which notes the
|
|
12
|
+
// agent added on its own.
|
|
13
|
+
const NEW_ITEM_PREFIX = 'N';
|
|
14
|
+
// Past these thresholds BACKLOG.md costs more tokens (on /improve) than it is
|
|
15
|
+
// worth, so the operator is nudged to collapse it.
|
|
16
|
+
export const BACKLOG_WARN_LINES = 500;
|
|
17
|
+
export const BACKLOG_WARN_CHARS = 60_000;
|
|
18
|
+
function squashBlankLines(lines) {
|
|
19
|
+
const out = [];
|
|
20
|
+
let blanks = 0;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.trim() === '') {
|
|
23
|
+
blanks++;
|
|
24
|
+
if (blanks > 2)
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
blanks = 0;
|
|
29
|
+
}
|
|
30
|
+
out.push(line);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
// A real, kept heading: a `## P<n>` section or a `### X<n>.` item heading that
|
|
35
|
+
// is NOT the archived copy (archived copies carry `~~` right after the id).
|
|
36
|
+
function isRealBoundary(line) {
|
|
37
|
+
const t = line.trim();
|
|
38
|
+
if (/^##\s/.test(t))
|
|
39
|
+
return true;
|
|
40
|
+
return /^###\s+[A-Z]\d+[.]/.test(t) && !/^###\s+[A-Z]\d+[.]\s+~~/.test(t);
|
|
41
|
+
}
|
|
42
|
+
function isDetailsOpen(line) {
|
|
43
|
+
return line.trim().startsWith('<details');
|
|
44
|
+
}
|
|
45
|
+
function isArchivedHeading(line) {
|
|
46
|
+
return /^###\s+[A-Z]\d+[.]\s+~~/.test(line.trim());
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove archived copies of finished items. A finished item keeps its
|
|
50
|
+
* `### X. [x] ... done` summary line; the `### X. ~~original~~` copy and its
|
|
51
|
+
* body (normally wrapped in a <details> block) are dropped.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately tolerant of an UNCLOSED <details>: a real backlog item was
|
|
54
|
+
* written as `### A1. [x] ...` then `<details>...` with no `</details>`, so the
|
|
55
|
+
* whole file became one nested block. Keying the end of a block on the next
|
|
56
|
+
* REAL heading (a `### X.` without `~~`, or a `## ` section) — not only on
|
|
57
|
+
* `</details>` — keeps the summary lines and the open items even then. Pure and
|
|
58
|
+
* unit-tested; a heading that looks like an item INSIDE an archive is skipped
|
|
59
|
+
* because it carries `~~`.
|
|
60
|
+
*/
|
|
61
|
+
export function collapseBacklog(text) {
|
|
62
|
+
const src = String(text ?? '');
|
|
63
|
+
const lines = src.split(NL);
|
|
64
|
+
const out = [];
|
|
65
|
+
let collapsed = 0;
|
|
66
|
+
let i = 0;
|
|
67
|
+
while (i < lines.length) {
|
|
68
|
+
if (isDetailsOpen(lines[i]) || isArchivedHeading(lines[i])) {
|
|
69
|
+
// Skip to the end of the archive: the first `</details>` OR the first
|
|
70
|
+
// real heading, whichever comes first (see the note above).
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
while (j < lines.length) {
|
|
73
|
+
const t = lines[j].trim();
|
|
74
|
+
if (t.startsWith('</details>')) {
|
|
75
|
+
j++;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (isRealBoundary(t))
|
|
79
|
+
break;
|
|
80
|
+
j++;
|
|
81
|
+
}
|
|
82
|
+
collapsed++;
|
|
83
|
+
i = j;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
out.push(lines[i]);
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
const next = squashBlankLines(out).join(NL);
|
|
90
|
+
return { text: next, changed: next !== src, collapsed };
|
|
91
|
+
}
|
|
92
|
+
export function backlogStats(text) {
|
|
93
|
+
const src = String(text ?? '');
|
|
94
|
+
const items = parseBacklogItems(src);
|
|
95
|
+
let archived = 0;
|
|
96
|
+
for (const line of src.split(NL)) {
|
|
97
|
+
if (line.trim().startsWith('<details'))
|
|
98
|
+
archived++;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
lines: src.split(NL).length,
|
|
102
|
+
chars: src.length,
|
|
103
|
+
open: items.filter((i) => i.status === 'open').length,
|
|
104
|
+
done: items.filter((i) => i.status === 'done').length,
|
|
105
|
+
archived,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** True when the file is big enough to deserve a collapse nudge. */
|
|
109
|
+
export function backlogNeedsPruning(stats) {
|
|
110
|
+
return stats.lines > BACKLOG_WARN_LINES || stats.chars > BACKLOG_WARN_CHARS;
|
|
111
|
+
}
|
|
112
|
+
/** The next free N<n> id not yet used in the file. */
|
|
113
|
+
export function nextBacklogId(text) {
|
|
114
|
+
let max = 0;
|
|
115
|
+
const re = /^###[ ]+([A-Z])([0-9]+)[.]/gm;
|
|
116
|
+
let m;
|
|
117
|
+
while ((m = re.exec(String(text ?? '')))) {
|
|
118
|
+
if (m[1] === NEW_ITEM_PREFIX)
|
|
119
|
+
max = Math.max(max, Number(m[2]));
|
|
120
|
+
}
|
|
121
|
+
return NEW_ITEM_PREFIX + (max + 1);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Insert a new OPEN item under the matching `## P<n>` section (or append a new
|
|
125
|
+
* section at the end). Returns the new file text and the generated id. Pure;
|
|
126
|
+
* the caller writes the file.
|
|
127
|
+
*/
|
|
128
|
+
export function appendBacklogItem(text, item) {
|
|
129
|
+
const src = String(text ?? '');
|
|
130
|
+
const title = String(item.title ?? '')
|
|
131
|
+
.trim()
|
|
132
|
+
.replace(/ {2,}/g, ' ');
|
|
133
|
+
const prio = /^P[0-3]$/i.test(String(item.priority ?? '').trim())
|
|
134
|
+
? String(item.priority).trim().toUpperCase()
|
|
135
|
+
: 'P2';
|
|
136
|
+
const note = String(item.note ?? '').trim();
|
|
137
|
+
const id = nextBacklogId(src);
|
|
138
|
+
const entry = ['### ' + id + '. ' + title, ''];
|
|
139
|
+
if (note) {
|
|
140
|
+
for (const l of note.split(NL))
|
|
141
|
+
entry.push(l);
|
|
142
|
+
entry.push('');
|
|
143
|
+
}
|
|
144
|
+
const lines = src.split(NL);
|
|
145
|
+
const headerRe = new RegExp('^##[ ]+' + prio + '(?![0-9])');
|
|
146
|
+
let at = -1;
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
if (headerRe.test(lines[i])) {
|
|
149
|
+
// Insert right after the header and any blank lines that follow it, so
|
|
150
|
+
// the newest note sits at the top of its priority block.
|
|
151
|
+
let j = i + 1;
|
|
152
|
+
while (j < lines.length && lines[j].trim() === '')
|
|
153
|
+
j++;
|
|
154
|
+
at = j;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (at === -1) {
|
|
159
|
+
while (lines.length && lines[lines.length - 1].trim() === '')
|
|
160
|
+
lines.pop();
|
|
161
|
+
const out = lines.concat(['', '## ' + prio, '', ...entry]).join(NL);
|
|
162
|
+
return { text: out, id };
|
|
163
|
+
}
|
|
164
|
+
const out = lines.slice(0, at).concat(entry, lines.slice(at)).join(NL);
|
|
165
|
+
return { text: out, id };
|
|
166
|
+
}
|