@magland/gentl 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +157 -2
- package/dist/claude.js +120 -0
- package/dist/cli.js +316 -0
- package/dist/docker.js +76 -0
- package/dist/evaluate.js +126 -0
- package/dist/finalstate.js +64 -0
- package/dist/format.js +15 -0
- package/dist/host.js +64 -0
- package/dist/hub.js +147 -0
- package/dist/localcopy.js +116 -0
- package/dist/model.js +112 -0
- package/dist/protocol.js +188 -0
- package/dist/run.js +423 -0
- package/dist/sandbox.js +129 -0
- package/dist/suite.js +114 -0
- package/dist/tokens.js +7 -0
- package/package.json +43 -4
- package/sandbox/Dockerfile +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# gentl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A readability test for codebases. A memoryless agent carries out a realistic task the way a newcomer would (clone the repo, set up the environment from the docs, make a change, verify it), and gentl measures how much the agent had to read. The premise is that if an agent with no memory beyond a small set of notes can do the task, a human newcomer probably can too.
|
|
4
|
+
|
|
5
|
+
gentl is an evaluation tool, not a coding agent; the code the agent writes is a by-product. There are two outputs: the reading effort (a number, mostly useful as a trend over doc changes) and a friction report from an evaluator (what was missing or wrong in the docs, and where the agent guessed).
|
|
6
|
+
|
|
7
|
+
Status: early.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Requires Node >= 20 and Docker (or use `--host`).
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install -g @magland/gentl
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
From source: `git clone https://github.com/magland/gentl && cd gentl && npm install && npm link`.
|
|
18
|
+
|
|
19
|
+
For the default backend, set `OPENROUTER_API_KEY` in the environment or in a `.env` file in the directory you run from. To use your Claude Code subscription instead, pass `--backend claude-code` (or set `GENTL_BACKEND=claude-code`); see Backends.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
gentl run TASK.md
|
|
25
|
+
gentl run -t "Clone https://github.com/owner/foo, install it and run its tests."
|
|
26
|
+
gentl run tasks/ # a suite: every *.md in the directory
|
|
27
|
+
gentl run https://<hub>/tasks/42 --upload # a task from a hub; see Hub
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A task is plain text. It names the repos to clone and, where it matters, the tag or commit; the agent does the cloning itself.
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
|
|
34
|
+
- `--image IMG`: the Docker image for the sandbox. The default is built on first use from [sandbox/Dockerfile](sandbox/Dockerfile) and tagged by its content (`gentl-sandbox:<hash>`).
|
|
35
|
+
- `--host`: run on this machine in a fresh temp directory instead of Docker, for software that can't run in a container (for example a licensed MATLAB).
|
|
36
|
+
- `--confirm`: ask before each action. This is the default with `--host`; Docker runs are unattended unless you pass it.
|
|
37
|
+
- `--yes`: with `--host`, run actions without asking.
|
|
38
|
+
- `--model M`: the agent's model, default `anthropic/claude-haiku-5.5`.
|
|
39
|
+
- `--evaluator-model M`: the evaluator's model, same default.
|
|
40
|
+
- `--upload`: upload each finished run to the hub its task came from.
|
|
41
|
+
- `--yes`: don't ask before running a hub task (and, with `--host`, before each action).
|
|
42
|
+
- `--backend B`: `openrouter` (default) or `claude-code`; see Backends.
|
|
43
|
+
- `--local` / `--remote`: serve clones of the task's repo from your checkout (the default inside a git repo), or clone the published repo; see Local copy.
|
|
44
|
+
- `--base-url URL`: any OpenAI-compatible endpoint (default `https://openrouter.ai/api/v1`). The key in `OPENROUTER_API_KEY` is sent to it.
|
|
45
|
+
- `--max-turns N`: default 50.
|
|
46
|
+
- `--repeat N`: run each task N times; see Repeated runs.
|
|
47
|
+
- `--keep`: keep the container or temp workspace instead of deleting it.
|
|
48
|
+
|
|
49
|
+
### Local copy
|
|
50
|
+
|
|
51
|
+
When the task file (or suite directory, or the current directory for `-t`) is inside a git repo with an `origin` remote, gentl serves clones of that origin from your checkout, including uncommitted changes. So you can edit the docs and rerun without pushing. At startup gentl makes a temporary bare mirror of your checkout; uncommitted changes (tracked and untracked, not ignored) become one extra commit, "Work in progress", under your git identity, on top of your current branch. Your own repo, index and working tree are not touched. The sandbox's git config then redirects the origin URL (in its https, http, `git@host:` and `ssh://` forms, with and without `.git`) to the mirror with `url.<mirror>.insteadOf`, so the agent runs the `git clone` the task names. In Docker the mirror is mounted read-only at `/srv/git`. A suite uses one mirror for all its tasks. gentl prints the local copy at the start, and records it in `summary.json` (`local_copy`); the evaluator is told about it.
|
|
52
|
+
|
|
53
|
+
`--remote` turns this off; `--local` makes it an error if there is no repo to serve. Hub tasks (later) will always clone the published repo.
|
|
54
|
+
|
|
55
|
+
Limits:
|
|
56
|
+
|
|
57
|
+
- Pages the agent reads on GitHub through w3m or curl still show the pushed version, and a task that pins a tag or commit gets that version, not your edits.
|
|
58
|
+
- `git remote -v` in the agent's clone shows the mirror's `file://` URL, so an agent that looks can tell.
|
|
59
|
+
- git's `insteadOf` matches by prefix, so `https://github.com/owner/foo` also catches `https://github.com/owner/foo-extra`. gentl warns when the task text mentions such a URL.
|
|
60
|
+
|
|
61
|
+
### Docker (default)
|
|
62
|
+
|
|
63
|
+
Each run gets a fresh container (`docker run --init`, working directory `/work`, network enabled), removed at the end. The default image is Debian (trixie) with Python 3.13, Node, build tools, git, curl and w3m; pip works as root without a virtual environment. The agent's commands run with `docker exec` under `setsid`, so a timeout or Ctrl-C kills the whole command inside the container. A custom image needs bash and `setsid` (util-linux), and git for most tasks. No environment variables from your machine are passed into the container.
|
|
64
|
+
|
|
65
|
+
The image and its id are recorded in `summary.json`.
|
|
66
|
+
|
|
67
|
+
### Host mode
|
|
68
|
+
|
|
69
|
+
The agent works in `work/` inside a fresh temp directory, with `HOME` set to a sibling `home/` so it does not see your git config, caches or credentials. Only a few environment variables are passed through (`PATH`, locale, proxies); API keys and tokens are not. Each command runs in a fresh bash shell in its own process group, with no input and a 300 s timeout. The workspace is deleted at the end unless `--keep` is given.
|
|
70
|
+
|
|
71
|
+
The agent can still reach anything on your machine that you can, so by default gentl shows each action and asks `ok? [y/n]` before running it. On `n` you can give a reason, which is passed back to the agent. Ctrl-C while a command runs stops that command (the agent is told); Ctrl-C at any other time ends the run, and the run directory is still written. This applies to Docker runs too.
|
|
72
|
+
|
|
73
|
+
Host mode also inherits whatever is installed on your machine (compilers, Python packages, uv), so setup steps can succeed without the docs being followed, and reading effort is not comparable with Docker runs.
|
|
74
|
+
|
|
75
|
+
### Suites
|
|
76
|
+
|
|
77
|
+
`gentl run DIR` runs every `*.md` file in the directory, in name order, skipping `README.md` and files starting with `_`. The task name is the file name. Runs go to `runs/<timestamp>-<suite>/<run>/`, and at the end gentl prints a results table (verdict, self-report, turns, read tokens, notes max, questions, cost) and writes it to `results.md` and `results.json` in the suite directory. Ctrl-C that ends a run skips the remaining runs.
|
|
78
|
+
|
|
79
|
+
### Repeated runs
|
|
80
|
+
|
|
81
|
+
Reading effort varies a lot from run to run, since the agent may take a different route each time, so a single run says little. `--repeat N` runs each task N times, one after another, into `runs/<timestamp>-<task>-xN/` (or the suite directory). Besides the per-run table, it reports per task the success rate (runs the evaluator judged successful) and the median, interquartile range and range of read tokens, the median turns and the median notes max, all over the successful runs only.
|
|
82
|
+
|
|
83
|
+
Each suite should include a baseline task ("clone, install, run the tests"). Its reading effort is the repo's onboarding cost; read other tasks against it rather than subtracting it.
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
Each turn the model sees only the system prompt, the task, its notes, and its last action with the result (shown once). It returns one JSON object:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
{"notes_keep": false, "notes": "...", "notes_edit": {"old": "...", "new": "..."}, "notes_append": "...", "action": {...}}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The notes are the agent's working memory, meant to mimic how much a person can hold in their head at once, so the agent is asked to keep them small. `notes_keep` and `action` are required. Each turn the agent either updates its notes (`notes_keep: false` with one or more of the operations, applied in order: rewrite, edit, append) or keeps them as they are (`notes_keep: true`). Making the choice explicit matters: without it, models often wrote the action first and skipped the notes, then forgot what they had learned. If a response makes no notes decision anyway, gentl asks again within the same turn, once, with a short reminder (counted as reading and recorded as `notes_reminders`).
|
|
94
|
+
|
|
95
|
+
Actions:
|
|
96
|
+
|
|
97
|
+
- `run {cmd, max_chars?}`: bash in the working directory. stdout and stderr are combined. Output longer than `max_chars` (default 2000, at most 20000) is cut in the middle, keeping the head and tail.
|
|
98
|
+
- `write_file {path, content}` and `edit_file {path, old, new}` (`old` must occur exactly once).
|
|
99
|
+
- `ask {question}`: no one answers. The question is recorded as a gap in the docs, and the agent is told to decide from the docs and code.
|
|
100
|
+
- `finish {success, summary, friction?}`: the summary cites the evidence of success; `friction` is the agent's own account of what was missing or unclear in the docs.
|
|
101
|
+
|
|
102
|
+
gentl asks for structured output (a JSON schema) so responses parse reliably; if the backend rejects it, gentl falls back to plain text and parses the first JSON object that contains an `action`.
|
|
103
|
+
|
|
104
|
+
## Backends
|
|
105
|
+
|
|
106
|
+
- `openrouter` (default): any OpenAI-compatible chat completions endpoint, OpenRouter unless `--base-url` says otherwise, with the key in `OPENROUTER_API_KEY`. Each turn is one request with the system prompt and one user message.
|
|
107
|
+
- `claude-code`: your Claude Code login, through a fresh `claude -p` process per call, with no tools, no settings files (so no CLAUDE.md, memory, hooks or plugins), no MCP servers and no saved session. Model ids like `anthropic/claude-haiku-5.5` are mapped to `claude-haiku-5-5`. Claude Code still adds a short preamble and an environment block (cwd, platform, date) to the system prompt, about 740 tokens; `--bare` would remove them but works only with an API key. Effort is Claude Code's default unless `GENTL_CLAUDE_EFFORT` is set. Reported cost is notional. The model call runs on your machine; the sandbox (Docker or host) is unaffected.
|
|
108
|
+
|
|
109
|
+
The backend is recorded in `summary.json`. Runs with different backends are not directly comparable.
|
|
110
|
+
|
|
111
|
+
## Evaluation
|
|
112
|
+
|
|
113
|
+
After a run that finishes (or reaches the turn limit), gentl records the final state of the workspace: for each git repo, `git status`, the commits and `git diff` against its upstream, and untracked files; also any loose files in the working directory. Then an evaluator (one model call) reads the full log, with the parts of each result that the agent did not see marked as such, the questions asked, the agent's finish report and the final state. It writes:
|
|
114
|
+
|
|
115
|
+
- a verdict: did the agent actually accomplish the task, judged from the evidence rather than the agent's claim;
|
|
116
|
+
- a friction report: docs used, what was missing or wrong, where the agent guessed, signs of prior knowledge (commands or APIs it used that never appeared in anything it read), and the questions it asked.
|
|
117
|
+
|
|
118
|
+
The evaluator reads only the run directory, so a run can be evaluated again later, for example with another model:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
gentl evaluate runs/<id> --evaluator-model anthropic/claude-sonnet-5.5
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The evaluator's tokens are not counted as reading effort. Reading effort should be compared only among runs that the evaluator judged successful, with the same agent model.
|
|
125
|
+
|
|
126
|
+
## Hub
|
|
127
|
+
|
|
128
|
+
A hub ([gentl-hub](https://github.com/magland/gentl-hub); the public one is https://gentl-hub.jeremy-magland.workers.dev) holds tasks with titles, shows the command to run each, and collects uploaded runs. gentl contacts a hub only when given a hub task URL or asked to upload.
|
|
129
|
+
|
|
130
|
+
- `gentl run https://<hub>/tasks/<id>` fetches the task, prints its title and text, and asks before running it (`--yes` skips this). Hub tasks always clone the published repo, never a local copy, so uploaded results match public code.
|
|
131
|
+
- `--upload` uploads each run that finished (or hit the turn limit) to that hub; `gentl upload runs/<id>` uploads one later, and `gentl upload runs/<batch>` uploads every run in a batch. A run goes only to the hub its task came from.
|
|
132
|
+
- The first upload to a hub asks for your name (shown with your runs) and the hub's passcode, and offers to remember them in `~/.config/gentl/hubs.json` (mode 0600). `GENTL_HUB_USER` and `GENTL_HUB_PASSCODE` override it.
|
|
133
|
+
- The upload holds `summary.json`, `evaluation.json`, `final-state.md`, and each turn as the agent saw it (notes, action, shown result); full command output stays local.
|
|
134
|
+
|
|
135
|
+
## Measurement
|
|
136
|
+
|
|
137
|
+
Tokens are counted with tiktoken `o200k_base` on exactly what the agent sees, so counts do not depend on the model.
|
|
138
|
+
|
|
139
|
+
- `read_tokens_total` = `notes_tokens_total` + `result_tokens_total`, summed over turns. This is the headline number.
|
|
140
|
+
- `notes_tokens_max`: the largest the notes got, i.e. the most the agent had to hold at once.
|
|
141
|
+
- `result_tokens_*`: the echoed action plus its result, per turn.
|
|
142
|
+
- `overhead_tokens`: system prompt plus task, counted once.
|
|
143
|
+
|
|
144
|
+
Also recorded: turns, model output tokens, provider-reported tokens and cost, parse errors, invalid, repeated and declined actions, and the questions asked.
|
|
145
|
+
|
|
146
|
+
## Run directory
|
|
147
|
+
|
|
148
|
+
Each run is written to `./runs/<YYYYMMDD-HHMMSS>-<task name>/`:
|
|
149
|
+
|
|
150
|
+
- `events.jsonl`: a `start` event, one `turn` event per turn (full model input and response, parsed response, notes, full and shown result, where the shown result was cut, token counts, provider usage), and an `end` event.
|
|
151
|
+
- `summary.json`: totals and settings, with a `format` version (currently 1).
|
|
152
|
+
- `transcript.md`: the task, system prompt and each turn as the agent saw it.
|
|
153
|
+
- `final-state.md`: the final state of the workspace (see Evaluation).
|
|
154
|
+
- `evaluation.json` and `friction.md`: the evaluator's verdict and friction report. The verdict is also copied into `summary.json` under `evaluation`.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
Apache-2.0
|
package/dist/claude.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The claude-code backend: each call is a fresh `claude -p` process using the user's Claude Code login.
|
|
2
|
+
// It runs with no tools, no settings files (so no CLAUDE.md, memory, hooks or plugins), no MCP servers and no saved
|
|
3
|
+
// session. Claude Code still adds a short preamble and an environment block (cwd, platform, date) to the system
|
|
4
|
+
// prompt, about 740 tokens; `--bare` would remove them but only works with an API key, not a subscription login.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { Interrupted, retryWait, RETRY_DELAYS_S } from "./model.js";
|
|
10
|
+
const CALL_TIMEOUT_S = 600;
|
|
11
|
+
/** OpenRouter-style ids (anthropic/claude-haiku-5.5) to Claude Code ids (claude-haiku-5-5); aliases pass through. */
|
|
12
|
+
export function claudeModelName(model) {
|
|
13
|
+
return model.replace(/^anthropic\//, "").replace(/(\d+)\.(\d+)/g, "$1-$2");
|
|
14
|
+
}
|
|
15
|
+
let scratchDir = null;
|
|
16
|
+
/** An empty directory to run `claude` in when no better cwd is given, removed when gentl exits. */
|
|
17
|
+
function emptyDir() {
|
|
18
|
+
if (!scratchDir) {
|
|
19
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gentl-claude-"));
|
|
20
|
+
scratchDir = dir;
|
|
21
|
+
process.on("exit", () => fs.rmSync(dir, { recursive: true, force: true }));
|
|
22
|
+
}
|
|
23
|
+
return scratchDir;
|
|
24
|
+
}
|
|
25
|
+
export class ClaudeCodeClient {
|
|
26
|
+
schema;
|
|
27
|
+
log;
|
|
28
|
+
cwd;
|
|
29
|
+
structured = true;
|
|
30
|
+
model;
|
|
31
|
+
constructor(model, schema, log, cwd) {
|
|
32
|
+
this.schema = schema;
|
|
33
|
+
this.log = log;
|
|
34
|
+
this.cwd = cwd;
|
|
35
|
+
this.model = claudeModelName(model);
|
|
36
|
+
}
|
|
37
|
+
async call(system, user, signal) {
|
|
38
|
+
for (let attempt = 0;; attempt++) {
|
|
39
|
+
const r = await this.once(system, user, signal, this.structured);
|
|
40
|
+
if (r.ok)
|
|
41
|
+
return r.reply;
|
|
42
|
+
// Claude Code gave up producing output that matches the schema. Retrying the same call tends to give a
|
|
43
|
+
// degenerate answer, so retry this one call as plain text (the parser finds the JSON object).
|
|
44
|
+
if (this.structured && /structured_output/i.test(r.error)) {
|
|
45
|
+
this.log(`structured output failed for this call (${r.error}); retrying it as plain text`);
|
|
46
|
+
const plain = await this.once(system, user, signal, false);
|
|
47
|
+
if (plain.ok)
|
|
48
|
+
return plain.reply;
|
|
49
|
+
}
|
|
50
|
+
if (/not logged in|\/login/i.test(r.error))
|
|
51
|
+
throw new Error(`claude: ${r.error} (log in to Claude Code first)`);
|
|
52
|
+
if (this.structured && /schema/i.test(r.error)) {
|
|
53
|
+
this.structured = false;
|
|
54
|
+
this.log(`structured output failed (${r.error.slice(0, 200)}); using plain text from now on`);
|
|
55
|
+
attempt--;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (attempt >= RETRY_DELAYS_S.length)
|
|
59
|
+
throw new Error(`claude call failed: ${r.error.slice(0, 500)}`);
|
|
60
|
+
await retryWait(attempt, `claude call failed: ${r.error.slice(0, 200)}`, signal, this.log);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
once(system, user, signal, structured) {
|
|
64
|
+
const args = ["-p", "--system-prompt", system, "--tools", "", "--output-format", "json", "--no-session-persistence",
|
|
65
|
+
"--setting-sources", "", "--strict-mcp-config", "--disable-slash-commands", "--model", this.model];
|
|
66
|
+
// Claude Code's default effort unless GENTL_CLAUDE_EFFORT is set (low, medium, high, ...).
|
|
67
|
+
if (process.env.GENTL_CLAUDE_EFFORT)
|
|
68
|
+
args.push("--effort", process.env.GENTL_CLAUDE_EFFORT);
|
|
69
|
+
if (structured)
|
|
70
|
+
args.push("--json-schema", JSON.stringify(this.schema.schema));
|
|
71
|
+
const cwd = this.cwd && fs.existsSync(this.cwd) ? this.cwd : emptyDir();
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const child = spawn("claude", args, { cwd, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
74
|
+
let out = "";
|
|
75
|
+
let err = "";
|
|
76
|
+
child.stdout.on("data", (b) => (out += b.toString("utf8")));
|
|
77
|
+
child.stderr.on("data", (b) => (err += b.toString("utf8")));
|
|
78
|
+
const kill = () => {
|
|
79
|
+
try {
|
|
80
|
+
process.kill(-child.pid, "SIGKILL");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// already gone
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(kill, CALL_TIMEOUT_S * 1000);
|
|
87
|
+
const onAbort = () => kill();
|
|
88
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
89
|
+
child.on("error", (e) => resolve({ ok: false, error: `could not run claude: ${e.message}` }));
|
|
90
|
+
child.on("close", () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
signal.removeEventListener("abort", onAbort);
|
|
93
|
+
if (signal.aborted)
|
|
94
|
+
return reject(new Interrupted());
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(out);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return resolve({ ok: false, error: (err || out || "no output").trim() });
|
|
101
|
+
}
|
|
102
|
+
// The output is a single result object, or (with verbose settings) an array of messages ending in one.
|
|
103
|
+
const r = Array.isArray(parsed) ? parsed.find((m) => m?.type === "result") : parsed;
|
|
104
|
+
if (!r)
|
|
105
|
+
return resolve({ ok: false, error: "no result in claude output" });
|
|
106
|
+
if (r.is_error)
|
|
107
|
+
return resolve({ ok: false, error: String(r.result ?? r.subtype ?? "error") });
|
|
108
|
+
const text = r.structured_output !== undefined ? JSON.stringify(r.structured_output) : String(r.result ?? "");
|
|
109
|
+
const u = r.usage ?? {};
|
|
110
|
+
resolve({ ok: true, reply: { text, usage: {
|
|
111
|
+
prompt_tokens: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
|
|
112
|
+
completion_tokens: u.output_tokens ?? 0,
|
|
113
|
+
cost: r.total_cost_usd ?? 0, // notional: billed to the subscription
|
|
114
|
+
claude: u,
|
|
115
|
+
} } });
|
|
116
|
+
});
|
|
117
|
+
child.stdin.end(user);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|