done-done-axi 0.6.14 → 0.6.16

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
@@ -68,11 +68,13 @@ Every answer is an exit code: **0** pass, **1** fail, **2** usage or config erro
68
68
 
69
69
  Four more moments from a demo notes app: a capture for the current commit, a judged pass, a check that refuses an older commit and two bad files, and a config error that stops the run. Real output from the current build; these four use a stub judge that says the screen shows the outcome, so no model runs.
70
70
 
71
- <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/run-dark.svg"><img src="docs/media/run-light.svg" alt="done-done run sync records one screenshot for the current commit." width="400" align="top"></picture>
72
- <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/pass-dark.svg"><img src="docs/media/pass-light.svg" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes." width="400" align="top"></picture>
71
+ <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/run-dark.svg"><img src="docs/media/run-light.svg" alt="done-done run sync records one screenshot for the current commit." width="505"></picture></p>
73
72
 
74
- <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/reasons-dark.svg"><img src="docs/media/reasons-light.svg" alt="done-done check fails: one screenshot is from an older commit, one file is empty, one is a blank frame." width="400" align="top"></picture>
75
- <picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/config-dark.svg"><img src="docs/media/config-light.svg" alt="done-done run stops with exit 2 because the config has an unknown key." width="400" align="top"></picture>
73
+ <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/pass-dark.svg"><img src="docs/media/pass-light.svg" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes." width="505"></picture></p>
74
+
75
+ <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/reasons-dark.svg"><img src="docs/media/reasons-light.svg" alt="done-done check fails: one screenshot is from an older commit, one file is empty, one is a blank frame." width="505"></picture></p>
76
+
77
+ <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/media/config-dark.svg"><img src="docs/media/config-light.svg" alt="done-done run stops with exit 2 because the config has an unknown key." width="505"></picture></p>
76
78
 
77
79
  The hero above is a real run of the published CLI on a fresh sample repo, replayed line by line: `run` recorded the capture at commit `ca78363`, the Pi judge on `commandcode/deepseek/deepseek-v4.1-flash` read it and passed it with that reason, `receipt` wrote the receipt, and `init` refused a journey whose only command was `npm test`. [The manual walkthrough](docs/manual-cli.md) does the same by hand.
78
80
 
@@ -92,8 +94,10 @@ A journey is the saved script that drives your app and captures what a user sees
92
94
 
93
95
  ```sh
94
96
  done-done journey add "greet Umer and check that Hello Umer is printed"
95
- done-done journey enhance greet-umer-prints-hello "add checks for an empty name and an unknown command"
96
- done-done journey improve greet-umer-prints-hello "also check that greeting Zoë prints exactly Hello Zoë"
97
+ done-done journey enhance greet-umer-prints-hello \
98
+ "add checks for an empty name and an unknown command"
99
+ done-done journey improve greet-umer-prints-hello \
100
+ "also check that greeting Zoë prints exactly Hello Zoë"
97
101
  ```
98
102
 
99
103
  `add` writes the script, its config entry and its feature in the verify skill; `enhance` deepens one journey; `improve` makes one change to one journey. Each exercise runs once on uncommitted files, which is a trial, not proof: read the script, commit it, then `done-done run <name>`. The saved script needs no agent or model, so CI can replay it; generation runs only on your machine ([worked transcript](docs/manual-cli.md)).
@@ -105,8 +109,9 @@ done-done works the same with or without Firstmate: the skill and CLI behave ide
105
109
  ```sh
106
110
  npx skills add umeranjum17/done-done-axi --skill done-done -g -a <agent> -y
107
111
  npm install -g done-done-axi
108
- cat "$(npm root -g)/done-done-axi/integrations/firstmate/brief-include.md" >> "$FM_HOME/config/brief-include.md"
109
- cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/config/merge-gate"
112
+ DD="$(npm root -g)/done-done-axi/integrations/firstmate"
113
+ cat "$DD/brief-include.md" >> "$FM_HOME/config/brief-include.md"
114
+ cp "$DD/merge-gate" "$FM_HOME/config/merge-gate"
110
115
  ```
111
116
 
112
117
  - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run the journeys `done-done affected` lists for its committed change, write a `## Checks` block, pass `check`, record a `receipt`, and see `affected` exit 0.
package/dist/cli.js CHANGED
@@ -281,7 +281,7 @@ each prints the files it changed and runs the journey once as a dirty-tree trial
281
281
  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):
282
282
  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
283
283
  (hash store lint.hashes in ~/.done-done, or $DONE_DONE_HOME). "evidence: not proven: <cause>" passes. A PR with 1,000 or more added
284
- non-generated lines fails (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
284
+ non-generated lines fails; the whole PR is counted (every page of its file list, its compare diff), and a failed page or diff fails instead of counting part of it (lockfiles, vendor/, dist/, LICENSE-type files, deletions and moved lines do not count); a bot PR passes.
285
285
  A PR URL whose diff changes a src/ file and no journeys/ file prints WARN instead of PASS (still exit 0): run the journey and commit it before the PR opens.
286
286
  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
287
287
  a "moved-from: <owner>/<repo>@<ref> <dst>=<src>" body line says came from a repo of the same owner; both are off by default.
package/dist/lint.js CHANGED
@@ -61,6 +61,34 @@ function without(a, b) {
61
61
  }
62
62
  const total = (n) => [...n.values()].reduce((sum, c) => sum + c, 0);
63
63
  const stdout = (r) => (r.status === 0 ? r.stdout.trim() : '');
64
+ const worst = (r) => r.stderr.trim().split('\n').pop()?.slice(0, 160) || '?';
65
+ // `gh pr view --json files` stops at 100 files, so the whole list comes from the paginated pull-files endpoint instead.
66
+ // GitHub stops that endpoint at 3000 files, so page 30 being full is already past what it will list.
67
+ const MAX_PAGES = 30;
68
+ function prFiles(run, nwo, num) {
69
+ const paths = [];
70
+ for (let page = 1; page <= MAX_PAGES; page++) {
71
+ const call = `repos/${nwo}/pulls/${num}/files?per_page=100&page=${page}`;
72
+ const r = run('api', call);
73
+ if (r.status)
74
+ return { why: `${call} failed: ${worst(r)}` };
75
+ let files;
76
+ try {
77
+ files = JSON.parse(r.stdout);
78
+ }
79
+ catch {
80
+ return { why: `${call} returned no file list` };
81
+ }
82
+ if (!Array.isArray(files))
83
+ return { why: `${call} returned no file list` };
84
+ for (const f of files)
85
+ if (f.path)
86
+ paths.push(f.path);
87
+ if (files.length < 100)
88
+ return { paths };
89
+ }
90
+ return { why: `${nwo}/pulls/${num}/files is still full on page ${MAX_PAGES}: more files than lint can count` };
91
+ }
64
92
  // checks.upstream_catch_up: lines the fork's parent repo already shipped are not authored here, so subtract the upstream delta the PR
65
93
  // 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
66
94
  // is empty. Any API failure keeps the full count.
@@ -107,9 +135,9 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
107
135
  task = basename(dirname(resolve(src)));
108
136
  }
109
137
  else {
110
- const r = run('pr', 'view', src, '--json', 'author,body,title,files,headRefOid,headRefName,baseRefOid');
138
+ const r = run('pr', 'view', src, '--json', 'author,body,title,headRefOid,headRefName,baseRefOid');
111
139
  if (r.status)
112
- return log({ ok: false, why: `gh pr view failed: ${r.stderr.trim().split('\n').pop()?.slice(0, 160) || '?'}`, head });
140
+ return log({ ok: false, why: `gh pr view failed: ${worst(r)}`, head });
113
141
  const pr = JSON.parse(r.stdout);
114
142
  ({ body, title } = { body: pr.body ?? '', title: pr.title ?? '' });
115
143
  head = pr.headRefOid ?? '-';
@@ -118,26 +146,31 @@ export function lint(src, ev, dir, rules = {}, run = gh) {
118
146
  if (/^(app\/)?(dependabot|renovate)(\[bot\])?$/.test(author))
119
147
  return log({ ok: true, why: 'bot', head });
120
148
  const nwo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//.exec(src)?.[1];
121
- const attrs = nwo && [head, pr.baseRefOid].map((ref) => ref && run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${nwo}/contents/.gitattributes?ref=${ref}`)).find((a) => a && a.status === 0);
149
+ const num = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/.exec(src)?.[1];
150
+ if (!nwo || !num)
151
+ return log({ ok: false, why: `not a GitHub pull URL: ${src.slice(0, 120)}`, head });
152
+ const listed = prFiles(run, nwo, num);
153
+ if (!listed.paths)
154
+ return log({ ok: false, why: listed.why ?? 'the pull file list failed', head });
155
+ const paths = listed.paths;
156
+ const attrs = [head, pr.baseRefOid].map((ref) => ref && run('api', '-H', 'Accept: application/vnd.github.raw', `repos/${nwo}/contents/.gitattributes?ref=${ref}`)).find((a) => a && a.status === 0);
122
157
  if (attrs) {
123
158
  const byAttr = attrGenerated(attrs.stdout);
124
159
  isGenerated = (p) => byConfig(p) || byAttr(p);
125
160
  }
126
- const paths = (pr.files ?? []).map((f) => f.path ?? '');
127
161
  if (paths.some((p) => p.startsWith('src/')) && !paths.some((p) => p.startsWith('journeys/')))
128
162
  warn = 'the diff changes src/ and no journeys/ file: run the journey, commit it, then open the PR';
129
- let size = (pr.files ?? []).reduce((n, f) => n + (isGenerated(f.path ?? '') ? 0 : f.additions ?? 0), 0);
130
- if (size >= SIZE_LIMIT) {
131
- const d = run('pr', 'diff', src);
132
- if (!d.status) {
133
- let net = netLines(d.stdout, isGenerated);
134
- if (nwo && rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
135
- net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
136
- if (nwo && rules.moved_from && total(net) >= SIZE_LIMIT)
137
- net = without(net, movedLines(run, nwo, body, (pr.files ?? []).map((f) => f.path ?? ''), isGenerated));
138
- size = total(net);
139
- }
140
- }
163
+ // The PR's whole net comes from the compare diff (three dots: from the merge base), never from the per-file additions.
164
+ const cmp = `repos/${nwo}/compare/${pr.baseRefOid ?? ''}...${head}`;
165
+ const d = run('api', '-H', 'Accept: application/vnd.github.diff', cmp);
166
+ if (d.status)
167
+ return log({ ok: false, why: `${cmp} failed: ${worst(d)}`, head });
168
+ let net = netLines(d.stdout, isGenerated);
169
+ if (rules.upstream_catch_up && total(net) >= SIZE_LIMIT)
170
+ net = without(net, upstreamDelta(run, nwo, head, pr.baseRefOid ?? '', isGenerated));
171
+ if (rules.moved_from && total(net) >= SIZE_LIMIT)
172
+ net = without(net, movedLines(run, nwo, body, paths, isGenerated));
173
+ const size = total(net);
141
174
  if (size >= SIZE_LIMIT)
142
175
  return log({ ok: false, why: `${size} added non-generated lines (moved lines excluded): split it (limit ${SIZE_LIMIT})`, head });
143
176
  }
Binary file
package/docs/reference.md CHANGED
@@ -314,7 +314,7 @@ journeys:
314
314
 
315
315
  ### Lint check
316
316
 
317
- `done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
317
+ `done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, and the project config only switches on two optional size rules. It prints `PASS`, `WARN: <reason>` or `FAIL: <reason>` and exits 0 (PASS or WARN), 1, or 2 on a usage error. A PR URL is read with `gh pr view` and `gh api`; a file is read as the report.
318
318
 
319
319
  - The newest `## Checks` block needs `outcome:`, `repro:`, `proof:` and `evidence:`.
320
320
  - `proof:` names a command or path, and the repo's `done-done-verify-<app>` skill (a legacy `verify-<app>` skill is read too); it may say `no verify skill` only when `.agents/skills/` in the current folder holds none.
@@ -322,8 +322,8 @@ journeys:
322
322
  - `evidence: not proven: <cause>` passes.
323
323
  - A PR URL whose changed files include one under `src/` and none under `journeys/` prints `WARN` instead of `PASS` (exit 0; a `FAIL` still wins). It needs no config and never applies to a report file. The worker rule it backs: run the journey, commit it, then open the PR.
324
324
  - Otherwise each named file (relative to the evidence folder, or absolute) exists, is non-empty and is newer than the evidence folder itself; no two are byte-identical; and at least one has a hash no other PR used. Hashes are kept in `lint.hashes` and each run is appended to `lint.log`, both in `~/.done-done` (`$DONE_DONE_HOME`). A later lint of the same PR is not reuse.
325
- - A PR with 1,000 or more added non-generated lines fails. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes.
326
- - Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines, and an API failure keeps the full count.
325
+ - A PR with 1,000 or more added non-generated lines fails. The whole PR is counted: its file list comes from every page of `pulls/<number>/files` (100 a page, up to GitHub's 3,000-file ceiling) and its net lines from the compare diff from the merge base, so a PR of any size is counted whole. Lockfiles, `vendor/`, `dist/` and `LICENSE`-type files do not count, and neither do deletions or lines moved between files. A dependabot or renovate PR passes. A page of the file list or the compare diff that fails makes lint `FAIL` with that call, never a partial count.
326
+ - Two optional size rules, both off by default and switched on per repo in `.done-done.yaml` (read from the current folder; they apply to a PR URL, never a report file). With both off a big PR is counted exactly as above. Each runs only when the PR is still at 1,000 lines.
327
327
  - `checks.upstream_catch_up: true`: in a fork, lines the parent repo already shipped are not authored here, so lint subtracts the changes between the PR's base and the merge base with the parent's default branch. A PR that merges in no new upstream commit loses nothing.
328
328
  - `checks.moved_from: true`: a line in the PR body `moved-from: <owner>/<repo>@<ref> <dst-prefix>=<src-prefix>` (more `dst=src` pairs may follow) cancels added lines that match, byte for byte after trimming, the file at the same path under `<src-prefix>` in that repo at `<ref>`. The source repo must have the PR's owner; authored lines still count.
329
329
  - `checks.generated_paths: ["gen/**", "*.snap"]`: globs for the repo's own generated files, counted as generated beside the built-in ones above, so they stay out of the size count. Default none, so results do not change. Like the two rules above it applies to a PR URL, never a report file.
@@ -860,6 +860,6 @@ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
860
860
 
861
861
  `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.
862
862
 
863
- The first-screen terminal images (`docs/media/hero-*.png`, and the animated `hero.gif`) are real output from the published CLI, captured in a sample repo. `node docs/media/hero.mjs` rebuilds their pages from the captured transcripts beside it (`hero-run.txt`, `hero-refuse.txt`) and fails when a shown value is not in them; screenshot `.hero/hero-desktop.html` (828x596) and `.hero/hero-phone.html` (418x792) at device scale factor 2, in light and dark. The GIF replays the same lines a step at a time on that page, ending on the judge's pass and the receipt. The four lower terminal illustrations (`hero`, `run`, `pass`, `reasons`, `config`) use a stub judge, so no model runs; redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
863
+ The first-screen terminal images (`docs/media/hero-*.png`, and the animated `hero.gif`) are real output from the published CLI, captured in a sample repo. `node docs/media/hero.mjs` rebuilds their pages from the captured transcripts beside it (`hero-run.txt`, `hero-refuse.txt`) and fails when a shown value is not in them; screenshot `.hero/hero-desktop.html` (828x596) and `.hero/hero-phone.html` (418x792) at device scale factor 2, in light and dark. The GIF replays the same lines a step at a time on that page; its first frame is the finished screen, so a still capture or a social preview never shows an empty terminal, and it ends on the judge's pass and the receipt. The four lower terminal illustrations (`hero`, `run`, `pass`, `reasons`, `config`) use a stub judge, so no model runs; redraw them with `npx tsc && node docs/media/make.mjs`. The logo (`docs/logo.svg`) and the flow picture are hand-made SVGs.
864
864
 
865
865
  The demo notes app behind those pictures: start one with `node docs/media/demo.mjs <fresh-scratch-directory> 0` (the server prints its owned port), open a native run in its `app/` repo, drive the checklist in the browser, save `app.png` at 400×140 DPR 2 into the run's evidence folder, seal it and run `check` with your configured judge. Save the open result as `<scratch>/open.json` and the check result as `<scratch>/check.json`; `/proof` displays that screenshot and verdict and refuses to show a pass without a real passing result. Capture `/proof` at 400×230 DPR 2 with `chrome-devtools-axi`, keep the scratch outside the repo and stop the server afterwards.
package/docs/releasing.md CHANGED
@@ -1,5 +1,5 @@
1
- # 0.6.14 patch release
1
+ # 0.6.16 patch release
2
2
 
3
- This release lets a home point its judge at a shared model pool: done-done picks the Claude account, moves on when one is rate or usage limited, and falls over to DeepSeek Goat or Cursor auto when every Claude account is under the floor.
3
+ This release counts a PR whole, however many files it touches. `lint` reads every page of the PR's file list (GitHub gives `gh pr view` only the first 100 files) and takes its net lines from the compare diff, so a PR over 100 files no longer gets a line count from its first 100 — one 408-file PR was counted as 6,198 added lines when its real net was about 688. The moved-from exclusion, the generated-file rules and the `journeys/` warning now see the whole list, and a failed page or compare diff fails the lint with that call instead of keeping a partial count.
4
4
 
5
- Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.14 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
5
+ Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.16 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Prove your app really works: done-done drives your app the way a user does, a fresh judge checks every capture, and the proof is tied to a commit.",
6
6
  "type": "module",