dunstan 0.1.10 → 0.1.12

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.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/dunstan.mjs +200 -37
  3. 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.10 mcp`, the `dunstan` npm package at that
32
+ The plugin runs the MCP server with `npx -y dunstan@0.1.12 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 usage or internal error (`verify`: 0 verified, 1 not).
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.6";
8033
+ var SPEC_VERSION = "0.1.8";
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
- if (excluded.has(run.id)) checker.set(run.name, Math.max(checker.get(run.name) ?? 0, run.id));
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
- return [...latest.values()].filter((run) => !excluded.has(run.id) && run.id > (checker.get(run.name) ?? 0)).sort((a, b) => a.id - b.id);
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) {
@@ -8624,6 +8639,7 @@ function checkChecks(block, evidence) {
8624
8639
  let reason;
8625
8640
  if (section === void 0) reason = absentSection("checkRuns");
8626
8641
  else if (section.status !== "ok") reason = unreadReason(section);
8642
+ else if (section.commit !== evidence.pullRequest.headSha) reason = "check_runs_not_at_head";
8627
8643
  const runs = section !== void 0 && section.status === "ok" ? countedCheckRuns(section.runs, section.excludedIds) : [];
8628
8644
  if (checks.total !== void 0) {
8629
8645
  const field = "/checks/total";
@@ -8865,7 +8881,7 @@ function findEndOfCentralDirectory(bytes) {
8865
8881
  }
8866
8882
  throw new ZipError("no end of central directory record");
8867
8883
  }
8868
- function readZipEntry(bytes, path) {
8884
+ function readZipEntry(bytes, path, limit = MAX_ENTRY_BYTES) {
8869
8885
  if (bytes.length < 22) throw new ZipError("archive too short");
8870
8886
  const dv = view(bytes);
8871
8887
  const eocd = findEndOfCentralDirectory(bytes);
@@ -8890,7 +8906,7 @@ function readZipEntry(bytes, path) {
8890
8906
  if (compressedSize === 4294967295 || size === 4294967295 || localOffset === 4294967295) {
8891
8907
  throw new ZipError("ZIP64 is not supported");
8892
8908
  }
8893
- if (size > MAX_ENTRY_BYTES) throw new ZipError(`entry larger than ${MAX_ENTRY_BYTES} bytes`);
8909
+ if (size > limit) throw new ZipError(`entry larger than ${limit} bytes`);
8894
8910
  if (localOffset + 30 > bytes.length || dv.getUint32(localOffset, true) !== LOCAL) {
8895
8911
  throw new ZipError("bad local file header");
8896
8912
  }
@@ -8901,7 +8917,8 @@ function readZipEntry(bytes, path) {
8901
8917
  if (method === 0) return data;
8902
8918
  if (method === 8) {
8903
8919
  try {
8904
- return new Uint8Array(inflateRawSync(data, { maxOutputLength: MAX_ENTRY_BYTES }));
8920
+ const out = inflateRawSync(data, { maxOutputLength: limit });
8921
+ return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
8905
8922
  } catch (e) {
8906
8923
  throw new ZipError(`cannot inflate ${path}: ${e.message}`);
8907
8924
  }
@@ -8921,6 +8938,14 @@ var PullRequestUnreadable = class extends Error {
8921
8938
  this.name = "PullRequestUnreadable";
8922
8939
  }
8923
8940
  };
8941
+ var PullRequestMoved = class extends Error {
8942
+ constructor(repository, number4, moves) {
8943
+ super(
8944
+ `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)`
8945
+ );
8946
+ this.name = "PullRequestMoved";
8947
+ }
8948
+ };
8924
8949
  var ShapeError = class extends Error {
8925
8950
  };
8926
8951
  function obj(value) {
@@ -8956,11 +8981,18 @@ function timestamp(value) {
8956
8981
  }
8957
8982
  return text;
8958
8983
  }
8984
+ function timestampOrNull(value) {
8985
+ return value === null || value === void 0 ? null : timestamp(value);
8986
+ }
8959
8987
  function second(t) {
8960
8988
  return t.slice(0, 19);
8961
8989
  }
8962
- async function readPullRequest(client, repository, number4) {
8963
- const read = await client.read("pull_request", `/repos/${repository}/pulls/${number4}`);
8990
+ async function readPullRequest(client, repository, number4, options = {}) {
8991
+ const read = await client.read(
8992
+ "pull_request",
8993
+ `/repos/${repository}/pulls/${number4}`,
8994
+ options.record === false ? { record: false } : {}
8995
+ );
8964
8996
  if (!read.ok) {
8965
8997
  throw new PullRequestUnreadable(
8966
8998
  `cannot read pull request ${repository}#${number4} (${read.error}); no record is written`
@@ -8972,6 +9004,7 @@ async function readPullRequest(client, repository, number4) {
8972
9004
  const state = str(pr.state);
8973
9005
  if (state !== "open" && state !== "closed") throw new ShapeError();
8974
9006
  const base = obj(obj(pr.base).repo);
9007
+ const baseRef = str(obj(pr.base).ref);
8975
9008
  return {
8976
9009
  evidence: {
8977
9010
  status: "ok",
@@ -8985,6 +9018,7 @@ async function readPullRequest(client, repository, number4) {
8985
9018
  changedFiles: int(pr.changed_files)
8986
9019
  },
8987
9020
  repository: str(base.full_name),
9021
+ baseRef,
8988
9022
  body: strOrNull(pr.body)
8989
9023
  };
8990
9024
  } catch (e) {
@@ -8996,6 +9030,20 @@ async function readPullRequest(client, repository, number4) {
8996
9030
  throw e;
8997
9031
  }
8998
9032
  }
9033
+ function pullRequestMoves(first, again) {
9034
+ const state = (pr) => pr.evidence.merged ? "merged" : pr.evidence.state;
9035
+ const pairs = [
9036
+ ["head", first.evidence.headSha, again.evidence.headSha],
9037
+ ["base", first.baseRef, again.baseRef],
9038
+ ["state", state(first), state(again)],
9039
+ ["repository", first.repository, again.repository],
9040
+ ["mergedAt", first.evidence.mergedAt, again.evidence.mergedAt],
9041
+ ["mergeSha", first.evidence.mergeSha, again.evidence.mergeSha],
9042
+ ["changedFiles", first.evidence.changedFiles, again.evidence.changedFiles],
9043
+ ["number", first.evidence.number, again.evidence.number]
9044
+ ];
9045
+ return pairs.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name} ${a} -> ${b}`);
9046
+ }
8999
9047
  async function readAllPages(client, kind, pathFor, itemsOf, map2, options = {}) {
9000
9048
  const out = [];
9001
9049
  for (let page = 1; page <= MAX_PAGES; page++) {
@@ -9529,6 +9577,13 @@ async function readWorkflowRunsAt(client, repository, commit) {
9529
9577
  }
9530
9578
  return bySuite;
9531
9579
  }
9580
+ function withinLatestAttempt(jobs, run, createdAt) {
9581
+ if (createdAt === null) return false;
9582
+ const created = second(createdAt);
9583
+ return jobs.some(
9584
+ (job) => (job.attempt === null || job.attempt <= run.attempt) && job.startedAt !== null && job.completedAt !== null && second(job.startedAt) <= created && created <= second(job.completedAt)
9585
+ );
9586
+ }
9532
9587
  async function readTestRecord(client, repository, record2, headCommit) {
9533
9588
  const workflowFile = record2.workflow.split("/").at(-1);
9534
9589
  const runsPath = (page) => `/repos/${repository}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?head_sha=${headCommit}&per_page=${PER_PAGE}&page=${page}`;
@@ -9552,10 +9607,24 @@ async function readTestRecord(client, repository, record2, headCommit) {
9552
9607
  "workflow_runs",
9553
9608
  (page) => `/repos/${repository}/actions/runs/${run.id}/jobs?filter=latest&per_page=${PER_PAGE}&page=${page}`,
9554
9609
  (json2) => arr(obj(json2).jobs),
9555
- (item) => str(obj(item).name)
9610
+ (item) => {
9611
+ const j = obj(item);
9612
+ let attempt = null;
9613
+ if (j.run_attempt !== null && j.run_attempt !== void 0) {
9614
+ attempt = int(j.run_attempt);
9615
+ if (attempt < 1) throw new ShapeError();
9616
+ }
9617
+ return {
9618
+ name: str(j.name),
9619
+ attempt,
9620
+ startedAt: timestampOrNull(j.started_at),
9621
+ completedAt: timestampOrNull(j.completed_at)
9622
+ };
9623
+ }
9556
9624
  );
9557
9625
  if (jobs === null) return { record: record2, ...unreadable("workflow_runs") };
9558
- if (!jobs.includes(record2.job)) continue;
9626
+ const named = jobs.filter((j) => j.name === record2.job);
9627
+ if (named.length === 0) continue;
9559
9628
  const artifacts = await readAllPages(
9560
9629
  client,
9561
9630
  "workflow_runs",
@@ -9563,15 +9632,23 @@ async function readTestRecord(client, repository, record2, headCommit) {
9563
9632
  (json2) => arr(obj(json2).artifacts),
9564
9633
  (item) => {
9565
9634
  const a = obj(item);
9566
- return { id: int(a.id), name: str(a.name), expired: a.expired === true };
9635
+ return {
9636
+ id: int(a.id),
9637
+ name: str(a.name),
9638
+ expired: a.expired === true,
9639
+ createdAt: timestampOrNull(a.created_at)
9640
+ };
9567
9641
  }
9568
9642
  );
9569
9643
  if (artifacts === null) return { record: record2, ...unreadable("workflow_runs") };
9570
- const named = artifacts.filter((a) => a.name === record2.artifact).sort((a, b) => a.id - b.id);
9571
- if (named.length === 0) continue;
9572
- if (named.length > 1) return { record: record2, ...unreadable("artifact") };
9573
- const artifact = named[0];
9644
+ const uploaded = artifacts.filter((a) => a.name === record2.artifact).sort((a, b) => a.id - b.id);
9645
+ if (uploaded.length === 0) continue;
9646
+ if (uploaded.length > 1) return { record: record2, ...unreadable("artifact") };
9647
+ const artifact = uploaded[0];
9574
9648
  if (artifact.expired) return { record: record2, ...unreadable("artifact") };
9649
+ if (!withinLatestAttempt(named, run, artifact.createdAt)) {
9650
+ return { record: record2, ...unreadable("artifact") };
9651
+ }
9575
9652
  const zipPath = `/repos/${repository}/actions/artifacts/${artifact.id}/zip`;
9576
9653
  const download = await client.read("artifact", zipPath, {
9577
9654
  json: false,
@@ -9579,7 +9656,7 @@ async function readTestRecord(client, repository, record2, headCommit) {
9579
9656
  });
9580
9657
  if (!download.ok) return { record: record2, ...unreadable("artifact") };
9581
9658
  try {
9582
- const file2 = readZipEntry(download.body, record2.path);
9659
+ const file2 = readZipEntry(download.body, record2.path, client.maxEntryBytes);
9583
9660
  if (file2 === null) continue;
9584
9661
  const counts = countJunit(new TextDecoder("utf-8").decode(file2));
9585
9662
  runs.push({ runId: run.id, runAttempt: run.attempt, ...counts });
@@ -9775,7 +9852,7 @@ async function reportFromComment(client, repository, number4, login) {
9775
9852
  // src/record/checker.ts
9776
9853
  import { readFileSync } from "node:fs";
9777
9854
  var CHECKER_NAME = "dunstan";
9778
- var CHECKER_VERSION = "0.1.10";
9855
+ var CHECKER_VERSION = "0.1.12";
9779
9856
  function checkerIdentity(artifact) {
9780
9857
  return {
9781
9858
  name: CHECKER_NAME,
@@ -9952,7 +10029,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
9952
10029
  }
9953
10030
  }
9954
10031
  }
9955
- `, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
10032
+ `, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|check_runs_not_at_head|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
9956
10033
  "$schema": "https://json-schema.org/draft/2020-12/schema",
9957
10034
  "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
9958
10035
  "title": "Dunstan record, DRAFT 0.2.0",
@@ -10434,7 +10511,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10434
10511
  "advisoryNote": {
10435
10512
  "description": "agrees when the gate check would pass; differs:<reason> when it would fail; unanswered:<reason> when it would be unverifiable. The reasons are the 0.1 claim reasons. A file claim matched by name only is agrees_by_name, or unanswered:ambiguous_path when more than one changed path has that name. A file claim not among the changed files is unanswered:no_such_path when nothing is at its path at the head, unanswered:directory when a directory is, and unanswered:source_unreadable:path when the answer was not read (D.11.4). Never a verdict.",
10436
10513
  "type": "string",
10437
- "pattern": "^(?:agrees|agrees_by_name|unanswered:(?:ambiguous_path|no_such_path|directory|source_unreadable:path)|(?:differs|unanswered):(?:head_mismatch|declared_not_changed|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|checks_incomplete|no_check_runs|no_comparable_record_field|not_merged|premature|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs)))$"
10514
+ "pattern": "^(?:agrees|agrees_by_name|unanswered:(?:ambiguous_path|no_such_path|directory|source_unreadable:path|check_runs_not_at_head)|(?:differs|unanswered):(?:head_mismatch|declared_not_changed|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|checks_incomplete|no_check_runs|no_comparable_record_field|not_merged|premature|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs)))$"
10438
10515
  },
10439
10516
  "readerReason": {
10440
10517
  "type": "string",
@@ -10748,9 +10825,14 @@ import { parseArgs } from "node:util";
10748
10825
 
10749
10826
  // src/evidence/http.ts
10750
10827
  var DEFAULT_API = "https://api.github.com";
10828
+ var MAX_ARCHIVE_BYTES = MAX_ENTRY_BYTES + 8 * 1024 * 1024;
10829
+ var MAX_BODY_BYTES = 48 * 1024 * 1024;
10751
10830
  var EMPTY = new Uint8Array();
10752
10831
  var GitHubClient = class {
10753
10832
  sources = /* @__PURE__ */ new Map();
10833
+ maxArchiveBytes;
10834
+ maxEntryBytes;
10835
+ #allocate;
10754
10836
  #token;
10755
10837
  #baseUrl;
10756
10838
  #fetch;
@@ -10769,6 +10851,9 @@ var GitHubClient = class {
10769
10851
  `));
10770
10852
  this.#maxAttempts = options.maxAttempts ?? 4;
10771
10853
  this.#maxWaitMs = options.maxWaitMs ?? 6e4;
10854
+ this.maxArchiveBytes = options.maxArchiveBytes ?? MAX_ARCHIVE_BYTES;
10855
+ this.maxEntryBytes = options.maxEntryBytes ?? MAX_ENTRY_BYTES;
10856
+ this.#allocate = options.allocate ?? ((size) => new Uint8Array(size));
10772
10857
  }
10773
10858
  get hasToken() {
10774
10859
  return this.#token !== void 0 && this.#token !== "";
@@ -10784,10 +10869,19 @@ var GitHubClient = class {
10784
10869
  async read(kind, path, options = {}) {
10785
10870
  const method = options.method ?? "GET";
10786
10871
  const locator = options.locator ?? `${method} ${path}${options.locatorSuffix ?? ""}`;
10787
- const result = await this.#fetchWithRetry(method, path, options.requestBody, locator);
10872
+ const budget = kind === "artifact" ? this.maxArchiveBytes : MAX_BODY_BYTES;
10873
+ const result = await this.#fetchWithRetry(method, path, options.requestBody, locator, budget);
10788
10874
  let read;
10789
10875
  if (result.kind === "network") {
10790
10876
  read = { ok: false, status: null, error: "network", body: EMPTY };
10877
+ } else if (result.body === null) {
10878
+ const failed = result.status < 200 || result.status > 299;
10879
+ read = {
10880
+ ok: false,
10881
+ status: result.status,
10882
+ error: failed ? `http_${result.status}` : "parse",
10883
+ body: EMPTY
10884
+ };
10791
10885
  } else if (result.status < 200 || result.status > 299) {
10792
10886
  read = {
10793
10887
  ok: false,
@@ -10824,7 +10918,7 @@ var GitHubClient = class {
10824
10918
  const source = this.sources.get(locator);
10825
10919
  if (source !== void 0) source.error = "parse";
10826
10920
  }
10827
- async #fetchWithRetry(method, path, body, locator) {
10921
+ async #fetchWithRetry(method, path, body, locator, budget) {
10828
10922
  const headers = {
10829
10923
  accept: "application/vnd.github+json",
10830
10924
  "x-github-api-version": "2022-11-28",
@@ -10849,10 +10943,10 @@ var GitHubClient = class {
10849
10943
  this.#warn(`${locator}: network error after ${attempt} attempts (${e.message})`);
10850
10944
  return { kind: "network" };
10851
10945
  }
10852
- const bytes = new Uint8Array(await response.arrayBuffer());
10853
10946
  const status = response.status;
10854
10947
  const wait = retryWaitMs(response, status, this.#now(), attempt);
10855
10948
  if (wait !== void 0 && attempt < this.#maxAttempts && wait <= this.#maxWaitMs) {
10949
+ await cancel(response.body);
10856
10950
  await this.#sleep(wait);
10857
10951
  continue;
10858
10952
  }
@@ -10860,10 +10954,52 @@ var GitHubClient = class {
10860
10954
  const why = wait > this.#maxWaitMs ? `rate limited for ${Math.ceil(wait / 1e3)}s` : `${attempt} attempts`;
10861
10955
  this.#warn(`${locator}: giving up with HTTP ${status} (${why}); recorded as unreadable`);
10862
10956
  }
10957
+ const bytes = await readWithin(response, budget, this.#allocate);
10958
+ if (bytes === null) {
10959
+ this.#warn(
10960
+ `${locator}: answer over ${budget} bytes, not read in full; recorded as unreadable`
10961
+ );
10962
+ }
10863
10963
  return { kind: "response", status, body: bytes, etag: response.headers.get("etag") };
10864
10964
  }
10865
10965
  }
10866
10966
  };
10967
+ async function cancel(body) {
10968
+ await body?.cancel().catch(() => {
10969
+ });
10970
+ }
10971
+ var FIRST_CAPACITY = 64 * 1024;
10972
+ async function readWithin(response, budget, allocate) {
10973
+ const length = response.headers.get("content-length");
10974
+ const declared = length !== null && /^[0-9]+$/.test(length) ? Number(length) : null;
10975
+ if (declared !== null && declared > budget) {
10976
+ await cancel(response.body);
10977
+ return null;
10978
+ }
10979
+ if (response.body === null) return EMPTY;
10980
+ const reader = response.body.getReader();
10981
+ let bytes = EMPTY;
10982
+ let size = 0;
10983
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
10984
+ const needed = size + chunk.value.length;
10985
+ if (needed > budget) {
10986
+ await reader.cancel().catch(() => {
10987
+ });
10988
+ return null;
10989
+ }
10990
+ if (needed > bytes.length) {
10991
+ let capacity = Math.max(needed, 2 * bytes.length, FIRST_CAPACITY);
10992
+ if (2 * capacity > budget) capacity = budget;
10993
+ if (bytes === EMPTY && declared !== null && declared >= needed) capacity = declared;
10994
+ const grown = allocate(capacity);
10995
+ grown.set(bytes.subarray(0, size));
10996
+ bytes = grown;
10997
+ }
10998
+ bytes.set(chunk.value, size);
10999
+ size = needed;
11000
+ }
11001
+ return size === bytes.length ? bytes : bytes.subarray(0, size);
11002
+ }
10867
11003
  function backoffMs(attempt) {
10868
11004
  return Math.min(1e3 * 2 ** (attempt - 1), 8e3);
10869
11005
  }
@@ -13094,13 +13230,34 @@ function serializeRecord(record2) {
13094
13230
  // src/pipeline/check-pull-request.ts
13095
13231
  function githubReaders(client) {
13096
13232
  return {
13097
- pullRequest: (repository, number4) => readPullRequest(client, repository, number4),
13233
+ pullRequest: (repository, number4, options) => readPullRequest(client, repository, number4, options),
13098
13234
  evidence: (input2) => readEvidence(client, input2),
13099
- pathObjects: (input2) => readPathObjects(client, input2)
13235
+ pathObjects: (input2) => readPathObjects(client, input2),
13236
+ restart: () => client.sources.clear()
13100
13237
  };
13101
13238
  }
13239
+ async function atOnePullRequest(readers, repository, number4, first, collect) {
13240
+ let pr = first ?? await readers.pullRequest(repository, number4);
13241
+ for (let run = 1; ; run++) {
13242
+ const collected = await collect(pr);
13243
+ const again = await readers.pullRequest(repository, number4, { record: false });
13244
+ const moves = pullRequestMoves(pr, again);
13245
+ if (moves.length === 0) return collected;
13246
+ if (run === 2) throw new PullRequestMoved(repository, number4, moves);
13247
+ readers.restart?.();
13248
+ pr = await readers.pullRequest(repository, number4);
13249
+ }
13250
+ }
13102
13251
  async function checkPullRequest(input2) {
13103
- const pr = input2.pullRequest ?? await input2.readers.pullRequest(input2.repository, input2.number);
13252
+ return atOnePullRequest(
13253
+ input2.readers,
13254
+ input2.repository,
13255
+ input2.number,
13256
+ input2.pullRequest,
13257
+ (pr) => checkAt(input2, pr)
13258
+ );
13259
+ }
13260
+ async function checkAt(input2, pr) {
13104
13261
  const repository = pr.repository;
13105
13262
  const ownRuns = await input2.afterPullRequest?.(pr) ?? [];
13106
13263
  const report = await input2.report(pr);
@@ -40734,7 +40891,7 @@ var Protocol = class {
40734
40891
  }
40735
40892
  };
40736
40893
  }
40737
- const cancel = (reason) => {
40894
+ const cancel2 = (reason) => {
40738
40895
  this._responseHandlers.delete(messageId);
40739
40896
  this._progressHandlers.delete(messageId);
40740
40897
  this._cleanupTimeout(messageId);
@@ -40768,10 +40925,10 @@ var Protocol = class {
40768
40925
  }
40769
40926
  });
40770
40927
  options?.signal?.addEventListener("abort", () => {
40771
- cancel(options?.signal?.reason);
40928
+ cancel2(options?.signal?.reason);
40772
40929
  });
40773
40930
  const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
40774
- const timeoutHandler = () => cancel(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
40931
+ const timeoutHandler = () => cancel2(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
40775
40932
  this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
40776
40933
  const relatedTaskId = relatedTask?.taskId;
40777
40934
  if (relatedTaskId) {
@@ -42966,7 +43123,7 @@ function handbackResult(record2, outPath, notes) {
42966
43123
  };
42967
43124
  }
42968
43125
  function toolError(error62) {
42969
- const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof CanonicalizationError || typeof error62.code === "string";
43126
+ const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || typeof error62.code === "string";
42970
43127
  const message = error62.message ?? String(error62);
42971
43128
  return {
42972
43129
  isError: true,
@@ -43105,14 +43262,20 @@ async function rerun(args, io) {
43105
43262
  warn: (message) => io.err(`dunstan: ${message}
43106
43263
  `)
43107
43264
  });
43108
- const pr = await readPullRequest(client, old.subject.repository, old.subject.pullRequest);
43109
43265
  const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
43110
- const evidence = await readEvidence(client, {
43111
- repository: old.subject.repository,
43112
- pullRequest: pr,
43113
- block: old.block.status === "found" ? old.block.value : null,
43114
- excludedCheckRunIds: [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])]
43115
- });
43266
+ const excluded = [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])];
43267
+ const evidence = await atOnePullRequest(
43268
+ githubReaders(client),
43269
+ old.subject.repository,
43270
+ old.subject.pullRequest,
43271
+ void 0,
43272
+ (pr) => readEvidence(client, {
43273
+ repository: old.subject.repository,
43274
+ pullRequest: pr,
43275
+ block: old.block.status === "found" ? old.block.value : null,
43276
+ excludedCheckRunIds: excluded
43277
+ })
43278
+ );
43116
43279
  const record2 = buildRecord({
43117
43280
  checker: checkerIdentity(io.artifact),
43118
43281
  report: old.report,
@@ -43473,7 +43636,7 @@ async function main(argv, io) {
43473
43636
  io.err(`dunstan: ${e.message}
43474
43637
 
43475
43638
  ${USAGE}`);
43476
- } else if (e instanceof PullRequestUnreadable || e instanceof ReportUnavailable || e instanceof TemplateError) {
43639
+ } else if (e instanceof PullRequestUnreadable || e instanceof PullRequestMoved || e instanceof ReportUnavailable || e instanceof TemplateError) {
43477
43640
  io.err(`dunstan: ${e.message}
43478
43641
  `);
43479
43642
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dunstan",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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",