done-done-axi 0.4.2 → 0.4.4
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 +45 -13
- package/dist/cli.js +127 -26
- package/dist/config.js +7 -3
- package/dist/lint.js +53 -13
- package/dist/look.js +1 -1
- package/docs/.nojekyll +0 -0
- package/docs/index.html +140 -0
- package/docs/logo.svg +1 -0
- package/docs/media/hero-dark.svg +17 -17
- package/docs/media/hero-light.svg +17 -17
- package/docs/media/pass-dark.svg +3 -3
- package/docs/media/pass-light.svg +3 -3
- package/docs/media/reasons-dark.svg +4 -4
- package/docs/media/reasons-light.svg +4 -4
- package/docs/media/run-dark.svg +5 -5
- package/docs/media/run-light.svg +5 -5
- package/docs/reference.md +42 -16
- package/integrations/firstmate/brief-include.md +1 -1
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +10 -1
- package/docs/media/wordmark-dark.svg +0 -7
- package/docs/media/wordmark-light.svg +0 -7
package/README.md
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
</picture>
|
|
3
|
+
<img src="docs/logo.svg" width="72" height="72" alt="done-done logo">
|
|
4
|
+
|
|
5
|
+
# done-done
|
|
7
6
|
|
|
8
7
|
### No proof, no "done".
|
|
9
8
|
|
|
9
|
+
**[Landing page](https://umeranjum17.github.io/done-done-axi/)**
|
|
10
|
+
|
|
10
11
|
done-done won't accept "done" without real, fresh proof from your product, captured at the exact commit being called done.
|
|
11
12
|
|
|
12
13
|
[](https://nodejs.org) [](#whats-next) [](https://www.npmjs.com/package/done-done-axi)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
## Install in 3 steps
|
|
18
|
+
|
|
19
|
+
You need Node 22.18 or later and git.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install -g done-done-axi
|
|
23
|
+
cd your-app && done-done init
|
|
24
|
+
done-done doctor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
1. **Install.** This puts `done-done` (and the long name `done-done-axi`) on your PATH. No `sudo`? Add `--prefix ~/.local` and put `~/.local/bin` on your `PATH`.
|
|
28
|
+
2. **Set up your repo.** `init` writes a minimal `.done-done.yaml` and asks you, in a terminal, which coding agent and which judge to use. done-done never picks either for you. Outside a terminal, pass `--agent <agent> --model <model> --judge <agent> --judge-model <model>` instead.
|
|
29
|
+
3. **Check the setup.** `doctor` changes nothing and names each missing piece with the exact command that fixes it.
|
|
30
|
+
|
|
31
|
+
Then prove something: `done-done verify --outcome "<what a user can now do>"`. [Manual CLI](#manual-cli) below walks through journeys, `check` and `receipt`.
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
15
34
|
|
|
16
35
|
<picture>
|
|
17
36
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/flow-dark.svg">
|
|
@@ -59,7 +78,7 @@ npx skills add umeranjum17/done-done-axi --skill done-done -a <agent> -y
|
|
|
59
78
|
|
|
60
79
|
`<agent>` is `claude-code`, `codex`, `opencode`, `pi` or `gemini-cli`. `-a` and `-y` skip the installer's four prompts and keep it from writing skill folders for agents you do not use.
|
|
61
80
|
|
|
62
|
-
The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.agents/skills/done-done/` for Codex, opencode, Pi and Gemini CLI) next to a `skills-lock.json`: commit both so teammates
|
|
81
|
+
The skill lands in this project (`.claude/skills/done-done/` for Claude Code, `.agents/skills/done-done/` for Codex, opencode, Pi and Gemini CLI) next to a `skills-lock.json`: commit both so your teammates get it too. Or add `-g` to install it once for all your projects (for example `~/.claude/skills/` for Claude Code) and leave the project untouched.
|
|
63
82
|
|
|
64
83
|
That is the entire setup - no npm install needed.
|
|
65
84
|
The skill tells your agent to use `done-done` if it is on PATH and otherwise run the npm package through `npx`, so the CLI comes along on demand:
|
|
@@ -68,7 +87,7 @@ The skill tells your agent to use `done-done` if it is on PATH and otherwise run
|
|
|
68
87
|
npx -y done-done-axi --help
|
|
69
88
|
```
|
|
70
89
|
|
|
71
|
-
The skill
|
|
90
|
+
The skill is a short stub and sends the agent to `--help` and the bare command for current instructions, so an installed copy cannot go stale against a newer CLI.
|
|
72
91
|
|
|
73
92
|
Then, in agents that expose skills as slash commands (Claude Code, for example), invoke it directly:
|
|
74
93
|
|
|
@@ -101,7 +120,7 @@ cd your-app
|
|
|
101
120
|
done-done init
|
|
102
121
|
```
|
|
103
122
|
|
|
104
|
-
`init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app, and a commented global `~/.done-done/config.yaml` (only if there is none). done-done never picks the coding agent it starts: `init` asks you in a terminal, or takes `--agent <agent> --model <model>`. It never picks the judge either, the agent of its own that `check`, `receipt`, `verify` and `design` always start to look at each capture: `init` asks in a terminal, or takes `--judge <agent> --judge-model <model>`. Any agent and model you choose runs, your coding agent's own too (the judge is a fresh session of its own). The config keys are in the [reference](docs/reference.md#config). A plain `init` never overwrites an existing config.
|
|
123
|
+
`init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app, and a commented global `~/.done-done/config.yaml` (only if there is none). done-done never picks the coding agent it starts: `init` asks you in a terminal, or takes `--agent <agent> --model <model>`. It never picks the judge either, the agent of its own that `check`, `receipt`, `verify` and `design` always start to look at each capture: `init` asks in a terminal, or takes `--judge <agent> --judge-model <model>`. Any agent and model you choose runs, your coding agent's own too (the judge is a fresh session of its own). The config keys are in the [reference](docs/reference.md#config). A plain `init` never overwrites an existing config. When your coding agent is set, `init` also has it write a verify skill, `.agents/skills/done-done-verify-<app>/SKILL.md`, from the repo's own files (never overwriting one; with no agent set, no skill is written; an older `verify-<app>` skill is renamed to this once). Check its launch never touches real accounts or your own state. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. For an `answerer:pi` judge with no explicit model, done-done uses `openai-codex/gpt-6-luna`; `judge.model` or `agent_config.pi.model` overrides it. Then describe how to capture proof — swap the command for whatever drives your real app and saves screenshots, recordings or logs:
|
|
105
124
|
|
|
106
125
|
```yaml
|
|
107
126
|
journeys:
|
|
@@ -158,18 +177,29 @@ Run `done-done` with no arguments to see the current config and the next step.
|
|
|
158
177
|
- ⏱️ **Fresh**: Proof from an older commit fails. Commits that only touch docs can be set to keep it fresh.
|
|
159
178
|
- ♻️ **Not reused**: Proof made only of files already used for a different claim fails, and so does citing the same file twice. A claim is named by `--claim-id <id>`, which defaults to your branch, so rewording it keeps its proof. Several claims on one branch each need their own `--claim-id`.
|
|
160
179
|
- 🧾 **Honest claims**: Reads a pull request's `## Checks` block — from a file, a URL, or stdin — and makes sure every line is filled in.
|
|
161
|
-
- 📌 **Verifiable later**: `receipt` records a passing check against the exact commit and the sha256 of every file it cited; `receipt --verify` re-checks it from any checkout of that commit on the same machine (a merge gate or a fresh clone)
|
|
180
|
+
- 📌 **Verifiable later**: `receipt` records a passing check against the exact commit and the sha256 of every file it cited; `receipt --verify` re-checks it from any checkout of that commit on the same machine (a merge gate or a fresh clone) ([how](docs/reference.md#where-proof-lives)).
|
|
162
181
|
- 👀 **Shows the outcome** (always): `check`, `receipt` and `verify` start done-done's own judge, a fresh, tools-off agent on your own plan or key (Claude Code, opencode, pi or Gemini CLI, or your own OpenAI API key), to say whether each capture is the right screen for the claim (and each text capture, such as a log, JSON or CLI output, the right output), and a wrong one fails. There is no way to turn it off: done-done gives no result without it. The checks above never pass on their own, and a check that cites only files the judge cannot read (a video) stops before the judge, with exit 2. You choose the judge route (`done-done init --judge <agent> --judge-model <model>`, or pick it from the list done-done shows in a terminal; `done-done check --judge <route>` overrides it for one call); for a Pi judge with no explicit model, done-done defaults to `openai-codex/gpt-6-luna` unless `agent_config.pi.model` is set. Any agent and model you choose runs, your coding agent's own too. A judge that fails or times out stops the command with its cause (exit 2; a `verify` run stops with exit 2 and its cause in the run record), and no receipt is written.
|
|
163
182
|
- 🎨 **Looks right**: `done-done design <screens...> [--mock <screen>=<mock>]` has the same judge look at each captured screen, alone or next to its approved mock, and fails it on stretched or squashed images, clipped or squashed text, overlap, elements off-screen, blank or black strips and broken layout, naming each defect and where it is ([how](docs/reference.md#design-check)).
|
|
164
183
|
- 📐 **Fits the screen**: `done-done look <url>` opens a web page in headless Chrome at a 390x844 phone viewport, captures it, and fails the journey that calls it on squashed text, clipped text, elements cut off by an edge or a page wider than the screen, naming each element, with no model ([how](docs/reference.md#look-check)).
|
|
165
184
|
- 🧮 **Honest, cheap claim gate**: `done-done lint <PR URL | report file> <evidence folder>` is a no-model gate on a worker's `## Checks` block, on whenever you call it. It prints `PASS` or `FAIL: <reason>` and exits 1 on a fail: the four lines present, proof naming a command or path, a real repro on a fix PR, evidence files that exist, are non-empty, are newer than the evidence folder and differ from each other, at least one never used by another PR (hashes kept in `~/.done-done/lint.hashes`, every run logged to `lint.log` there), and under 1,000 added non-generated lines ([how](docs/reference.md#lint-check)). `evidence: not proven: <cause>` passes.
|
|
185
|
+
- 🚦 **CI gate, no model**: `done-done gate [<head>]` passes when a receipt committed under `.done-done-receipts/` covers the head, with no judge, config, key or evidence files. CI never runs `check`, `run` or `verify` and never gets a model key; proof runs on your desktop ([how](docs/reference.md#ci-gate)).
|
|
166
186
|
- 🧩 **Page is clean** (with a `dom` block): each captured screen also needs its page-check record. A page that overflows the viewport, logs a console error, has a failed request or shows squashed, clipped or cut-off UI fails, unless an allow list names it ([how](docs/reference.md#dom-assertions)).
|
|
167
|
-
- 🔒 **Safe in CI**: `--trusted-ref origin/main` reads the rules from your main branch, so a pull request can't loosen them.
|
|
168
187
|
|
|
169
188
|
Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error (including no agent or judge set, a check that fails before its judge, a capture the judge cannot be sent, or a judge that did not answer: never a result), **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
|
|
170
189
|
|
|
190
|
+
## Alone, and with the other tools
|
|
191
|
+
|
|
192
|
+
done-done works on its own: the CLI and the skill need nothing else. It is one of three tools that each do one job and connect through small written contracts ([Vision](VISION.md)):
|
|
193
|
+
|
|
194
|
+
- [`sign-off-axi`](https://www.npmjs.com/package/sign-off-axi): the board where agents ask you questions. `done-done signoff` writes a run's gate (when the judge was unsure) or its evidence in sign-off's message format and prints the post command; it never posts. Without sign-off you answer at the terminal with `done-done respond`.
|
|
195
|
+
- [`learn-from-mistakes-axi`](https://www.npmjs.com/package/learn-from-mistakes-axi): the retro tool that finds mistakes your agents keep repeating, with evidence. Neither tool needs the other.
|
|
196
|
+
- [`first-draft-axi`](https://www.npmjs.com/package/first-draft-axi): turns a prompt into design mocks. Pass a mock to `done-done design <screen> --mock <screen>=<mock>` to judge a captured screen next to it.
|
|
197
|
+
|
|
171
198
|
## Use with Firstmate or another agent fleet
|
|
172
199
|
|
|
200
|
+
done-done works the same with or without Firstmate: nothing below is needed to use it, and the skill and CLI behave identically inside or outside a Firstmate home.
|
|
201
|
+
|
|
202
|
+
|
|
173
203
|
[Firstmate](https://github.com/kunchenguid/firstmate) hands tasks to agent workers and merges their work. done-done ships what those workers need to prove each task, and what the merge needs to check that proof, in [`integrations/firstmate/`](integrations/firstmate/).
|
|
174
204
|
|
|
175
205
|
Install the skill once per machine for your workers' agent (the same `-a` values), install the CLI from npm, and copy two files from it into your Firstmate home:
|
|
@@ -195,7 +225,7 @@ Another fleet works the same way: add the include text to whatever your workers
|
|
|
195
225
|
|
|
196
226
|
## What's next
|
|
197
227
|
|
|
198
|
-
So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
|
|
228
|
+
So far: `init`, `doctor`, `run` (including `--open` and `--close`), `check`, `receipt`, `lint` (a no-model gate on a Checks block), `gate` (a no-model CI check of a committed receipt), `verify` (by journey or by your coding agent, with a gate you answer with `respond`), `maintain`, the capture judge (always on), the `design` visual check, the `look` step, the run tools (`status`, `attach`, `logs`, `rerun`, `abort`, `signoff`, `history`) and the opt-in session hooks for Claude Code, Codex, opencode, Gemini CLI and pi. Coming next: help writing the proof steps for your app.
|
|
199
229
|
|
|
200
230
|
## Develop
|
|
201
231
|
|
|
@@ -206,7 +236,7 @@ npm install
|
|
|
206
236
|
npm install -g --prefix ~/.local . # optional: links this checkout as done-done on your PATH
|
|
207
237
|
npm test # builds, then runs the end-to-end tests against throwaway git repos
|
|
208
238
|
npm run build:skill # regenerate skills/done-done/SKILL.md from src/skill.ts
|
|
209
|
-
npm run build:skill -- --check # fail if the committed skill is out of date
|
|
239
|
+
npm run build:skill -- --check # fail if the committed skill is out of date
|
|
210
240
|
```
|
|
211
241
|
|
|
212
242
|
To try an unreleased commit without cloning, run it straight from GitHub (npm 12 needs `--allow-git=all`):
|
|
@@ -217,4 +247,6 @@ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
|
|
|
217
247
|
|
|
218
248
|
`dist/` is committed, so installing from GitHub needs no build step. `npm test` rebuilds it and fails while the rebuilt `dist/` differs from the commit or has files git does not track, so `git add` the new `dist/` files with your change.
|
|
219
249
|
|
|
220
|
-
The terminal pictures (hero, run, pass, reasons, config) are real output from the current build, with a stub judge (no model); redraw them with `npx tsc && node docs/media/make.mjs`. The
|
|
250
|
+
The terminal pictures (hero, run, pass, reasons, config) are real output from the current build, with a stub judge (no model); redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
|
|
251
|
+
|
|
252
|
+
The landing page ([`docs/index.html`](docs/index.html), served by GitHub Pages from `main` `/docs`) is static: change it in the same PR as any README change it repeats (install steps, what it checks, the other tools).
|
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ const JOURNEYS_TEXT = `journeys (.done-done.yaml): the command runs with DONE_DO
|
|
|
34
34
|
const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then these four keys, each at the start of a line:
|
|
35
35
|
outcome: <what a user can now do>
|
|
36
36
|
repro: <command that failed before and passes now, or n/a; a fix PR needs one>
|
|
37
|
-
proof: <the command or path that drove the real product; name the repo's verify-<app> skill, or say "no verify skill">
|
|
37
|
+
proof: <the command or path that drove the real product; name the repo's done-done-verify-<app> skill, or say "no verify skill">
|
|
38
38
|
evidence: <the evidence file paths, or "not proven: <cause>">`;
|
|
39
39
|
const RUNS_TEXT = `runs: each verify or maintain run lives in $DONE_DONE_HOME/runs/<app>/<run-id> (run.json, events.jsonl, logs/<step>.log,
|
|
40
40
|
agents/<n>-<name>.json for each agent call) and goes on when the command that started it ends;
|
|
@@ -53,7 +53,7 @@ const RUNS_TEXT = `runs: each verify or maintain run lives in $DONE_DONE_HOME/ru
|
|
|
53
53
|
ignored files copied in, and verify runs again there, up to n times. Result: changed (the fix on a done-done/verify-<run-id>
|
|
54
54
|
branch, your HEAD still fails; exit 1) or blocked (dirty checkout, a guard, or every attempt used; exit 1)`;
|
|
55
55
|
const MAINTAIN_TEXT = `maintain reads the verify skill and the source; HEAD, git status and the content of changed files must be the same before and after.
|
|
56
|
-
locate: the verify skill (.agents/skills/verify-<app>/SKILL.md); none: blocked, create it with done-done init
|
|
56
|
+
locate: the verify skill (.agents/skills/done-done-verify-<app>/SKILL.md); none: blocked, create it with done-done init
|
|
57
57
|
hygiene: every local link in the skill resolves; features/README.md lists each features/*.md file once
|
|
58
58
|
source-wave: one fresh read-only reader per feature file at the configured agent (agent, agent_config) explains it from source
|
|
59
59
|
reconcile: each file:line the skill or a reader cites must exist (a bare file name matches a tracked file); one tool-less call
|
|
@@ -73,7 +73,7 @@ const DOCTOR_TEXT = `doctor changes nothing except the --probe call record (the
|
|
|
73
73
|
for claude-code an executable claude binary and the judge kit, for answerer:<agent> that agent's CLI and the
|
|
74
74
|
kit, for openai:key OPENAI_API_KEY and the kit; doctor does not call the model; missing when none is set, with the command
|
|
75
75
|
that saves the user's choice).
|
|
76
|
-
also listed: verify_skill (.agents/skills/verify-<app>/), agent_settings (agent, model, effort,
|
|
76
|
+
also listed: verify_skill (.agents/skills/done-done-verify-<app>/), agent_settings (agent, model, effort,
|
|
77
77
|
args, path and agent_timeout, each with the file it came from: the global config, .done-done.yaml or default),
|
|
78
78
|
judge_choice (the judge, and how far it is from the author: another agent, another model, or session-only), ffprobe (video evidence only).
|
|
79
79
|
--probe also asks the agent one JSON-schema question (one call at the agent's model: its cheap default where it has one, else its own; set agent_config.<agent>.model to choose; its record goes to
|
|
@@ -93,7 +93,7 @@ const FLAGS = {
|
|
|
93
93
|
'--close <run-id>': 'seal an open run and record the files in its folder',
|
|
94
94
|
'--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
|
|
95
95
|
'--from <file | - | PR URL>': 'read the claim from the newest "## Checks" block of a file, stdin or a PR body',
|
|
96
|
-
'--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (
|
|
96
|
+
'--trusted-ref <ref>': 'read .done-done.yaml from this git ref instead of the working tree (for scripts)',
|
|
97
97
|
'--judge <route>': 'check with this judge for this call instead of judge.route (same form: claude-code, answerer:opencode, answerer:pi, answerer:gemini or openai:key); its model is that agent\'s default, or judge.model when the route is the configured one',
|
|
98
98
|
'--citation-base <dir>': 'resolve evidence citations against this directory (default: cwd); absolute citations stay unchanged; does not change repo, config or run storage',
|
|
99
99
|
'--claim-id <id>': 'the claim the evidence proves; evidence already used for another claim id is reuse (default: the current branch, or the PR branch with --from <PR URL>; required on a detached HEAD)',
|
|
@@ -101,7 +101,7 @@ const FLAGS = {
|
|
|
101
101
|
'--outcome "<what a user can now do>"': 'the outcome the run must prove',
|
|
102
102
|
'--wait': 'wait until the run ends (the default)',
|
|
103
103
|
'--no-wait': 'start the run and return at once (exit 4); read it later with done-done status',
|
|
104
|
-
'--foreground': 'run every step in this process instead of a detached runner (
|
|
104
|
+
'--foreground': 'run every step in this process instead of a detached runner (for scripts)',
|
|
105
105
|
'--run <run-id>': 'the run to show (default: the newest run of this checkout)',
|
|
106
106
|
'--probe': 'ask the configured agent one JSON-schema question and require a valid answer (one call at the agent\'s model: its cheap default where it has one, else its own; set agent_config.<agent>.model to choose)',
|
|
107
107
|
'--mock <screen>=<mock>': 'judge this screen next to its approved mock (repeat per screen; the screen is judged even when not listed)',
|
|
@@ -122,7 +122,7 @@ const COMMANDS = {
|
|
|
122
122
|
init: {
|
|
123
123
|
usage: [
|
|
124
124
|
['done-done init', 'write a minimal .done-done.yaml (evidence lives outside the repo) and, when absent, a commented ~/.done-done/config.yaml'],
|
|
125
|
-
['done-done init', 'have the agent write .agents/skills/verify-<app> from this repo\'s own proof paths (never overwrites; no agent, no skill)'],
|
|
125
|
+
['done-done init', 'have the agent write .agents/skills/done-done-verify-<app> from this repo\'s own proof paths (never overwrites; no agent, no skill)'],
|
|
126
126
|
['done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor', 'set the coding agent done-done starts and its model (in a terminal, init asks when none is set)'],
|
|
127
127
|
['done-done init --judge pi --judge-model openai-codex/gpt-6-luna', 'set the judge done-done starts and its model (in a terminal, init asks when none is set)'],
|
|
128
128
|
['done-done init --hook claude', 'also show this repo\'s done-done state at each Claude Code session start'],
|
|
@@ -174,7 +174,7 @@ const COMMANDS = {
|
|
|
174
174
|
usage: [
|
|
175
175
|
['done-done verify [<journey>] --outcome "<what a user can now do>" [--claim-id <id>]', 'drive the journey in a run that outlives this command, check its evidence, judge each capture and record a receipt'],
|
|
176
176
|
['done-done verify --outcome "<claim>" [--feature <name>]', 'with no journey, the author agent drives a verify skill feature and the judge the user set judges its captures'],
|
|
177
|
-
['done-done verify [<journey>] --outcome "<claim>" --no-wait | --wait | --foreground', 'return at once, wait until the run ends or waits at its gate (default), or run in this process
|
|
177
|
+
['done-done verify [<journey>] --outcome "<claim>" --no-wait | --wait | --foreground', 'return at once, wait until the run ends or waits at its gate (default), or run in this process'],
|
|
178
178
|
],
|
|
179
179
|
flags: ['--outcome "<what a user can now do>"', '--feature <name>', '--claim-id <id>', '--wait', '--no-wait', '--foreground'],
|
|
180
180
|
more: [RUNS_TEXT],
|
|
@@ -198,16 +198,27 @@ const COMMANDS = {
|
|
|
198
198
|
overflow, squashed text, text clipped by its container and elements cut off by the left, right or top edge, writes the
|
|
199
199
|
screenshot and its .dom.json record, and exits 1 naming each element, kind and text, 0 when there is none. A hit always
|
|
200
200
|
fails: never wrap it in || true. Needs Chrome or Chromium (DONE_DONE_CHROME names the binary)`],
|
|
201
|
+
},
|
|
202
|
+
gate: {
|
|
203
|
+
usage: [['done-done gate [<head>] [--dir <folder>] [--pushed <time>]', 'no-model check for a hosted runner: PASS or FAIL: <reason> for a receipt committed under .done-done-receipts/']],
|
|
204
|
+
flags: ['--dir <folder>', '--pushed <time>'],
|
|
205
|
+
more: [`gate: reads git objects only, so it needs no config, judge, model key or evidence files, and a hosted runner never runs check, run or verify.
|
|
206
|
+
Copy the receipt that receipt wrote (<evidence root>/.done-done/receipts/<head>.json) into .done-done-receipts/ and commit it in a follow-up commit.
|
|
207
|
+
PASS when a receipt there is a well-formed judged pass, its head is <head> (default HEAD) or an ancestor of it, every file changed since that head is
|
|
208
|
+
inside the folder, and its time is not after the push (--pushed, default the commit time of <head>). It does not re-hash evidence: that is receipt --verify
|
|
209
|
+
on the machine that holds it. Needs the history back to the receipt head (fetch-depth: 0). Exit 0 PASS, 1 FAIL, 2 usage error`],
|
|
201
210
|
},
|
|
202
211
|
lint: {
|
|
203
|
-
usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS or FAIL: <reason>; the rules need no judge
|
|
212
|
+
usage: [['done-done lint <PR URL | report file> <evidence folder>', 'merge gate on a worker\'s claim: prints PASS or FAIL: <reason>; the rules need no judge or model, and two optional size rules are config keys']],
|
|
204
213
|
flags: [],
|
|
205
214
|
more: [`lint: a deterministic gate that is on whenever you call it. The newest "## Checks" block of the PR body (gh pr view) or report file needs
|
|
206
|
-
outcome:, repro:, proof: and evidence: lines. proof names a command or path, and the repo's verify-<app> skill, or says "no verify skill"
|
|
215
|
+
outcome:, repro:, proof: and evidence: lines. proof names a command or path, and the repo's done-done-verify-<app> skill, or says "no verify skill"
|
|
207
216
|
when the repo (the current folder) has none; repro is not n/a on a fix PR. evidence names files in the evidence folder (or absolute):
|
|
208
217
|
each exists, is non-empty and is newer than the folder itself; no two are byte-identical; at least one has a hash no other PR used
|
|
209
218
|
(hash store lint.hashes in ~/.done-done, or $DONE_DONE_HOME). "evidence: not proven: <cause>" passes. A PR with 1,000 or more added
|
|
210
219
|
non-generated lines fails (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
|
|
220
|
+
In .done-done.yaml, checks.upstream_catch_up: true also drops a fork's upstream catch-up lines, and checks.moved_from: true drops lines
|
|
221
|
+
a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
|
|
211
222
|
Every run is appended to lint.log next to the store. Exit 0 PASS, 1 FAIL, 2 usage error. Needs gh for a PR URL`],
|
|
212
223
|
},
|
|
213
224
|
status: {
|
|
@@ -572,7 +583,7 @@ function claimFromChecks(body, title, repo, cfg, out) {
|
|
|
572
583
|
out.push({ level: 'fail', why: 'evidence names no files' });
|
|
573
584
|
return { outcome: f.outcome, files, title };
|
|
574
585
|
}
|
|
575
|
-
// A PR body also names its head branch, which is the claim a
|
|
586
|
+
// A PR body also names its head branch, which is the claim a detached-HEAD checkout is checking.
|
|
576
587
|
function readSource(from) {
|
|
577
588
|
if (from === '-')
|
|
578
589
|
return { body: readFileSync(0, 'utf8') };
|
|
@@ -823,7 +834,7 @@ async function cmdCheck(argv) {
|
|
|
823
834
|
}
|
|
824
835
|
const receiptsDir = (cfg) => join(cfg.evidence.root, '.done-done', 'receipts');
|
|
825
836
|
// A receipt names a cited file relative to the repo, or to an evidence root outside it under this prefix,
|
|
826
|
-
// so any checkout of the repo
|
|
837
|
+
// so any checkout of the repo finds the same file.
|
|
827
838
|
const IN_EVIDENCE = '<evidence root>/';
|
|
828
839
|
function receiptPath(repo, cfg, p) {
|
|
829
840
|
const inRepo = under(repo, p);
|
|
@@ -839,6 +850,17 @@ function writeReceipt(cfg, rec) {
|
|
|
839
850
|
renameSync(`${path}.tmp`, path);
|
|
840
851
|
return path;
|
|
841
852
|
}
|
|
853
|
+
// Why a parsed receipt is not a well-formed passing one (a judge block that names its judge and passes only captures the receipt cites), or null.
|
|
854
|
+
function receiptFault(rec, short) {
|
|
855
|
+
if (!rec || typeof rec !== 'object' || rec.schema !== 2 || rec.result !== 'pass' || !Array.isArray(rec.files) || !rec.files.length)
|
|
856
|
+
return `receipt for ${short} is malformed`;
|
|
857
|
+
if (!rec.files.every((f) => f && typeof f === 'object' && typeof f.path === 'string' && typeof f.sha256 === 'string'))
|
|
858
|
+
return `receipt for ${short} is malformed`;
|
|
859
|
+
const j = rec.judge;
|
|
860
|
+
const badJudge = !j || typeof j !== 'object' || typeof j.route !== 'string' || typeof j.model !== 'string' || !['agent', 'model', 'session-only'].includes(j.independence) || !Array.isArray(j.verdicts) || !j.verdicts.length
|
|
861
|
+
|| !j.verdicts.every((v) => v && v.verdict === 'pass' && rec.files.some((f) => f.path === v.path && f.sha256 === v.sha256));
|
|
862
|
+
return badJudge ? `receipt for ${short} has a malformed judge block` : null;
|
|
863
|
+
}
|
|
842
864
|
// Verify a recorded receipt for `headArg`, which may be a full sha or a ref (HEAD, a branch or a tag).
|
|
843
865
|
// Read-only, so a missing or changed record is a failure, not a usage error.
|
|
844
866
|
function verifyReceipt(repo, cfg, headArg) {
|
|
@@ -861,16 +883,10 @@ function verifyReceipt(repo, cfg, headArg) {
|
|
|
861
883
|
catch (e) {
|
|
862
884
|
return fail(`receipt for ${short} is malformed (${e.message})`);
|
|
863
885
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
return fail(`receipt for ${short} is malformed`);
|
|
868
|
-
// The judge block must name its judge, and pass only captures this receipt cites.
|
|
886
|
+
const fault = receiptFault(rec, short);
|
|
887
|
+
if (fault)
|
|
888
|
+
return fail(fault);
|
|
869
889
|
const j = rec.judge;
|
|
870
|
-
const badJudge = !j || typeof j !== 'object' || typeof j.route !== 'string' || typeof j.model !== 'string' || !['agent', 'model', 'session-only'].includes(j.independence) || !Array.isArray(j.verdicts) || !j.verdicts.length
|
|
871
|
-
|| !j.verdicts.every((v) => v && v.verdict === 'pass' && rec.files.some((f) => f.path === v.path && f.sha256 === v.sha256));
|
|
872
|
-
if (badJudge)
|
|
873
|
-
return fail(`receipt for ${short} has a malformed judge block`);
|
|
874
890
|
if (rec.head !== commit)
|
|
875
891
|
return fail(`receipt records head ${String(rec.head).slice(0, 12)}, not ${short}`);
|
|
876
892
|
const now = head(repo);
|
|
@@ -935,7 +951,7 @@ function recordReceipt(repo, cfg, r) {
|
|
|
935
951
|
const rec = { schema: 2, repo: cfg.app, head: r.head.commit, tree: r.head.tree, claim: r.claim ?? '', result: 'pass', at: new Date().toISOString(), files: r.cited, judge: r.judgeBlock };
|
|
936
952
|
const path = writeReceipt(cfg, rec);
|
|
937
953
|
const result = { command: 'receipt', result: 'pass', head: r.head.commit, tree: r.head.tree, claim: rec.claim, files: rec.files.length, receipt: shown(repo, path) };
|
|
938
|
-
return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (
|
|
954
|
+
return { code: PASS, result: { ...result, ...(r.judge && { judge: r.judge }), help: [`Verify this receipt (merge gate): done-done receipt --verify ${r.head.commit}`] } };
|
|
939
955
|
}
|
|
940
956
|
// Why evidence from `from` no longer proves `to`, or '' if it still does.
|
|
941
957
|
function staleness(repo, cfg, from, to) {
|
|
@@ -961,10 +977,10 @@ function checkHelp(code, findings, files, { cfg, repo, claim, source, citationBa
|
|
|
961
977
|
if (judgeLevel && code === INCONCLUSIVE)
|
|
962
978
|
return ['Look at the capture yourself, or capture a clearer screen of the outcome and re-run this check'];
|
|
963
979
|
if (code === PASS) {
|
|
964
|
-
// The
|
|
980
|
+
// The merge gate needs a PR on a remote; without one, a receipt records the pass locally.
|
|
965
981
|
const citationArgs = citationBase === undefined ? '' : ` --citation-base ${arg(citationBase)}`;
|
|
966
982
|
if (spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: repo }).status === 0)
|
|
967
|
-
return [`Gate
|
|
983
|
+
return [`Gate the merge on this proof: done-done check --from <PR URL> --trusted-ref origin/main${citationArgs}`];
|
|
968
984
|
const claimArgs = source ? ['--from', source] : ['--outcome', claim ?? '', ...files];
|
|
969
985
|
return [`Record this pass for HEAD: done-done receipt ${claimArgs.map(arg).join(' ')}${citationArgs}`];
|
|
970
986
|
}
|
|
@@ -1892,7 +1908,7 @@ function showRun(dir, cfg, command, gone = false) {
|
|
|
1892
1908
|
: /^locate: no verify skill/.test(run.reason ?? '') ? ['Create the verify skill: done-done init']
|
|
1893
1909
|
: /^locate:/.test(run.reason ?? '') ? [`Make ${cfg.skill} a real folder inside the repository (not a link), commit, then run again: ${again}`]
|
|
1894
1910
|
: [`Read the step logs in ${shown(run.repo, join(dir, 'logs'))}`, `Fix the cause, then run again: ${again}`])
|
|
1895
|
-
: code === PASS ? [`Verify this receipt (
|
|
1911
|
+
: code === PASS ? [`Verify this receipt (merge gate): done-done receipt --verify ${rec.commit}`]
|
|
1896
1912
|
: status === 'cancelled' ? [...(run.kind === 'maintain' ? [] : [`Start again from the failed step: done-done rerun --run ${run.run_id}`]), `List past runs: done-done history`]
|
|
1897
1913
|
: [`Read the step logs in ${shown(run.repo, join(dir, 'logs'))}`, ...(run.kind === 'maintain' ? [] : [`Fix the cause, commit, then run again from the failed step: done-done rerun --run ${run.run_id}`]), `Or start a new run: ${again}`];
|
|
1898
1914
|
emit(result);
|
|
@@ -2557,7 +2573,7 @@ function scaffoldProblem(a, tracked, shown) {
|
|
|
2557
2573
|
return '';
|
|
2558
2574
|
}
|
|
2559
2575
|
function verifySkillMd(app, agent, s, isolated, features, omitted) {
|
|
2560
|
-
const name = `verify-${app}`;
|
|
2576
|
+
const name = `done-done-verify-${app}`;
|
|
2561
2577
|
const source = cited(s.launch.source);
|
|
2562
2578
|
const launch = isolated
|
|
2563
2579
|
? `${s.launch.steps.trim()}\n\nSafe: ${clause(s.launch.why)} (from \`${source}\`).`
|
|
@@ -2624,6 +2640,22 @@ function symlinkOnPath(repo, p) {
|
|
|
2624
2640
|
}
|
|
2625
2641
|
return undefined;
|
|
2626
2642
|
}
|
|
2643
|
+
// An older init wrote verify-<app>; with no `skill:` set, init moves that folder to the prefixed name once, and says so.
|
|
2644
|
+
// A prefixed folder that already exists wins, and the old one is left alone.
|
|
2645
|
+
function renameLegacySkill(repo, cfg) {
|
|
2646
|
+
const old = `.agents/skills/verify-${cfg.app}`;
|
|
2647
|
+
if (cfg.from.skill || cfg.skill !== old)
|
|
2648
|
+
return undefined;
|
|
2649
|
+
const next = `.agents/skills/done-done-verify-${cfg.app}`;
|
|
2650
|
+
if (existsSync(join(repo, next)) || symlinkOnPath(repo, join(repo, old)) !== undefined)
|
|
2651
|
+
return undefined;
|
|
2652
|
+
renameSync(join(repo, old), join(repo, next));
|
|
2653
|
+
const md = join(repo, next, 'SKILL.md');
|
|
2654
|
+
if (existsSync(md))
|
|
2655
|
+
writeFileSync(md, readFileSync(md, 'utf8').replace(`name: verify-${cfg.app}`, `name: done-done-verify-${cfg.app}`).replace(`name: "verify-${cfg.app}"`, `name: "done-done-verify-${cfg.app}"`));
|
|
2656
|
+
cfg.skill = next;
|
|
2657
|
+
return `${old} -> ${next}`;
|
|
2658
|
+
}
|
|
2627
2659
|
const SCAFFOLD_HELP = 'The verify skill was not written: fix the cause verify_skill names, then re-run: done-done init';
|
|
2628
2660
|
// All-or-nothing: when any target (SKILL.md, features/README.md, a features file) exists as a file or directory,
|
|
2629
2661
|
// nothing is written and the paths are named; otherwise every file is written into a temp dir inside the skill's
|
|
@@ -3080,6 +3112,7 @@ async function cmdInit(argv) {
|
|
|
3080
3112
|
const cfg = loadConfig(repo); // validates the existing project config; malformed -> exit 2, no writes
|
|
3081
3113
|
if (cfg.skill.split(/[\\/]/).includes('..'))
|
|
3082
3114
|
throw new UsageError(`verify skill ${cfg.skill} has a .. segment; use a path inside the repo`, 'Set skill to a repo-relative path in .done-done.yaml');
|
|
3115
|
+
const renamed = renameLegacySkill(repo, cfg);
|
|
3083
3116
|
const spec = values.hook === undefined ? undefined : HOOKS[values.hook];
|
|
3084
3117
|
const hook = spec?.make(repo); // likewise validated before any write
|
|
3085
3118
|
// The agent: --agent, else with none set the user's pick in a terminal (asked before any write); never done-done's.
|
|
@@ -3156,6 +3189,7 @@ async function cmdInit(argv) {
|
|
|
3156
3189
|
app: fresh.app,
|
|
3157
3190
|
evidence_root: root,
|
|
3158
3191
|
gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
|
|
3192
|
+
...(renamed && { skill_renamed: renamed }),
|
|
3159
3193
|
verify_skill: verify.status,
|
|
3160
3194
|
...agentHook.out,
|
|
3161
3195
|
help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
|
|
@@ -3195,6 +3229,7 @@ app: ${JSON.stringify(cfg.app)}
|
|
|
3195
3229
|
app: cfg.app,
|
|
3196
3230
|
evidence_root: root,
|
|
3197
3231
|
gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
|
|
3232
|
+
...(renamed && { skill_renamed: renamed }),
|
|
3198
3233
|
verify_skill: verify.status,
|
|
3199
3234
|
...agentHook.out,
|
|
3200
3235
|
help: [
|
|
@@ -3478,11 +3513,75 @@ async function cmdLook(argv) {
|
|
|
3478
3513
|
});
|
|
3479
3514
|
return hits.length ? FAIL : PASS;
|
|
3480
3515
|
}
|
|
3516
|
+
// The folder of a branch where a receipt copied from the evidence root is committed, for `gate`.
|
|
3517
|
+
const GATE_DIR = '.done-done-receipts';
|
|
3518
|
+
// The reason no committed receipt vouches for `tip`, or null when one does. Reads git objects only: no config, no judge, no evidence files.
|
|
3519
|
+
function gateFault(repo, tip, dir, pushed) {
|
|
3520
|
+
const short = tip.slice(0, 12);
|
|
3521
|
+
const names = git(repo, '-c', 'core.quotepath=off', 'ls-tree', '-r', '--name-only', tip, '--', dir).split('\n').filter((n) => n.endsWith('.json'));
|
|
3522
|
+
if (!names.length)
|
|
3523
|
+
return `no receipt in ${dir}/ at ${short}`;
|
|
3524
|
+
const reasons = [];
|
|
3525
|
+
for (const name of names) {
|
|
3526
|
+
const why = (() => {
|
|
3527
|
+
let rec;
|
|
3528
|
+
try {
|
|
3529
|
+
rec = JSON.parse(git(repo, 'show', `${tip}:${name}`));
|
|
3530
|
+
}
|
|
3531
|
+
catch (e) {
|
|
3532
|
+
return `${name} is malformed (${e.message})`;
|
|
3533
|
+
}
|
|
3534
|
+
const fault = receiptFault(rec, basename(name));
|
|
3535
|
+
if (fault)
|
|
3536
|
+
return fault;
|
|
3537
|
+
if (name !== `${dir}/${rec.head}.json`)
|
|
3538
|
+
return `${name} does not name its own head ${String(rec.head).slice(0, 12)}`;
|
|
3539
|
+
try {
|
|
3540
|
+
git(repo, 'merge-base', '--is-ancestor', rec.head, tip);
|
|
3541
|
+
}
|
|
3542
|
+
catch {
|
|
3543
|
+
return `receipt head ${rec.head.slice(0, 12)} is not ${short} or an ancestor of it (a shallow checkout needs full history)`;
|
|
3544
|
+
}
|
|
3545
|
+
const outside = git(repo, '-c', 'core.quotepath=off', 'diff', '--name-only', rec.head, tip).split('\n').filter((n) => n && !n.startsWith(`${dir}/`));
|
|
3546
|
+
if (outside.length)
|
|
3547
|
+
return `${named(outside)} changed after receipt head ${rec.head.slice(0, 12)}, so its proof no longer covers ${short}`;
|
|
3548
|
+
const at = Date.parse(rec.at);
|
|
3549
|
+
if (!(Math.floor(at / 1000) <= Math.floor(pushed / 1000)))
|
|
3550
|
+
return `receipt time ${rec.at} is not before the push (${new Date(pushed).toISOString()})`;
|
|
3551
|
+
return null;
|
|
3552
|
+
})();
|
|
3553
|
+
if (!why)
|
|
3554
|
+
return null;
|
|
3555
|
+
reasons.push(why);
|
|
3556
|
+
}
|
|
3557
|
+
return reasons.join('; ');
|
|
3558
|
+
}
|
|
3559
|
+
// `done-done gate`: the no-model check a hosted runner can run. A receipt committed under GATE_DIR vouches for the head.
|
|
3560
|
+
function cmdGate(argv) {
|
|
3561
|
+
const { values, positionals } = parseArgs({ args: argv, options: { dir: { type: 'string' }, pushed: { type: 'string' } }, allowPositionals: true });
|
|
3562
|
+
if (positionals.length > 1)
|
|
3563
|
+
throw new UsageError('gate takes at most one head', 'Re-run as: done-done gate [<head>] [--dir <folder>] [--pushed <time>]');
|
|
3564
|
+
const repo = repoRoot();
|
|
3565
|
+
let tip;
|
|
3566
|
+
try {
|
|
3567
|
+
tip = git(repo, 'rev-parse', '--verify', `${positionals[0] ?? 'HEAD'}^{commit}`);
|
|
3568
|
+
}
|
|
3569
|
+
catch {
|
|
3570
|
+
throw new UsageError(`gate: ${positionals[0]} is not a commit in this checkout`, 'Pass the pull request head sha, or run it in a checkout of that head');
|
|
3571
|
+
}
|
|
3572
|
+
const pushed = values.pushed === undefined ? Number(git(repo, 'log', '-1', '--format=%ct', tip)) * 1000 : Date.parse(values.pushed);
|
|
3573
|
+
if (Number.isNaN(pushed))
|
|
3574
|
+
throw new UsageError(`gate: --pushed ${values.pushed} is not a time`, 'Pass an ISO time such as 2026-10-20T09:05:56Z');
|
|
3575
|
+
const why = gateFault(repo, tip, (values.dir ?? GATE_DIR).replace(/\/+$/, ''), pushed);
|
|
3576
|
+
console.log(why === null ? 'PASS' : `FAIL: ${why}`);
|
|
3577
|
+
return why === null ? PASS : FAIL;
|
|
3578
|
+
}
|
|
3481
3579
|
function cmdLint(argv) {
|
|
3482
3580
|
const { positionals } = parseArgs({ args: argv, allowPositionals: true });
|
|
3483
3581
|
if (positionals.length !== 2)
|
|
3484
3582
|
throw new UsageError('lint needs a PR URL or report file, then the evidence folder', 'Re-run as: done-done lint <PR URL | report file> <evidence folder>');
|
|
3485
|
-
const
|
|
3583
|
+
const { checks } = loadConfig(process.cwd());
|
|
3584
|
+
const v = lint(positionals[0], positionals[1], home(), checks);
|
|
3486
3585
|
console.log(v.ok ? 'PASS' : `FAIL: ${v.why}`);
|
|
3487
3586
|
return v.ok ? PASS : FAIL;
|
|
3488
3587
|
}
|
|
@@ -3611,6 +3710,8 @@ async function main(argv) {
|
|
|
3611
3710
|
return await cmdLook(rest);
|
|
3612
3711
|
if (cmd === 'lint')
|
|
3613
3712
|
return cmdLint(rest);
|
|
3713
|
+
if (cmd === 'gate')
|
|
3714
|
+
return cmdGate(rest);
|
|
3614
3715
|
if (cmd === 'status')
|
|
3615
3716
|
return cmdStatus(rest);
|
|
3616
3717
|
if (cmd === 'attach')
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { parse } from 'yaml';
|
|
@@ -69,6 +69,8 @@ const KEYS = {
|
|
|
69
69
|
'secrets.patterns': { scope: 'repo', type: 'regexes' },
|
|
70
70
|
'secrets.allow': { scope: 'repo', type: 'regexes' },
|
|
71
71
|
'checks.fix_title': { scope: 'repo', type: 'string' },
|
|
72
|
+
'checks.upstream_catch_up': { scope: 'repo', type: 'boolean' },
|
|
73
|
+
'checks.moved_from': { scope: 'repo', type: 'boolean' },
|
|
72
74
|
'judge.model': { scope: 'global', type: 'string' },
|
|
73
75
|
'judge.route': { scope: 'global', type: 'route' },
|
|
74
76
|
'judge.floor': { scope: 'global', type: 'number' },
|
|
@@ -273,7 +275,7 @@ function defaultConfig(repo) {
|
|
|
273
275
|
base_ref: '',
|
|
274
276
|
evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
|
|
275
277
|
secrets: { patterns: {}, allow: [] },
|
|
276
|
-
checks: { fix_title: '^(fix|bugfix|hotfix)\\b' },
|
|
278
|
+
checks: { fix_title: '^(fix|bugfix|hotfix)\\b', upstream_catch_up: false, moved_from: false },
|
|
277
279
|
judge: { route: '', model: '', floor: 0.7, env_file: '' },
|
|
278
280
|
trusted: false,
|
|
279
281
|
from: {},
|
|
@@ -287,7 +289,9 @@ export function loadConfig(repo, projectText) {
|
|
|
287
289
|
apply(cfg, readOptional(globalFile), globalFile, 'global');
|
|
288
290
|
const projectFile = join(repo, '.done-done.yaml');
|
|
289
291
|
apply(cfg, projectText ?? readOptional(projectFile), projectFile, 'repo');
|
|
290
|
-
|
|
292
|
+
// A skill dir an older init wrote (verify-<app>) is still read until init renames it to the prefixed name.
|
|
293
|
+
cfg.skill ||= existsSync(join(repo, `.agents/skills/done-done-verify-${cfg.app}`)) || !existsSync(join(repo, `.agents/skills/verify-${cfg.app}`))
|
|
294
|
+
? `.agents/skills/done-done-verify-${cfg.app}` : `.agents/skills/verify-${cfg.app}`;
|
|
291
295
|
const local = tilde(cfg.evidence.local_root);
|
|
292
296
|
cfg.acpx_path = tilde(cfg.acpx_path);
|
|
293
297
|
for (const a of Object.keys(cfg.agent_path_override))
|
package/dist/lint.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `done-done lint`: the deterministic merge gate on a worker's claim. It reads the newest "## Checks" block of a PR body
|
|
2
|
-
// or report file and fails an unproven, reused or oversized claim. No judge, no model
|
|
2
|
+
// or report file and fails an unproven, reused or oversized claim. No judge, no model; the repo config only switches on
|
|
3
|
+
// the two optional size rules (checks.upstream_catch_up, checks.moved_from).
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
6
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
@@ -13,7 +14,6 @@ const gh = (...args) => {
|
|
|
13
14
|
const r = spawnSync('gh', args, { encoding: 'utf8', timeout: 60_000, maxBuffer: 1 << 28 });
|
|
14
15
|
return { status: r.error ? 1 : r.status, stdout: r.stdout ?? '', stderr: r.error?.message ?? r.stderr ?? '' };
|
|
15
16
|
};
|
|
16
|
-
// Added minus deleted non-generated lines of a unified diff: a line moved between files cancels out.
|
|
17
17
|
function netLines(diff) {
|
|
18
18
|
const net = new Map();
|
|
19
19
|
let generated = false;
|
|
@@ -27,15 +27,48 @@ function netLines(diff) {
|
|
|
27
27
|
if (l[0] === '+' || l[0] === '-')
|
|
28
28
|
net.set(l.slice(1).trim(), (net.get(l.slice(1).trim()) ?? 0) + (l[0] === '+' ? 1 : -1));
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
return without(net, new Map());
|
|
31
|
+
}
|
|
32
|
+
// `a` less the lines `b` holds, keeping only what is still added.
|
|
33
|
+
function without(a, b) {
|
|
34
|
+
const out = new Map();
|
|
35
|
+
for (const [l, n] of a)
|
|
36
|
+
if (n - (b.get(l) ?? 0) > 0)
|
|
37
|
+
out.set(l, n - (b.get(l) ?? 0));
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const total = (n) => [...n.values()].reduce((sum, c) => sum + c, 0);
|
|
41
|
+
const stdout = (r) => (r.status === 0 ? r.stdout.trim() : '');
|
|
42
|
+
// checks.upstream_catch_up: lines the fork's parent repo already shipped are not authored here, so subtract the upstream delta the PR
|
|
43
|
+
// merges in (PR base ... the merge base with the parent's default branch). A plain fork PR merges no new upstream commit, so its delta
|
|
44
|
+
// is empty. Any API failure keeps the full count.
|
|
45
|
+
function upstreamDelta(run, nwo, head, base) {
|
|
46
|
+
const parent = stdout(run('api', `repos/${nwo}`, '--jq', '.parent | select(.) | .owner.login + ":" + .default_branch'));
|
|
47
|
+
const tip = parent && stdout(run('api', `repos/${nwo}/compare/${parent}...${head}`, '--jq', '.merge_base_commit.sha'));
|
|
48
|
+
const diff = tip && run('api', '-H', 'Accept: application/vnd.github.diff', `repos/${nwo}/compare/${base}...${tip}`);
|
|
49
|
+
return diff && diff.status === 0 ? netLines(diff.stdout) : new Map();
|
|
50
|
+
}
|
|
51
|
+
// checks.moved_from: a body line "moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>..." cancels added lines that byte-match
|
|
52
|
+
// the mapped source files in a repo of the PR's own owner; authored lines still count.
|
|
53
|
+
function movedLines(run, nwo, body, paths) {
|
|
54
|
+
const mv = /^\s*moved-from\s*:\s*([\w.-]+)\/([\w.-]+)@(\S+)\s+(.+)$/m.exec(body);
|
|
55
|
+
const rules = mv && mv[1] === nwo.split('/')[0] ? mv[4].split(/\s+/).filter((r) => r.includes('=')).map((r) => [r.slice(0, r.indexOf('=')), r.slice(r.indexOf('=') + 1)]) : [];
|
|
56
|
+
const moved = new Map();
|
|
57
|
+
for (const p of paths) {
|
|
58
|
+
const rule = rules.find(([dst]) => p.startsWith(dst));
|
|
59
|
+
if (!mv || !rule || GENERATED.test(p))
|
|
60
|
+
continue;
|
|
61
|
+
const src = run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${mv[1]}/${mv[2]}/contents/${rule[1]}${p.slice(rule[0].length)}?ref=${mv[3]}`);
|
|
62
|
+
if (src.status === 0)
|
|
63
|
+
for (const l of src.stdout.split('\n'))
|
|
64
|
+
if (l.trim())
|
|
65
|
+
moved.set(l.trim(), (moved.get(l.trim()) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
return moved;
|
|
35
68
|
}
|
|
36
69
|
const sha256 = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
|
|
37
70
|
// src: a PR URL or a report file; ev: the folder the evidence names are relative to; dir: where the hash store and log live.
|
|
38
|
-
export function lint(src, ev, dir, run = gh) {
|
|
71
|
+
export function lint(src, ev, dir, rules = {}, run = gh) {
|
|
39
72
|
const log = (v) => {
|
|
40
73
|
mkdirSync(dir, { recursive: true });
|
|
41
74
|
appendFileSync(join(dir, 'lint.log'), `${new Date().toISOString()}\t${src}\t${v.head}\t${v.ok ? 'pass' : 'fail'}\t${v.why}\n`);
|
|
@@ -47,7 +80,7 @@ export function lint(src, ev, dir, run = gh) {
|
|
|
47
80
|
task = basename(dirname(resolve(src)));
|
|
48
81
|
}
|
|
49
82
|
else {
|
|
50
|
-
const r = run('pr', 'view', src, '--json', 'author,body,title,files,headRefOid,headRefName');
|
|
83
|
+
const r = run('pr', 'view', src, '--json', 'author,body,title,files,headRefOid,headRefName,baseRefOid');
|
|
51
84
|
if (r.status)
|
|
52
85
|
return log({ ok: false, why: `gh pr view failed: ${r.stderr.trim().split('\n').pop()?.slice(0, 160) || '?'}`, head });
|
|
53
86
|
const pr = JSON.parse(r.stdout);
|
|
@@ -60,8 +93,15 @@ export function lint(src, ev, dir, run = gh) {
|
|
|
60
93
|
let size = (pr.files ?? []).reduce((n, f) => n + (GENERATED.test(f.path ?? '') ? 0 : f.additions ?? 0), 0);
|
|
61
94
|
if (size >= SIZE_LIMIT) {
|
|
62
95
|
const d = run('pr', 'diff', src);
|
|
63
|
-
if (!d.status)
|
|
64
|
-
|
|
96
|
+
if (!d.status) {
|
|
97
|
+
let net = netLines(d.stdout);
|
|
98
|
+
const nwo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//.exec(src)?.[1];
|
|
99
|
+
if (nwo && rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
|
|
100
|
+
net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? ''));
|
|
101
|
+
if (nwo && rules.moved_from && total(net) >= SIZE_LIMIT)
|
|
102
|
+
net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? '')));
|
|
103
|
+
size = total(net);
|
|
104
|
+
}
|
|
65
105
|
}
|
|
66
106
|
if (size >= SIZE_LIMIT)
|
|
67
107
|
return log({ ok: false, why: `${size} added non-generated lines (moved lines excluded): split it (limit ${SIZE_LIMIT})`, head });
|
|
@@ -79,9 +119,9 @@ export function lint(src, ev, dir, run = gh) {
|
|
|
79
119
|
if (!/[/`]|\.\w{2,4}\b|\b(npm|pnpm|yarn|npx|bash|sh|python3?|node|cargo|go|gradle|make|adb|xcrun|done-done)\b/.test(f.proof))
|
|
80
120
|
return fail('proof: names no command or path');
|
|
81
121
|
const skills = join(process.cwd(), '.agents', 'skills');
|
|
82
|
-
const hasVerify = existsSync(skills) && readdirSync(skills).some((d) =>
|
|
122
|
+
const hasVerify = existsSync(skills) && readdirSync(skills).some((d) => /^(done-done-)?verify-/.test(d));
|
|
83
123
|
if (!f.proof.includes('verify-') && (hasVerify || !/no verify skill/i.test(f.proof)))
|
|
84
|
-
return fail(`proof: does not name the verify-<app> skill run${hasVerify ? '' : ' (or "no verify skill")'}`);
|
|
124
|
+
return fail(`proof: does not name the done-done-verify-<app> skill run${hasVerify ? '' : ' (or "no verify skill")'}`);
|
|
85
125
|
if (NOT_APPLICABLE.test(f.repro) && FIX_TITLE.test(title))
|
|
86
126
|
return fail('repro: is n/a on a fix PR');
|
|
87
127
|
if (/^not proven\s*:\s*\S/i.test(f.evidence))
|
package/dist/look.js
CHANGED
|
@@ -7,7 +7,7 @@ import { domRecord, LOOK, SQUASHED_TEXT } from './dom.js';
|
|
|
7
7
|
import { findBin } from './agents.js';
|
|
8
8
|
// done-done look: opens one page in headless Chrome at a phone viewport over the DevTools protocol (Node's own
|
|
9
9
|
// WebSocket, no browser library), writes the screenshot and its page-check record next to it, and returns the record.
|
|
10
|
-
// Chrome runs without its sandbox so it starts in containers
|
|
10
|
+
// Chrome runs without its sandbox so it starts in containers: point look at your own app, not untrusted sites.
|
|
11
11
|
const BROWSERS = ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
|
|
12
12
|
const WAIT = 30_000;
|
|
13
13
|
export async function look(url, capture, width, height) {
|
package/docs/.nojekyll
ADDED
|
File without changes
|