done-done-axi 0.4.3 → 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
@@ -6,6 +6,8 @@
6
6
 
7
7
  ### No proof, no "done".
8
8
 
9
+ **[Landing page](https://umeranjum17.github.io/done-done-axi/)**
10
+
9
11
  done-done won't accept "done" without real, fresh proof from your product, captured at the exact commit being called done.
10
12
 
11
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)
@@ -118,7 +120,7 @@ cd your-app
118
120
  done-done init
119
121
  ```
120
122
 
121
- `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/verify-<app>/SKILL.md`, from the repo's own files (never overwriting one; with no agent set, no skill is written). 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:
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:
122
124
 
123
125
  ```yaml
124
126
  journeys:
@@ -246,3 +248,5 @@ npx -y --allow-git=all github:umeranjum17/done-done-axi --help
246
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.
247
249
 
248
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
@@ -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'],
@@ -209,14 +209,16 @@ const COMMANDS = {
209
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`],
210
210
  },
211
211
  lint: {
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, 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']],
213
213
  flags: [],
214
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
215
- 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"
216
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):
217
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
218
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
219
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.
220
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`],
221
223
  },
222
224
  status: {
@@ -2571,7 +2573,7 @@ function scaffoldProblem(a, tracked, shown) {
2571
2573
  return '';
2572
2574
  }
2573
2575
  function verifySkillMd(app, agent, s, isolated, features, omitted) {
2574
- const name = `verify-${app}`;
2576
+ const name = `done-done-verify-${app}`;
2575
2577
  const source = cited(s.launch.source);
2576
2578
  const launch = isolated
2577
2579
  ? `${s.launch.steps.trim()}\n\nSafe: ${clause(s.launch.why)} (from \`${source}\`).`
@@ -2638,6 +2640,22 @@ function symlinkOnPath(repo, p) {
2638
2640
  }
2639
2641
  return undefined;
2640
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
+ }
2641
2659
  const SCAFFOLD_HELP = 'The verify skill was not written: fix the cause verify_skill names, then re-run: done-done init';
2642
2660
  // All-or-nothing: when any target (SKILL.md, features/README.md, a features file) exists as a file or directory,
2643
2661
  // nothing is written and the paths are named; otherwise every file is written into a temp dir inside the skill's
@@ -3094,6 +3112,7 @@ async function cmdInit(argv) {
3094
3112
  const cfg = loadConfig(repo); // validates the existing project config; malformed -> exit 2, no writes
3095
3113
  if (cfg.skill.split(/[\\/]/).includes('..'))
3096
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);
3097
3116
  const spec = values.hook === undefined ? undefined : HOOKS[values.hook];
3098
3117
  const hook = spec?.make(repo); // likewise validated before any write
3099
3118
  // The agent: --agent, else with none set the user's pick in a terminal (asked before any write); never done-done's.
@@ -3170,6 +3189,7 @@ async function cmdInit(argv) {
3170
3189
  app: fresh.app,
3171
3190
  evidence_root: root,
3172
3191
  gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
3192
+ ...(renamed && { skill_renamed: renamed }),
3173
3193
  verify_skill: verify.status,
3174
3194
  ...agentHook.out,
3175
3195
  help: [`Capture proof: done-done run ${journey}, then: ${check}`, ...agentHook.help, ...ag.help, ...jg.help, ...(verify.failed ? [SCAFFOLD_HELP] : []), `Commit ${commitFirst(fresh.skill)}`],
@@ -3209,6 +3229,7 @@ app: ${JSON.stringify(cfg.app)}
3209
3229
  app: cfg.app,
3210
3230
  evidence_root: root,
3211
3231
  gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
3232
+ ...(renamed && { skill_renamed: renamed }),
3212
3233
  verify_skill: verify.status,
3213
3234
  ...agentHook.out,
3214
3235
  help: [
@@ -3559,7 +3580,8 @@ function cmdLint(argv) {
3559
3580
  const { positionals } = parseArgs({ args: argv, allowPositionals: true });
3560
3581
  if (positionals.length !== 2)
3561
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>');
3562
- const v = lint(positionals[0], positionals[1], home());
3583
+ const { checks } = loadConfig(process.cwd());
3584
+ const v = lint(positionals[0], positionals[1], home(), checks);
3563
3585
  console.log(v.ok ? 'PASS' : `FAIL: ${v.why}`);
3564
3586
  return v.ok ? PASS : FAIL;
3565
3587
  }
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/docs/.nojekyll ADDED
File without changes
@@ -0,0 +1,140 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>done-done: no proof, no "done"</title>
7
+ <meta name="description" content="done-done won't accept &quot;done&quot; without real, fresh proof from your product, captured at the exact commit being called done.">
8
+ <meta property="og:title" content="done-done">
9
+ <meta property="og:description" content="No proof, no &quot;done&quot;.">
10
+ <link rel="icon" href="logo.svg" type="image/svg+xml">
11
+ <meta name="theme-color" content="#F5F5F7" media="(prefers-color-scheme: light)">
12
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
13
+ <style>
14
+ /* The same Spacious tokens as sign-off's page: grey grouped page and white tiles by day, black and #161617 by night. */
15
+ :root { color-scheme: light dark; --page: #F5F5F7; --tile: #FFFFFF; --sunk: #E8E8ED; --line: #D2D2D7; --text: #1D1D1F; --muted: #6E6E73; --accent: #0066CC; --action: #0071E3; --code: #1D1D1F; --code-text: #F5F5F7; }
16
+ @media (prefers-color-scheme: dark) { :root { --page: #000000; --tile: #161617; --sunk: #2C2C2E; --line: #424245; --text: #F5F5F7; --muted: #86868B; --accent: #2997FF; --code: #161617; --code-text: #F5F5F7; } }
17
+ * { box-sizing: border-box; }
18
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; scroll-behavior: smooth; }
19
+ body { margin: 0; background: var(--page); color: var(--text); overflow-wrap: anywhere; -webkit-font-smoothing: antialiased;
20
+ font: 1.0625rem/1.4706 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", Roboto, sans-serif; letter-spacing: -0.022em; }
21
+ a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }
22
+ :focus-visible { outline: 3px solid var(--action); outline-offset: 2px; border-radius: 0.5rem; }
23
+ img, picture { display: block; max-width: 100%; height: auto; }
24
+ code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: 0; }
25
+ .wrap { max-width: 68rem; margin: 0 auto; padding: 0 max(1.25rem, env(safe-area-inset-left)) 0 max(1.25rem, env(safe-area-inset-right)); }
26
+ nav { display: flex; align-items: center; gap: 1rem; padding: 1.25rem 0; }
27
+ nav .brand { display: flex; align-items: center; gap: 0.625rem; font-weight: 600; color: var(--text); }
28
+ nav .brand img { width: 2rem; height: 2rem; border-radius: 0.5rem; }
29
+ nav .links { margin-left: auto; display: flex; gap: 1.25rem; font-size: 0.9375rem; }
30
+ nav .links a.gh { display: none; }
31
+ h1 { font-size: clamp(2.25rem, 7vw, 4rem); line-height: 1.05; letter-spacing: -0.03em; margin: 0 0 1rem; font-weight: 700; }
32
+ h2 { font-size: clamp(1.625rem, 4.5vw, 2.5rem); line-height: 1.1; letter-spacing: -0.028em; margin: 0 0 0.75rem; font-weight: 700; }
33
+ h3 { font-size: 1.25rem; margin: 0 0 0.25rem; letter-spacing: -0.02em; }
34
+ p { margin: 0 0 1rem; } .lead { font-size: 1.25rem; line-height: 1.4; color: var(--muted); max-width: 38rem; }
35
+ section { padding: 3rem 0; }
36
+ .hero { padding: 2rem 0 1rem; text-align: center; } .hero .lead { margin: 0 auto 1.5rem; }
37
+ .actions { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin-bottom: 2.5rem; }
38
+ .btn { display: inline-flex; align-items: center; min-height: 2.75rem; padding: 0 1.375rem; border-radius: 999px; background: var(--action); color: #fff; font-weight: 600; }
39
+ .btn:hover { text-decoration: none; filter: brightness(1.08); }
40
+ .btn.quiet { background: var(--sunk); color: var(--text); }
41
+ .shot { border-radius: 1rem; border: 1px solid var(--line); overflow: hidden; background: var(--tile); }
42
+ .hero .shot { max-width: 44rem; margin: 0 auto; }
43
+ .grid { display: grid; gap: 1rem; grid-template-columns: 1fr; }
44
+ .tile { background: var(--tile); border-radius: 1.25rem; padding: 1.5rem; }
45
+ .step { counter-increment: step; } .step::before { content: counter(step); display: grid; place-items: center; width: 1.75rem; height: 1.75rem; border-radius: 50%; background: var(--action); color: #fff; font-weight: 600; font-size: 0.9375rem; margin-bottom: 0.75rem; }
46
+ .steps { counter-reset: step; }
47
+ pre { margin: 0.5rem 0 0; padding: 0.75rem 1rem; border-radius: 0.75rem; background: var(--code); color: var(--code-text); overflow-x: auto; font: 0.875rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: 0; white-space: pre; }
48
+ .tile p.note { margin: 0.75rem 0 0; color: var(--muted); }
49
+ .figs { display: grid; gap: 1rem; grid-template-columns: 1fr; }
50
+ .figs figure { margin: 0; } .figs figcaption { margin-top: 0.5rem; font-size: 0.9375rem; color: var(--muted); } .figs b { color: var(--text); display: block; }
51
+ .figs .shot { background: transparent; }
52
+ ul.plain { padding-left: 1.25rem; margin: 0 0 1rem; } ul.plain li { margin-bottom: 0.5rem; }
53
+ .sibs { display: grid; gap: 1rem; grid-template-columns: 1fr; }
54
+ .tile p.what { margin: 0; color: var(--muted); }
55
+ footer { padding: 2.5rem 0 3rem; color: var(--muted); font-size: 0.9375rem; display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; }
56
+ @media (min-width: 48rem) {
57
+ nav .links a.gh { display: inline; }
58
+ .grid.three { grid-template-columns: repeat(3, 1fr); } .figs { grid-template-columns: repeat(2, 1fr); }
59
+ .sibs { grid-template-columns: repeat(2, 1fr); } section { padding: 4.5rem 0; }
60
+ }
61
+ @media (min-width: 64rem) { .sibs { grid-template-columns: repeat(4, 1fr); } }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div class="wrap">
66
+ <nav aria-label="Main">
67
+ <a class="brand" href="./"><img src="logo.svg" alt="" width="32" height="32">done-done</a>
68
+ <div class="links"><a href="#install">Install</a><a href="#see">See it work</a><a class="gh" href="https://github.com/umeranjum17/done-done-axi">GitHub</a><a href="https://www.npmjs.com/package/done-done-axi">npm</a></div>
69
+ </nav>
70
+
71
+ <header class="hero">
72
+ <h1>No proof, no &ldquo;done&rdquo;.</h1>
73
+ <p class="lead">done-done won't accept &ldquo;done&rdquo; without real, fresh proof from your product, captured at the exact commit being called done.</p>
74
+ <div class="actions"><a class="btn" href="#install">Install in 3 steps</a><a class="btn quiet" href="#see">See it work</a></div>
75
+ <div class="shot"><picture><source media="(prefers-color-scheme: dark)" srcset="media/flow-dark.svg"><img src="media/flow-light.svg" width="710" height="270" alt="How it works: capture your real app, tie every file to the exact commit, then check: pass with exit 0 or fail with exit 1."></picture></div>
76
+ </header>
77
+
78
+ <section id="install">
79
+ <h2>Install in 3 steps</h2>
80
+ <p class="lead">From npm, with Node 22.18 or newer and git.</p>
81
+ <div class="grid three steps">
82
+ <div class="tile step"><h3>Install</h3><pre>npm install -g done-done-axi</pre><p class="note">Puts <code>done-done</code> on your PATH.</p></div>
83
+ <div class="tile step"><h3>Set up your repo</h3><pre>cd your-app &amp;&amp; done-done init</pre><p class="note">Writes a minimal <code>.done-done.yaml</code> and asks which coding agent and which judge to use. It never picks either for you.</p></div>
84
+ <div class="tile step"><h3>Check the setup</h3><pre>done-done doctor</pre><p class="note">Changes nothing and names each missing piece with the command that fixes it.</p></div>
85
+ </div>
86
+ <p style="margin-top:1rem;color:var(--muted)">Then prove something: <code>done-done verify --outcome "&lt;what a user can now do&gt;"</code>. Or give your agent the skill with <code>npx skills add umeranjum17/done-done-axi --skill done-done -a &lt;agent&gt; -y</code> and ask &ldquo;is this really done?&rdquo;</p>
87
+ </section>
88
+
89
+ <section>
90
+ <h2>Why</h2>
91
+ <p class="lead">&ldquo;Done&rdquo; is easy to say. Screenshots get reused, recordings go stale, and a green test run is not the app.</p>
92
+ <p class="lead" style="margin-top:-0.25rem">done-done runs your capture and ties every screenshot, recording and log it saves to the exact commit. When someone says &ldquo;done&rdquo;, it checks the proof they point to: is it really there, is it a real picture or video, is it from this commit, and is it new? If not, the check fails. If it can't tell, it says so instead of passing.</p>
93
+ </section>
94
+
95
+ <section id="see">
96
+ <h2>See it work</h2>
97
+ <p class="lead">Four moments from a demo notes app. Every picture is real output from the current build; the demo's judge is a stub, so no model runs.</p>
98
+ <div class="figs">
99
+ <figure><div class="shot"><picture><source media="(prefers-color-scheme: dark)" srcset="media/run-dark.svg"><img src="media/run-light.svg" width="505" height="478" loading="lazy" alt="done-done run sync records one screenshot for the current commit."></picture></div><figcaption><b>Record a run</b>One screenshot, tied to the current commit.</figcaption></figure>
100
+ <figure><div class="shot"><picture><source media="(prefers-color-scheme: dark)" srcset="media/pass-dark.svg"><img src="media/pass-light.svg" width="505" height="838" loading="lazy" alt="A Checks block cites the new screenshot, the judge passes it, and done-done check passes."></picture></div><figcaption><b>Fresh proof passes</b>The judge sees the right screen; the check exits 0.</figcaption></figure>
101
+ <figure><div class="shot"><picture><source media="(prefers-color-scheme: dark)" srcset="media/reasons-dark.svg"><img src="media/reasons-light.svg" width="505" height="406" loading="lazy" alt="done-done check fails: one screenshot is from an older commit, one file is empty, one is a blank frame."></picture></div><figcaption><b>Weak proof is blocked</b>Older commit, empty file, blank frame: each named.</figcaption></figure>
102
+ <figure><div class="shot"><picture><source media="(prefers-color-scheme: dark)" srcset="media/config-dark.svg"><img src="media/config-light.svg" width="505" height="166" loading="lazy" alt="done-done run stops with exit 2 because the config has an unknown key."></picture></div><figcaption><b>Mistakes stop early</b>A config typo exits 2 before anything runs.</figcaption></figure>
103
+ </div>
104
+ </section>
105
+
106
+ <section>
107
+ <h2>What it checks</h2>
108
+ <ul class="plain">
109
+ <li><b>Tied to a commit.</b> Every run records the commit, and whether there were uncommitted changes.</li>
110
+ <li><b>Real media.</b> Empty, cut-off, blank or tiny screenshots fail. Videos must hold real frames.</li>
111
+ <li><b>Fresh, and not reused.</b> Proof from an older commit fails, and so does proof reused for a different claim.</li>
112
+ <li><b>Shows the outcome.</b> A fresh, tools-off judge on your own plan or key says whether each capture is the right screen for the claim. There is no way to turn it off.</li>
113
+ <li><b>Honest claims.</b> A pull request's <code>## Checks</code> block must have every line filled in, and <code>lint</code> gates it with no model.</li>
114
+ <li><b>Verifiable later.</b> <code>receipt</code> records a passing check against the commit and the sha256 of every file it cited.</li>
115
+ <li><b>Looks right.</b> <code>design</code> and <code>look</code> catch squashed or clipped text, overlap and pages wider than the screen.</li>
116
+ </ul>
117
+ <p style="color:var(--muted)">Every answer is an exit code: <b>0</b> pass, <b>1</b> fail, <b>2</b> usage or config error, <b>3</b> can't tell, <b>4</b> a run is still running or waits at its gate.</p>
118
+ </section>
119
+
120
+ <section>
121
+ <h2>Alone, and with the other tools</h2>
122
+ <p class="lead">done-done works on its own: the CLI and the skill need nothing else. It is one of four tools that each do one job and connect through small written contracts. Neither needs the others.</p>
123
+ <div class="sibs">
124
+ <div class="tile"><h3>done-done</h3><p class="what">Verification. This page.</p></div>
125
+ <div class="tile"><h3><a href="https://www.npmjs.com/package/sign-off-axi">sign-off</a></h3><p class="what">The board where agents ask you questions. <code>done-done signoff</code> writes a run's gate or evidence in its format; it never posts.</p></div>
126
+ <div class="tile"><h3><a href="https://www.npmjs.com/package/learn-from-mistakes-axi">learn-from-mistakes</a></h3><p class="what">Retros that find mistakes your agents keep repeating, with evidence.</p></div>
127
+ <div class="tile"><h3><a href="https://www.npmjs.com/package/first-draft-axi">first-draft</a></h3><p class="what">Turns a prompt into design mocks. Pass one to <code>done-done design --mock</code> to judge a screen next to it.</p></div>
128
+ </div>
129
+ </section>
130
+
131
+ <section>
132
+ <h2>Inside Firstmate, or outside</h2>
133
+ <p class="lead">done-done behaves the same with or without <a href="https://github.com/kunchenguid/firstmate">Firstmate</a>. Nothing here is needed to use it.</p>
134
+ <p class="lead">Inside a Firstmate home, <code>integrations/firstmate/</code> ships a brief include that tells each worker to run a journey, write a <code>## Checks</code> block, pass <code>check</code> and record a <code>receipt</code>, and an optional merge gate that refuses a merge without it. A lone agent in a terminal skips all of that.</p>
135
+ </section>
136
+
137
+ <footer><span>done-done · Apache-2.0</span><a href="https://github.com/umeranjum17/done-done-axi">GitHub</a><a href="https://www.npmjs.com/package/done-done-axi">npm</a><a href="https://github.com/umeranjum17/done-done-axi/blob/main/docs/reference.md">Reference</a><a href="https://github.com/umeranjum17/done-done-axi/blob/main/VISION.md">Vision</a><a href="https://github.com/umeranjum17/done-done-axi/blob/main/docs/example-cli.md">Worked example</a></footer>
138
+ </div>
139
+ </body>
140
+ </html>
package/docs/reference.md CHANGED
@@ -16,7 +16,7 @@ done-done init
16
16
 
17
17
  `init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, the verify skill outcome (`verify_skill`), and the next step.
18
18
 
19
- `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from, and plain code refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
19
+ `init` also has the agent the user set write the verify skill at `skill` (default `.agents/skills/done-done-verify-<app>`) from the repo's own files: plain code gives it the tracked file list and the text of the files that show how the repo proves itself (manifests, `.done-done.yaml` journeys, verify, e2e, smoke and demo scripts, `bin/` files, Dockerfiles, then source files (including Vue, Svelte, HTML, CSS and common compiled-language sources); JSON, YAML and Markdown are excluded except for manifests; at most 150,000 characters, and the prompt names what it left out; a `.env` or `.env.*` file, a file or folder whose path contains `secret`, `credential`, `token` or `password`, and `*.pem`, `*.key`, `*.p12` files never have their text sent by `init`, the agent is told not to open them, and no launch or feature can cite them; the tracked file list still names them), and one read-only call answers with the launch, readiness check, harness, evidence and up to five features (claude reads the repo with its read tools; every other agent gets only that text, and codex and gemini cannot run it tool-less). Launch prefers the repo's own safe proof path, a stub or demo engine with its own state, and when the repo shows none, `SKILL.md` opens Launch with `Not isolated:` and what the launch would touch, and `verify_skill` says so. `Safe:` is written only when the model marks the launch safe and its source is a tracked file that is not a manifest such as `package.json` or `README.md`; any other launch is `Not isolated:`, and its steps are not parsed. Each feature names the repo file it comes from, and plain code refuses an answer that cites a file the repo does not track, a secret-looking file, or, for a tool-less agent, a tracked file whose text its prompt left out. It writes `SKILL.md`, plus `features/README.md` and one file per feature with numbered drive and capture steps; `features/README.md` says how many more were found and not written. With no agent set, no skill is written and `init` asks for the agent (exit 2); an agent that fails or answers with an uncited or missing field writes no skill, and `verify_skill` names the cause and the call record (exit 1). On a repo with nothing tracked yet (a fresh `git init`), no skill is written and no agent is asked — no answer could cite a tracked file — and `verify_skill` names the remedy: commit the repo's files, then re-run (exit 1). The write is all-or-nothing: when any target already exists — `SKILL.md`, `features/README.md` or a `features/<name>.md`, as a file or a directory — nothing is written and `verify_skill` says `skipped: <paths> already exist; delete the skill folder to regenerate`, so a re-run after a lost `SKILL.md` can never pair new answers with the old feature map; a finished skill is left byte-identical and a re-run asks the agent nothing. A symlink on the path skips the scaffold and reports why in `verify_skill`, with exit 0. Otherwise the files are written into a temporary directory inside the skill's parent and moved into place, so a failed write leaves no partial skill: the temporary directory and any moved file are removed, `init` exits 1, and `verify_skill` names the error and says to re-run `init`. The config is kept either way. A `skill` path with a `..` segment is refused (exit 2) before anything is written. A repo whose older `init` wrote `.agents/skills/verify-<app>` (and sets no `skill`) keeps working: every command reads that folder, and the next `init` moves it once to `.agents/skills/done-done-verify-<app>` (also updating the `name:` line in its `SKILL.md`) and reports `skill_renamed: <old> -> <new>`; when the prefixed folder already exists, the old one is left alone. Commit `.done-done.yaml` and the verify skill before the first capture: a capture from a tree with uncommitted changes fails `check`, and `init` says so in its help.
20
20
 
21
21
  ```sh
22
22
  done-done init --agent opencode --model opencode-go/muse-spark-1.3-contributor
@@ -70,7 +70,7 @@ done-done init --hook pi
70
70
  done-done doctor
71
71
  ```
72
72
 
73
- `doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks nine items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`). `git`, `config`, `evidence_root`, `agent` and `judge` are required, since done-done gives no result without its agent and judge: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
73
+ `doctor` answers "is this repo worth driving" and changes nothing except the `--probe` call record (the agent itself may keep its own session files). It checks nine items, each reported `ok`, `warning` or `missing` with the exact fix command (plus a `tool_gate` `warning` row when the agent is pi or codex; see [Agents](#agents)): `git` (inside a repository), `config` (a valid `.done-done.yaml`), `evidence_root` (writable), `verify_skill` (`.agents/skills/done-done-verify-<app>/SKILL.md`), `agent` (the coding agent the user set and its model; `missing` when none is set, with the command that saves the user's choice, or when it is not installed or done-done cannot start it yet; see [Agents](#agents)), `agent_settings` (each effective agent setting, `agent`, its model, effort, how many extra flags are set (never their values), binary and `agent_timeout`, and where it came from: the global config, `.done-done.yaml` or `default`), `judge` (`missing` when no judge is set, with the command that saves the user's choice; else the judge kit is present, and the route's own setup: for `claude-code` an executable `claude` binary (`agent_path_override.claude`, else PATH), with your own sign-in checked only when a capture is judged; for `answerer:<agent>` the agent on PATH; for `openai:key` the `OPENAI_API_KEY` variable and a `judge.model`; doctor does not call the model; it is checked even with no agent set), `ffprobe` (video evidence only) and `judge_choice` (the route and model the judge runs and the author it judges, see [Judge relevance](#judge-relevance): `ok`, naming how far apart they are (`agent`, `model` or `session-only`), since any judge the user sets runs; `missing`, with no fix of its own, until the agent is set; `missing` when no judge is set or the judge config is incomplete, such as a key route with no `judge.model`). `git`, `config`, `evidence_root`, `agent` and `judge` are required, since done-done gives no result without its agent and judge: `doctor` exits 0 when they are all ok and 2 otherwise. A missing `verify_skill` or `ffprobe`, or a `warning`, never fails it. In a fresh repo with nothing set up, `doctor` names each missing piece, starting with `done-done init`.
74
74
 
75
75
  ```sh
76
76
  done-done doctor --probe
@@ -223,14 +223,17 @@ journeys:
223
223
 
224
224
  ### Lint check
225
225
 
226
- `done-done lint <PR URL | report file> <evidence folder>` is a deterministic gate on a worker's claim: no judge, no model, no config. It prints `PASS` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
226
+ `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` or `FAIL: <reason>` and exits 0, 1, or 2 on a usage error. A PR URL is read with `gh pr view`; a file is read as the report.
227
227
 
228
228
  - The newest `## Checks` block needs `outcome:`, `repro:`, `proof:` and `evidence:`.
229
- - `proof:` names a command or path, and the repo's `verify-<app>` skill; it may say `no verify skill` only when `.agents/skills/` in the current folder holds none.
229
+ - `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.
230
230
  - `repro:` is not `n/a` when the PR title starts with `fix`, `bugfix` or `hotfix`.
231
231
  - `evidence: not proven: <cause>` passes.
232
232
  - 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.
233
233
  - 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.
234
+ - 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.
235
+ - `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.
236
+ - `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.
234
237
 
235
238
  ### Base-ref before
236
239
 
@@ -266,7 +269,7 @@ evidence: <paths of the evidence files, or "not proven: <cause>">
266
269
  **Form rules for the Checks block:**
267
270
  - all four lines must be present;
268
271
  - `proof` names a command or path;
269
- - if the repo has its verify skill (`.agents/skills/verify-<app>`), `proof` names it (`verify-<app>`); otherwise `proof` names some `verify-` skill or says "no verify skill";
272
+ - if the repo has its verify skill (`.agents/skills/done-done-verify-<app>`), `proof` names it (`done-done-verify-<app>`; an older `verify-<app>` name is accepted); otherwise `proof` names some `verify-` skill or says "no verify skill";
270
273
  - a fix PR needs a repro (checked against the PR title; a local file or stdin uses the HEAD commit subject as its title).
271
274
 
272
275
  **Each cited file must pass these checks:**
@@ -450,7 +453,7 @@ done-done maintain # --no-wait, --wait (the default), or --foreground (fo
450
453
 
451
454
  `maintain` checks the repo's verify skill against the source in a run of kind `maintain`, in the same store as `verify`, so `status`, `attach`, `logs`, `abort` and `history` work on it. Its pass reads only: it never edits the skill, the product code or git, and it records HEAD, `git status --porcelain --untracked-files=all` and the content of each changed or untracked path (regular files are hashed with sha256 as they are read; symlinks are compared by target text and never read through) before and after; if they differ, the run is `blocked` with a `repo-changed` finding. Its steps:
452
455
 
453
- - `locate`: the verify skill (`skill`, default `.agents/skills/verify-<app>`) must have a `SKILL.md`; with none, the run ends `blocked` and points at `done-done init`.
456
+ - `locate`: the verify skill (`skill`, default `.agents/skills/done-done-verify-<app>`) must have a `SKILL.md`; with none, the run ends `blocked` and points at `done-done init`.
454
457
  - `hygiene` (plain code): every local markdown link in the skill must resolve (`dead-link`); `features/README.md` must exist (`index`), list each `features/*.md` file (`unlisted`) and list it only once (`duplicate`).
455
458
  - `source-wave`: one fresh reader per feature file, at the configured agent (`agent`, `agent_config`). Every reader except claude's gets no tools (pi `--no-tools`, opencode and acp with every tool denied); codex and gemini cannot read tool-less at all, so `maintain` never runs without a reader: an agent that cannot read tool-less, or that done-done cannot start, stops it before any run with exit 2 and the reason (use claude, opencode, pi or acp). Where readers run, plain code puts the feature file and the text of each tracked file it cites into the prompt, each file capped at 20,000 characters (a cut file is marked `[truncated after 20000 characters]` in the prompt, with no finding of its own) and all of a feature's cited text at 60,000 characters, with one `truncated` notice naming the feature and the cited files left out when any are dropped. Only claude readers get read-only tools in the repo (`--tools Read,Grep,Glob` with `--setting-sources ''` and `--strict-mcp-config`, so no user settings or MCP servers load): a live probe showed opencode reads through an in-repo symlink to an outside target, while claude refuses it. Each reader explains the feature from source and returns its drift (each statement the source contradicts, with the source file:line) and one live recipe. Readers run one after another; a run makes at most 30 recorded agent calls, each bounded by `agent_timeout`; a tool-less call that does not answer valid JSON is tried up to three times in all within its record, so a run starts at most 90 agent processes. A feature file longer than 20,000 characters is read only up to that cap, and a `truncated` notice names it; the notice is listed but does not make the result `drift`. A symlink on the skill path (the skill folder, its features folder, the index or a feature file) is a blocking `coverage` finding and is never read, so a committed link cannot send a file outside the repo to the agent; a skill reached through a symlinked ancestor is refused at `locate`. Readers cannot read outside the repo: claude runs without `--add-dir` or a permission bypass, so reads outside its working directory are refused.
456
459
  - `reconcile`: the tool-less call names at most 500 recently changed paths, most recent first, and says how many older ones it left out (those get no `unmapped` check). Plain code checks each `path:line` the skill cites (`citation`) and each file:line a reader cites. A bare file name (`app.js:50`) is checked against the tracked files whose path ends with it, and the line must exist in one of them; no tracked match is a finding. A repo path (`src/app.js:50`) must be a tracked file too, so an untracked or ignored file is a citation finding in the skill and an unverified drift in a reader's answer. A bare name is a citation only when its extension is one that a tracked file has, so host:port text such as `api.example.com:8443` is ignored. A reader's drift cites files the same way, and its drift becomes a `drift` finding only when its citation exists, and unverified citations are listed under `unverified` in the run's status (and in the step log), with a count and the first few. An answer item in an unexpected shape, or a list field that is not a list, is listed there too and never trusted. One tool-less call merges the recipes into as few app states as it can and names files changed in the last 30 days (`git log`, outside the skill folder) that no feature covers; each one that is in that list becomes an `unmapped` finding. maintain gives no result without its readers: with no agent set it asks, as in [Agents](#agents). Gemini cannot be a reader yet: its no-tools mode is read-only plan mode, not tool-free, so `maintain` with gemini stops with exit 2 before any run.
@@ -558,7 +561,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
558
561
  | Key | Default |
559
562
  |---|---|
560
563
  | `app` | repo directory name |
561
- | `skill` | `.agents/skills/verify-<app>` |
564
+ | `skill` | `.agents/skills/done-done-verify-<app>` |
562
565
  | `journeys.<name>.command` | none |
563
566
  | `journeys.<name>.spends` | none; what a run of the journey spends, so `verify` waits for the user approval, see [Journeys that spend](#journeys-that-spend) |
564
567
  | `secrets.patterns`, `secrets.allow` | `{}` and `[]`; extra `<name>: <regex>` secret shapes and known-safe regexes, see [Secrets in the evidence](#secrets-in-the-evidence) |
@@ -572,6 +575,8 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
572
575
  | `evidence.allow_dirty` | `false` |
573
576
  | `evidence.ignore_paths` | `[]` |
574
577
  | `checks.fix_title` | `^(fix\|bugfix\|hotfix)\b`: titles that need a repro |
578
+ | `checks.upstream_catch_up` | `false`; `true` lets `lint` subtract a fork's upstream catch-up lines from the size count, see [Lint check](#lint-check) |
579
+ | `checks.moved_from` | `false`; `true` lets `lint` subtract lines a PR moves from another repo of the same owner, see [Lint check](#lint-check) |
575
580
  | `agent` | as in the global file; set here, it wins |
576
581
  | `auto_fix.maintain` | as in the global file (`0`, report only); set here, it wins |
577
582
  | `auto_fix.verify` | as in the global file (`0`, report only); set here, it wins |
@@ -17,7 +17,7 @@ wherever this text or the skill says `done-done`. If done-done cannot run, repor
17
17
  ## Checks
18
18
  outcome: <what a user can now do>
19
19
  repro: <command that failed before and passes now, or n/a; a fix PR needs one>
20
- proof: <the command or path that drove the real product; name the repo's verify-<app> skill, or say "no verify skill">
20
+ proof: <the command or path that drove the real product; name the repo's done-done-verify-<app> skill, or say "no verify skill">
21
21
  evidence: <the evidence file paths, or "not proven: <cause>">
22
22
  ```
23
23
  Put it at the end of the PR body. With no PR, put it in `checks.md` in your task's data folder outside the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
6
6
  "type": "module",
@@ -57,7 +57,7 @@ A CI job gets no model and no key, so it never runs `check`, `run` or `verify`.
57
57
  receipt it wrote (`<evidence root>/.done-done/receipts/<head>.json`) into `.done-done-receipts/` in a follow-up commit. CI runs only
58
58
  `done-done gate <head>` (full history): `PASS` or `FAIL: <reason>` for a missing, late, other-head or moved-on receipt.
59
59
 
60
- `init` also has that agent write the repo's `.agents/skills/verify-<app>/SKILL.md` (never overwriting one): read it before
60
+ `init` also has that agent write the repo's `.agents/skills/done-done-verify-<app>/SKILL.md` (never overwriting one): read it before
61
61
  you drive the app, and fix it when it is wrong.
62
62
 
63
63
  done-done starts a coding agent the user chose. When it says no agent is set, ask the user which agent