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 +2 -2
- package/dist/cli.js +36 -1
- package/docs/reference.md +2 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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",
|