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 CHANGED
@@ -1,17 +1,36 @@
1
1
  <div align="center">
2
2
 
3
- <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/media/wordmark-dark.svg">
5
- <img src="docs/media/wordmark-light.svg" alt="done-done" width="300">
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
  [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-3fb950?logo=nodedotjs&logoColor=white)](https://nodejs.org) [![Status: early preview](https://img.shields.io/badge/status-early%20preview-d29922)](#whats-next) [![npm: done-done-axi](https://img.shields.io/npm/v/done-done-axi?logo=npm&logoColor=white&label=npm)](https://www.npmjs.com/package/done-done-axi)
13
14
 
14
- <br>
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 and CI 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.
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 stays 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.
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. It writes no verify skill: write `.agents/skills/verify-<app>/SKILL.md` by hand, with a launch that 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:
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), or in CI once the evidence folder is copied there ([how](docs/reference.md#where-proof-lives)).
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 (CI runs this)
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 wordmark and the flow picture are hand-made SVGs.
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 (use in CI)',
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 (use in CI)',
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 (CI)'],
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, model or config']],
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 CI checkout (a detached HEAD) is checking.
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 (or a CI job that restored the evidence root) finds the same file.
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
- if (!rec || typeof rec !== 'object' || rec.schema !== 2 || rec.result !== 'pass' || !Array.isArray(rec.files) || !rec.files.length)
865
- return fail(`receipt for ${short} is malformed`);
866
- if (!rec.files.every((f) => f && typeof f === 'object' && typeof f.path === 'string' && typeof f.sha256 === 'string'))
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 (CI or merge): done-done receipt --verify ${r.head.commit}`] } };
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 CI gate needs a PR on a remote; without one, a receipt records the pass locally.
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 CI on this proof: done-done check --from <PR URL> --trusted-ref origin/main${citationArgs}`];
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 (CI or merge): done-done receipt --verify ${rec.commit}`]
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 v = lint(positionals[0], positionals[1], home());
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
- cfg.skill ||= `.agents/skills/verify-${cfg.app}`;
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, no repo config.
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
- let total = 0;
31
- for (const n of net.values())
32
- if (n > 0)
33
- total += n;
34
- return total;
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
- size = netLines(d.stdout);
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) => d.startsWith('verify-'));
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 and CI: point look at your own app, not untrusted sites.
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