done-done-axi 0.6.19 → 0.6.20

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
@@ -48,12 +48,12 @@ Commit `.done-done.yaml` and the verify skill so later runs and your teammates u
48
48
  Installing done-done from another installer (a fleet bootstrap, a Juggernaut-style one-command install) takes three calls and no prompts. Pin the version:
49
49
 
50
50
  ```sh
51
- npm install -g done-done-axi@0.6.19
51
+ npm install -g done-done-axi@0.6.20
52
52
  done-done init --no-skill --agent <agent> --model <model> --judge <route> --judge-model <model>
53
53
  done-done doctor --setup --json
54
54
  ```
55
55
 
56
- - `done-done --version` prints the installed version (here `0.6.19`); check it before anything else.
56
+ - `done-done --version` prints the installed version (here `0.6.20`); check it before anything else.
57
57
  - `init --no-skill` writes `.done-done.yaml` and the global `~/.done-done/config.yaml`, and starts no agent: it skips the verify skill, so it spends nothing and asks nothing. `<route>` is `answerer:<agent>` or a bare agent name. Exit `0` when the agent is set and the config is valid, `2` when it is not.
58
58
  - `doctor --setup --json` is install health: exit `0` when git, the config, the evidence root, the agent and the judge are ok, `2` when one of them is not. The verify skill and the journey rows are warnings, so a fresh install passes.
59
59
  - Exit codes: `0` pass, `2` usage or config error ([all codes](docs/reference.md#exit-codes)).
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn, spawnSync } from 'node:child_process';
3
3
  import { createHash, randomBytes } from 'node:crypto';
4
- import { accessSync, appendFileSync, closeSync, constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { accessSync, appendFileSync, closeSync, constants, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
5
  import { homedir, tmpdir, userInfo } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -505,6 +505,31 @@ function writeRecord(cfg, rec) {
505
505
  }
506
506
  // The random suffix keeps two runs in the same second at one commit apart; mkdir without recursive refuses a reused id.
507
507
  const newRunId = (commit) => `${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}-${commit.slice(0, 8)}-${randomBytes(3).toString('hex')}`;
508
+ // A passing check seals the evidence it accepted that no run at this commit holds, so a proof made by `check` alone leaves a run
509
+ // record at HEAD (exit 0, byte-for-byte copies, hashes). Its command names the seal, so it is never taken for a command that ran;
510
+ // it keeps the step record of the run it copies from, so a later check that ties to it passes the same rules.
511
+ function sealEvidence(repo, cfg, items) {
512
+ const rec = openRun(repo, cfg, null, 'check (sealed)');
513
+ const names = new Set();
514
+ rec.files = items.map(({ path }) => {
515
+ let name = basename(path);
516
+ for (let n = 2; names.has(name); n++)
517
+ name = `${n}-${basename(path)}`;
518
+ names.add(name);
519
+ copyFileSync(path, join(runDir(cfg, rec.run_id), name));
520
+ const b = readFileSync(join(runDir(cfg, rec.run_id), name));
521
+ return { path: join(rec.run_id, name), sha256: sha256(b), bytes: b.length };
522
+ });
523
+ const from = items.find((i) => i.source.steps || i.source.steps_error)?.source;
524
+ if (from?.steps)
525
+ rec.steps = from.steps;
526
+ if (from?.steps_error)
527
+ rec.steps_error = from.steps_error;
528
+ rec.ended_at = new Date().toISOString();
529
+ rec.exit = 0;
530
+ writeRecord(cfg, rec);
531
+ return `${rec.run_id} (${shown(repo, join(recordsDir(cfg), `${rec.run_id}.json`))})`;
532
+ }
508
533
  function openRun(repo, cfg, journey, command, id) {
509
534
  const h = head(repo);
510
535
  const runId = id ?? newRunId(h.commit);
@@ -825,6 +850,8 @@ async function runCheck(argv, run) {
825
850
  const seen = new Map();
826
851
  const hashes = [];
827
852
  const cited = [];
853
+ // Cited files no run at this commit holds yet: a pass seals them (sealEvidence), so a check-only proof leaves a run record at HEAD.
854
+ const unsealed = [];
828
855
  const captures = [];
829
856
  // The run behind each cited capture, for the real-product rule and the receipt.
830
857
  const runs = new Map();
@@ -907,6 +934,8 @@ async function runCheck(argv, run) {
907
934
  continue;
908
935
  }
909
936
  const rec = (atNow.length ? atNow : claims).reduce((a, b) => (b.started_at > a.started_at ? b : a));
937
+ if (!byHash.some((r) => r.commit === now.commit && r.exit === 0))
938
+ unsealed.push({ path, source: rec });
910
939
  runs.set(rec.run_id, { run_id: rec.run_id, commit: rec.commit, journey: rec.journey, command: rec.command, ...(rec.feature && { feature: rec.feature }), ...(rec.tool && { tool: rec.tool }), ...(rec.steps && { steps: rec.steps }), ...(rec.steps_error && { steps_error: rec.steps_error }) });
911
940
  // Only a capture tied to a run record can reach the judge, so a stray cited file is never sent.
912
941
  const c = { name, bytes, sha256: hash, media: v };
@@ -1107,6 +1136,12 @@ async function runCheck(argv, run) {
1107
1136
  if (rep.code === PASS) {
1108
1137
  mkdirSync(dirname(ledger), { recursive: true });
1109
1138
  appendFileSync(ledger, hashes.filter((h) => !used.get(h)?.has(claimId)).map((h) => `${h} ${claimId}\n`).join(''));
1139
+ if (unsealed.length) {
1140
+ if (now.dirty)
1141
+ rep.result.not_sealed = 'the tree has uncommitted changes, so a run record at this commit would not be clean proof; commit, then check again to seal the evidence';
1142
+ else
1143
+ rep.result.sealed = sealEvidence(repo, cfg, unsealed);
1144
+ }
1110
1145
  }
1111
1146
  return { code: rep.code, result: rep.result, judge: judged, judgeBlock, claim: claim?.outcome, skill: claim?.skill, files, cited, runs: [...runs.values()], head: now, findings, warnings, ...(evals && { evals }) };
1112
1147
  }
package/docs/reference.md CHANGED
@@ -462,6 +462,8 @@ A plain check is never a verdict on its own. A cited file that fails one of thes
462
462
 
463
463
  Files that are in no run record give an inconclusive result when another cited capture is one the judge reads: nothing ties them to a commit. So does a file that more than one run record claims when none of those runs is at the current commit (possible only with records from before each run had its own folder). A file ties to a run by its bytes: a sealed or renamed copy of a file a run wrote, cited outside the evidence root or at another path inside it, is that run's evidence, and the record's own path for those bytes picks the run when several wrote them. Byte-identical captures from several runs are fine when one of those runs is at the current commit: that run ties the file to the commit. A file whose path is named in a run record but whose bytes no longer match fails as `changed after run <run-id>` (exit 2, before the judge), not inconclusive: the path ties it to that run, so editing it after the run is tampering, not untied evidence.
464
464
 
465
+ A passing `check` seals the evidence it accepted, so a proof made by `check` alone leaves a run record at its commit: when a cited file is in no run record at HEAD with `exit: 0` (it ties to a run at an earlier commit, for example after a docs-only change), the pass copies each such file byte for byte into a new run folder and writes a schema 1 record at HEAD with `exit: 0`, `dirty: false`, the files with their sha256 and bytes, `tool` and the step record of the run it copied from, and the command `check (sealed)`, which names the seal so it is never taken for a command that ran. The result names it as `sealed: <run id> (<record path>)`. Nothing is sealed on a fail or an inconclusive result, on a dirty tree (the result says so in `not_sealed`), or when every cited file already ties to a run at HEAD with `exit: 0`; a second pass at the same head finds the seal and writes no second record. The record is never a pass by itself: only a check's verdict is, and how `check` ties files and what it reports are unchanged.
466
+
465
467
  A config whose journeys are all code-only, or cited captures that all come from code-only runs, stop the check the same way, before the judge, naming the journeys ([Real-product evidence](#real-product-evidence)). So does a claim whose cited runs have no well-formed step record ([Step record](#step-record)).
466
468
 
467
469
  `evidence: not proven: <cause>` stops the check before its judge is asked, always: exit 2, no result, no receipt. `checks.not_proven` is gone: a config that still sets it is a config error (exit 2) that says to delete the line.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
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",