dunstan 0.1.10 → 0.1.11
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 +11 -2
- package/dist/dunstan.mjs +143 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
|
|
|
29
29
|
/plugin install dunstan@barglabs
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The plugin runs the MCP server with `npx -y dunstan@0.1.
|
|
32
|
+
The plugin runs the MCP server with `npx -y dunstan@0.1.11 mcp`, the `dunstan` npm package at that
|
|
33
33
|
exact version: one self-contained file, no dependencies.
|
|
34
34
|
|
|
35
35
|
**The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
|
|
@@ -76,7 +76,9 @@ dist/dunstan.mjs verify record.json # offline: recompute claims, verdict and d
|
|
|
76
76
|
dist/dunstan.mjs rerun record.json # online: re-read the sources, report evidence_changed
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Exit codes: 0 pass, 1 fail, 2 unverifiable, 3
|
|
79
|
+
Exit codes: 0 pass, 1 fail, 2 unverifiable, 3 no record: a usage error, a pull request that cannot
|
|
80
|
+
be read or that moved while it was checked and again when checked once more (`pull_request_moved`),
|
|
81
|
+
or an internal error (`verify`: 0 verified, 1 not).
|
|
80
82
|
`--sign-key <path> --signer <principal>` writes the record as its JCS bytes with a detached SSH
|
|
81
83
|
signature; `verify --sig <file> --allowed-signers <file>` checks it.
|
|
82
84
|
|
|
@@ -87,6 +89,13 @@ agent calls with its block before it says done, beside `dunstan_suggest_declarat
|
|
|
87
89
|
`checker.digest` in a record is the SHA-256 of `dist/dunstan.mjs`, the single-file bundle that ran:
|
|
88
90
|
the checker code, its dependencies and both schemas from `spec/schema/`, inlined at build time.
|
|
89
91
|
|
|
92
|
+
## What Dunstan does not check
|
|
93
|
+
|
|
94
|
+
Dunstan checks the claims a handback declares against the record. It does not check that the
|
|
95
|
+
handback covers what the issue asked for: an accurate report of the wrong work passes.
|
|
96
|
+
|
|
97
|
+
What to declare: [`docs/agents.md`](docs/agents.md#the-instruction-block).
|
|
98
|
+
|
|
90
99
|
## Declare before done
|
|
91
100
|
|
|
92
101
|
Checking a declared claim is exact. Finding the claims in prose is where readers fail: a hosted
|
package/dist/dunstan.mjs
CHANGED
|
@@ -8030,7 +8030,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
8030
8030
|
import { createHash } from "node:crypto";
|
|
8031
8031
|
|
|
8032
8032
|
// src/spec/constants.ts
|
|
8033
|
-
var SPEC_VERSION = "0.1.
|
|
8033
|
+
var SPEC_VERSION = "0.1.7";
|
|
8034
8034
|
var BLOCK_VERSION = "0.1";
|
|
8035
8035
|
var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
|
|
8036
8036
|
var BLOCK_INFO_STRING = "dunstan-handback";
|
|
@@ -8589,17 +8589,32 @@ function later(a, b) {
|
|
|
8589
8589
|
if ((a.runAttempt ?? 0) !== (b.runAttempt ?? 0)) return (a.runAttempt ?? 0) > (b.runAttempt ?? 0);
|
|
8590
8590
|
return a.id > b.id;
|
|
8591
8591
|
}
|
|
8592
|
+
function identity(run) {
|
|
8593
|
+
if (run.workflowPath !== void 0)
|
|
8594
|
+
return JSON.stringify(["workflow", run.workflowPath, run.name]);
|
|
8595
|
+
if (run.appSlug === "github-actions") return void 0;
|
|
8596
|
+
if (run.appSlug !== void 0) return JSON.stringify(["app", run.appSlug, run.name]);
|
|
8597
|
+
if (run.checkSuiteId === void 0) return JSON.stringify(["name", run.name]);
|
|
8598
|
+
return void 0;
|
|
8599
|
+
}
|
|
8592
8600
|
function countedCheckRuns(runs, excludedIds) {
|
|
8593
8601
|
const excluded = new Set(excludedIds);
|
|
8594
8602
|
const checker = /* @__PURE__ */ new Map();
|
|
8595
8603
|
const latest = /* @__PURE__ */ new Map();
|
|
8596
8604
|
for (const run of runs) {
|
|
8597
|
-
|
|
8605
|
+
const id = identity(run);
|
|
8606
|
+
if (excluded.has(run.id) && id !== void 0) {
|
|
8607
|
+
checker.set(id, Math.max(checker.get(id) ?? 0, run.id));
|
|
8608
|
+
}
|
|
8598
8609
|
const key = checkKey(run);
|
|
8599
8610
|
const seen = latest.get(key);
|
|
8600
8611
|
if (seen === void 0 || later(run, seen)) latest.set(key, run);
|
|
8601
8612
|
}
|
|
8602
|
-
|
|
8613
|
+
const superseded = (run) => {
|
|
8614
|
+
const id = identity(run);
|
|
8615
|
+
return id !== void 0 && run.id < (checker.get(id) ?? 0);
|
|
8616
|
+
};
|
|
8617
|
+
return [...latest.values()].filter((run) => !excluded.has(run.id) && !superseded(run)).sort((a, b) => a.id - b.id);
|
|
8603
8618
|
}
|
|
8604
8619
|
var NOT_FAILED_CONCLUSIONS = ["success", "skipped", "neutral"];
|
|
8605
8620
|
function booleanRow(field, declared, reason, runs, failed, mismatch, unsettled) {
|
|
@@ -8921,6 +8936,14 @@ var PullRequestUnreadable = class extends Error {
|
|
|
8921
8936
|
this.name = "PullRequestUnreadable";
|
|
8922
8937
|
}
|
|
8923
8938
|
};
|
|
8939
|
+
var PullRequestMoved = class extends Error {
|
|
8940
|
+
constructor(repository, number4, moves) {
|
|
8941
|
+
super(
|
|
8942
|
+
`pull request ${repository}#${number4} moved while it was checked, and again when it was checked once more (${moves.join(", ")}); no record is written (pull_request_moved)`
|
|
8943
|
+
);
|
|
8944
|
+
this.name = "PullRequestMoved";
|
|
8945
|
+
}
|
|
8946
|
+
};
|
|
8924
8947
|
var ShapeError = class extends Error {
|
|
8925
8948
|
};
|
|
8926
8949
|
function obj(value) {
|
|
@@ -8959,8 +8982,12 @@ function timestamp(value) {
|
|
|
8959
8982
|
function second(t) {
|
|
8960
8983
|
return t.slice(0, 19);
|
|
8961
8984
|
}
|
|
8962
|
-
async function readPullRequest(client, repository, number4) {
|
|
8963
|
-
const read = await client.read(
|
|
8985
|
+
async function readPullRequest(client, repository, number4, options = {}) {
|
|
8986
|
+
const read = await client.read(
|
|
8987
|
+
"pull_request",
|
|
8988
|
+
`/repos/${repository}/pulls/${number4}`,
|
|
8989
|
+
options.record === false ? { record: false } : {}
|
|
8990
|
+
);
|
|
8964
8991
|
if (!read.ok) {
|
|
8965
8992
|
throw new PullRequestUnreadable(
|
|
8966
8993
|
`cannot read pull request ${repository}#${number4} (${read.error}); no record is written`
|
|
@@ -8972,6 +8999,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8972
8999
|
const state = str(pr.state);
|
|
8973
9000
|
if (state !== "open" && state !== "closed") throw new ShapeError();
|
|
8974
9001
|
const base = obj(obj(pr.base).repo);
|
|
9002
|
+
const baseRef = str(obj(pr.base).ref);
|
|
8975
9003
|
return {
|
|
8976
9004
|
evidence: {
|
|
8977
9005
|
status: "ok",
|
|
@@ -8985,6 +9013,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8985
9013
|
changedFiles: int(pr.changed_files)
|
|
8986
9014
|
},
|
|
8987
9015
|
repository: str(base.full_name),
|
|
9016
|
+
baseRef,
|
|
8988
9017
|
body: strOrNull(pr.body)
|
|
8989
9018
|
};
|
|
8990
9019
|
} catch (e) {
|
|
@@ -8996,6 +9025,20 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8996
9025
|
throw e;
|
|
8997
9026
|
}
|
|
8998
9027
|
}
|
|
9028
|
+
function pullRequestMoves(first, again) {
|
|
9029
|
+
const state = (pr) => pr.evidence.merged ? "merged" : pr.evidence.state;
|
|
9030
|
+
const pairs = [
|
|
9031
|
+
["head", first.evidence.headSha, again.evidence.headSha],
|
|
9032
|
+
["base", first.baseRef, again.baseRef],
|
|
9033
|
+
["state", state(first), state(again)],
|
|
9034
|
+
["repository", first.repository, again.repository],
|
|
9035
|
+
["mergedAt", first.evidence.mergedAt, again.evidence.mergedAt],
|
|
9036
|
+
["mergeSha", first.evidence.mergeSha, again.evidence.mergeSha],
|
|
9037
|
+
["changedFiles", first.evidence.changedFiles, again.evidence.changedFiles],
|
|
9038
|
+
["number", first.evidence.number, again.evidence.number]
|
|
9039
|
+
];
|
|
9040
|
+
return pairs.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name} ${a} -> ${b}`);
|
|
9041
|
+
}
|
|
8999
9042
|
async function readAllPages(client, kind, pathFor, itemsOf, map2, options = {}) {
|
|
9000
9043
|
const out = [];
|
|
9001
9044
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
@@ -9775,7 +9818,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9775
9818
|
// src/record/checker.ts
|
|
9776
9819
|
import { readFileSync } from "node:fs";
|
|
9777
9820
|
var CHECKER_NAME = "dunstan";
|
|
9778
|
-
var CHECKER_VERSION = "0.1.
|
|
9821
|
+
var CHECKER_VERSION = "0.1.11";
|
|
9779
9822
|
function checkerIdentity(artifact) {
|
|
9780
9823
|
return {
|
|
9781
9824
|
name: CHECKER_NAME,
|
|
@@ -10748,6 +10791,8 @@ import { parseArgs } from "node:util";
|
|
|
10748
10791
|
|
|
10749
10792
|
// src/evidence/http.ts
|
|
10750
10793
|
var DEFAULT_API = "https://api.github.com";
|
|
10794
|
+
var MAX_ARCHIVE_BYTES = MAX_ENTRY_BYTES + 8 * 1024 * 1024;
|
|
10795
|
+
var MAX_BODY_BYTES = 48 * 1024 * 1024;
|
|
10751
10796
|
var EMPTY = new Uint8Array();
|
|
10752
10797
|
var GitHubClient = class {
|
|
10753
10798
|
sources = /* @__PURE__ */ new Map();
|
|
@@ -10784,10 +10829,19 @@ var GitHubClient = class {
|
|
|
10784
10829
|
async read(kind, path, options = {}) {
|
|
10785
10830
|
const method = options.method ?? "GET";
|
|
10786
10831
|
const locator = options.locator ?? `${method} ${path}${options.locatorSuffix ?? ""}`;
|
|
10787
|
-
const
|
|
10832
|
+
const budget = kind === "artifact" ? MAX_ARCHIVE_BYTES : MAX_BODY_BYTES;
|
|
10833
|
+
const result = await this.#fetchWithRetry(method, path, options.requestBody, locator, budget);
|
|
10788
10834
|
let read;
|
|
10789
10835
|
if (result.kind === "network") {
|
|
10790
10836
|
read = { ok: false, status: null, error: "network", body: EMPTY };
|
|
10837
|
+
} else if (result.body === null) {
|
|
10838
|
+
const failed = result.status < 200 || result.status > 299;
|
|
10839
|
+
read = {
|
|
10840
|
+
ok: false,
|
|
10841
|
+
status: result.status,
|
|
10842
|
+
error: failed ? `http_${result.status}` : "parse",
|
|
10843
|
+
body: EMPTY
|
|
10844
|
+
};
|
|
10791
10845
|
} else if (result.status < 200 || result.status > 299) {
|
|
10792
10846
|
read = {
|
|
10793
10847
|
ok: false,
|
|
@@ -10824,7 +10878,7 @@ var GitHubClient = class {
|
|
|
10824
10878
|
const source = this.sources.get(locator);
|
|
10825
10879
|
if (source !== void 0) source.error = "parse";
|
|
10826
10880
|
}
|
|
10827
|
-
async #fetchWithRetry(method, path, body, locator) {
|
|
10881
|
+
async #fetchWithRetry(method, path, body, locator, budget) {
|
|
10828
10882
|
const headers = {
|
|
10829
10883
|
accept: "application/vnd.github+json",
|
|
10830
10884
|
"x-github-api-version": "2022-11-28",
|
|
@@ -10849,10 +10903,10 @@ var GitHubClient = class {
|
|
|
10849
10903
|
this.#warn(`${locator}: network error after ${attempt} attempts (${e.message})`);
|
|
10850
10904
|
return { kind: "network" };
|
|
10851
10905
|
}
|
|
10852
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
10853
10906
|
const status = response.status;
|
|
10854
10907
|
const wait = retryWaitMs(response, status, this.#now(), attempt);
|
|
10855
10908
|
if (wait !== void 0 && attempt < this.#maxAttempts && wait <= this.#maxWaitMs) {
|
|
10909
|
+
await cancel(response.body);
|
|
10856
10910
|
await this.#sleep(wait);
|
|
10857
10911
|
continue;
|
|
10858
10912
|
}
|
|
@@ -10860,10 +10914,48 @@ var GitHubClient = class {
|
|
|
10860
10914
|
const why = wait > this.#maxWaitMs ? `rate limited for ${Math.ceil(wait / 1e3)}s` : `${attempt} attempts`;
|
|
10861
10915
|
this.#warn(`${locator}: giving up with HTTP ${status} (${why}); recorded as unreadable`);
|
|
10862
10916
|
}
|
|
10917
|
+
const bytes = await readWithin(response, budget);
|
|
10918
|
+
if (bytes === null) {
|
|
10919
|
+
this.#warn(
|
|
10920
|
+
`${locator}: answer over ${budget} bytes, not read in full; recorded as unreadable`
|
|
10921
|
+
);
|
|
10922
|
+
}
|
|
10863
10923
|
return { kind: "response", status, body: bytes, etag: response.headers.get("etag") };
|
|
10864
10924
|
}
|
|
10865
10925
|
}
|
|
10866
10926
|
};
|
|
10927
|
+
async function cancel(body) {
|
|
10928
|
+
await body?.cancel().catch(() => {
|
|
10929
|
+
});
|
|
10930
|
+
}
|
|
10931
|
+
async function readWithin(response, budget) {
|
|
10932
|
+
const length = response.headers.get("content-length");
|
|
10933
|
+
if (length !== null && /^[0-9]+$/.test(length) && Number(length) > budget) {
|
|
10934
|
+
await cancel(response.body);
|
|
10935
|
+
return null;
|
|
10936
|
+
}
|
|
10937
|
+
if (response.body === null) return EMPTY;
|
|
10938
|
+
const reader = response.body.getReader();
|
|
10939
|
+
const chunks = [];
|
|
10940
|
+
let size = 0;
|
|
10941
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
10942
|
+
size += chunk.value.length;
|
|
10943
|
+
if (size > budget) {
|
|
10944
|
+
await reader.cancel().catch(() => {
|
|
10945
|
+
});
|
|
10946
|
+
return null;
|
|
10947
|
+
}
|
|
10948
|
+
chunks.push(chunk.value);
|
|
10949
|
+
}
|
|
10950
|
+
if (chunks.length === 1) return chunks[0];
|
|
10951
|
+
const bytes = new Uint8Array(size);
|
|
10952
|
+
let at = 0;
|
|
10953
|
+
for (const chunk of chunks) {
|
|
10954
|
+
bytes.set(chunk, at);
|
|
10955
|
+
at += chunk.length;
|
|
10956
|
+
}
|
|
10957
|
+
return bytes;
|
|
10958
|
+
}
|
|
10867
10959
|
function backoffMs(attempt) {
|
|
10868
10960
|
return Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
10869
10961
|
}
|
|
@@ -13094,13 +13186,34 @@ function serializeRecord(record2) {
|
|
|
13094
13186
|
// src/pipeline/check-pull-request.ts
|
|
13095
13187
|
function githubReaders(client) {
|
|
13096
13188
|
return {
|
|
13097
|
-
pullRequest: (repository, number4) => readPullRequest(client, repository, number4),
|
|
13189
|
+
pullRequest: (repository, number4, options) => readPullRequest(client, repository, number4, options),
|
|
13098
13190
|
evidence: (input2) => readEvidence(client, input2),
|
|
13099
|
-
pathObjects: (input2) => readPathObjects(client, input2)
|
|
13191
|
+
pathObjects: (input2) => readPathObjects(client, input2),
|
|
13192
|
+
restart: () => client.sources.clear()
|
|
13100
13193
|
};
|
|
13101
13194
|
}
|
|
13195
|
+
async function atOnePullRequest(readers, repository, number4, first, collect) {
|
|
13196
|
+
let pr = first ?? await readers.pullRequest(repository, number4);
|
|
13197
|
+
for (let run = 1; ; run++) {
|
|
13198
|
+
const collected = await collect(pr);
|
|
13199
|
+
const again = await readers.pullRequest(repository, number4, { record: false });
|
|
13200
|
+
const moves = pullRequestMoves(pr, again);
|
|
13201
|
+
if (moves.length === 0) return collected;
|
|
13202
|
+
if (run === 2) throw new PullRequestMoved(repository, number4, moves);
|
|
13203
|
+
readers.restart?.();
|
|
13204
|
+
pr = await readers.pullRequest(repository, number4);
|
|
13205
|
+
}
|
|
13206
|
+
}
|
|
13102
13207
|
async function checkPullRequest(input2) {
|
|
13103
|
-
|
|
13208
|
+
return atOnePullRequest(
|
|
13209
|
+
input2.readers,
|
|
13210
|
+
input2.repository,
|
|
13211
|
+
input2.number,
|
|
13212
|
+
input2.pullRequest,
|
|
13213
|
+
(pr) => checkAt(input2, pr)
|
|
13214
|
+
);
|
|
13215
|
+
}
|
|
13216
|
+
async function checkAt(input2, pr) {
|
|
13104
13217
|
const repository = pr.repository;
|
|
13105
13218
|
const ownRuns = await input2.afterPullRequest?.(pr) ?? [];
|
|
13106
13219
|
const report = await input2.report(pr);
|
|
@@ -40734,7 +40847,7 @@ var Protocol = class {
|
|
|
40734
40847
|
}
|
|
40735
40848
|
};
|
|
40736
40849
|
}
|
|
40737
|
-
const
|
|
40850
|
+
const cancel2 = (reason) => {
|
|
40738
40851
|
this._responseHandlers.delete(messageId);
|
|
40739
40852
|
this._progressHandlers.delete(messageId);
|
|
40740
40853
|
this._cleanupTimeout(messageId);
|
|
@@ -40768,10 +40881,10 @@ var Protocol = class {
|
|
|
40768
40881
|
}
|
|
40769
40882
|
});
|
|
40770
40883
|
options?.signal?.addEventListener("abort", () => {
|
|
40771
|
-
|
|
40884
|
+
cancel2(options?.signal?.reason);
|
|
40772
40885
|
});
|
|
40773
40886
|
const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
40774
|
-
const timeoutHandler = () =>
|
|
40887
|
+
const timeoutHandler = () => cancel2(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
|
|
40775
40888
|
this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
|
|
40776
40889
|
const relatedTaskId = relatedTask?.taskId;
|
|
40777
40890
|
if (relatedTaskId) {
|
|
@@ -42966,7 +43079,7 @@ function handbackResult(record2, outPath, notes) {
|
|
|
42966
43079
|
};
|
|
42967
43080
|
}
|
|
42968
43081
|
function toolError(error62) {
|
|
42969
|
-
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
43082
|
+
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
42970
43083
|
const message = error62.message ?? String(error62);
|
|
42971
43084
|
return {
|
|
42972
43085
|
isError: true,
|
|
@@ -43105,14 +43218,20 @@ async function rerun(args, io) {
|
|
|
43105
43218
|
warn: (message) => io.err(`dunstan: ${message}
|
|
43106
43219
|
`)
|
|
43107
43220
|
});
|
|
43108
|
-
const pr = await readPullRequest(client, old.subject.repository, old.subject.pullRequest);
|
|
43109
43221
|
const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
|
|
43110
|
-
const
|
|
43111
|
-
|
|
43112
|
-
|
|
43113
|
-
|
|
43114
|
-
|
|
43115
|
-
|
|
43222
|
+
const excluded = [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])];
|
|
43223
|
+
const evidence = await atOnePullRequest(
|
|
43224
|
+
githubReaders(client),
|
|
43225
|
+
old.subject.repository,
|
|
43226
|
+
old.subject.pullRequest,
|
|
43227
|
+
void 0,
|
|
43228
|
+
(pr) => readEvidence(client, {
|
|
43229
|
+
repository: old.subject.repository,
|
|
43230
|
+
pullRequest: pr,
|
|
43231
|
+
block: old.block.status === "found" ? old.block.value : null,
|
|
43232
|
+
excludedCheckRunIds: excluded
|
|
43233
|
+
})
|
|
43234
|
+
);
|
|
43116
43235
|
const record2 = buildRecord({
|
|
43117
43236
|
checker: checkerIdentity(io.artifact),
|
|
43118
43237
|
report: old.report,
|
|
@@ -43473,7 +43592,7 @@ async function main(argv, io) {
|
|
|
43473
43592
|
io.err(`dunstan: ${e.message}
|
|
43474
43593
|
|
|
43475
43594
|
${USAGE}`);
|
|
43476
|
-
} else if (e instanceof PullRequestUnreadable || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43595
|
+
} else if (e instanceof PullRequestUnreadable || e instanceof PullRequestMoved || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43477
43596
|
io.err(`dunstan: ${e.message}
|
|
43478
43597
|
`);
|
|
43479
43598
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dunstan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"mcpName": "io.github.BargLabs/dunstan",
|
|
5
5
|
"description": "Holds a coding agent's completion report to the record: deterministic checks of a declared handback block against the repository's own metadata.",
|
|
6
6
|
"license": "AGPL-3.0-only",
|