dunstan 0.1.9 → 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.
Files changed (3) hide show
  1. package/README.md +14 -3
  2. package/dist/dunstan.mjs +200 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,9 @@ checks. It writes a record anyone can re-run on their own machine and get the sa
13
13
 
14
14
  The claim format, the block an agent declares and the record Dunstan writes, is an open
15
15
  specification: [`spec/claim-format.md`](spec/claim-format.md), with JSON Schemas in `spec/schema/`
16
- and examples in `spec/examples/`.
16
+ and examples in `spec/examples/`. A record is an in-toto Statement. How it reads in in-toto, SLSA
17
+ and PER terms, and what does not carry over: [`docs/interop.md`](docs/interop.md) (informative; no
18
+ export exists).
17
19
 
18
20
  ## Use with Claude
19
21
 
@@ -27,7 +29,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
27
29
  /plugin install dunstan@barglabs
28
30
  ```
29
31
 
30
- The plugin runs the MCP server with `npx -y dunstan@0.1.9 mcp`, the `dunstan` npm package at that
32
+ The plugin runs the MCP server with `npx -y dunstan@0.1.11 mcp`, the `dunstan` npm package at that
31
33
  exact version: one self-contained file, no dependencies.
32
34
 
33
35
  **The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
@@ -74,7 +76,9 @@ dist/dunstan.mjs verify record.json # offline: recompute claims, verdict and d
74
76
  dist/dunstan.mjs rerun record.json # online: re-read the sources, report evidence_changed
75
77
  ```
76
78
 
77
- 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).
78
82
  `--sign-key <path> --signer <principal>` writes the record as its JCS bytes with a detached SSH
79
83
  signature; `verify --sig <file> --allowed-signers <file>` checks it.
80
84
 
@@ -85,6 +89,13 @@ agent calls with its block before it says done, beside `dunstan_suggest_declarat
85
89
  `checker.digest` in a record is the SHA-256 of `dist/dunstan.mjs`, the single-file bundle that ran:
86
90
  the checker code, its dependencies and both schemas from `spec/schema/`, inlined at build time.
87
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
+
88
99
  ## Declare before done
89
100
 
90
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.5";
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,47 @@ 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);
8618
+ }
8619
+ var NOT_FAILED_CONCLUSIONS = ["success", "skipped", "neutral"];
8620
+ function booleanRow(field, declared, reason, runs, failed, mismatch, unsettled) {
8621
+ if (reason !== void 0) return unverifiable("count", field, declared, reason);
8622
+ if (runs.length === 0) return unverifiable("count", field, declared, "no_check_runs");
8623
+ let observed;
8624
+ if (runs.some((r) => r.status === "completed" && failed(r.conclusion))) observed = false;
8625
+ else if (runs.some((r) => r.status !== "completed")) {
8626
+ return unverifiable("count", field, declared, "checks_incomplete");
8627
+ } else {
8628
+ const why = unsettled?.(runs);
8629
+ if (why !== void 0) return unverifiable("count", field, declared, why);
8630
+ observed = true;
8631
+ }
8632
+ return observed === declared ? pass("count", field, declared, observed) : fail("count", field, declared, observed, mismatch);
8603
8633
  }
8604
8634
  function checkChecks(block, evidence) {
8605
8635
  const checks = block.checks;
@@ -8618,26 +8648,29 @@ function checkChecks(block, evidence) {
8618
8648
  else rows.push(fail("count", field, checks.total, runs.length, "count_mismatch"));
8619
8649
  }
8620
8650
  if (checks.allSucceeded !== void 0) {
8621
- const field = "/checks/allSucceeded";
8622
- const declared = checks.allSucceeded;
8623
- if (reason !== void 0) {
8624
- rows.push(unverifiable("count", field, declared, reason));
8625
- } else if (runs.length === 0) {
8626
- rows.push(unverifiable("count", field, declared, "no_check_runs"));
8627
- } else {
8628
- let observed;
8629
- if (runs.some((r) => r.status === "completed" && r.conclusion !== "success"))
8630
- observed = false;
8631
- else if (runs.some((r) => r.status !== "completed")) observed = void 0;
8632
- else observed = true;
8633
- if (observed === void 0) {
8634
- rows.push(unverifiable("count", field, declared, "checks_incomplete"));
8635
- } else if (observed === declared) {
8636
- rows.push(pass("count", field, declared, observed));
8637
- } else {
8638
- rows.push(fail("count", field, declared, observed, "all_succeeded_mismatch"));
8639
- }
8640
- }
8651
+ rows.push(
8652
+ booleanRow(
8653
+ "/checks/allSucceeded",
8654
+ checks.allSucceeded,
8655
+ reason,
8656
+ runs,
8657
+ (conclusion) => conclusion !== "success",
8658
+ "all_succeeded_mismatch"
8659
+ )
8660
+ );
8661
+ }
8662
+ if (checks.noneFailed !== void 0) {
8663
+ rows.push(
8664
+ booleanRow(
8665
+ "/checks/noneFailed",
8666
+ checks.noneFailed,
8667
+ reason,
8668
+ runs,
8669
+ (conclusion) => conclusion === null || !NOT_FAILED_CONCLUSIONS.includes(conclusion),
8670
+ "none_failed_mismatch",
8671
+ (completed) => completed.some((r) => r.conclusion === "success") ? void 0 : "no_succeeded_check_runs"
8672
+ )
8673
+ );
8641
8674
  }
8642
8675
  return rows;
8643
8676
  }
@@ -8903,6 +8936,14 @@ var PullRequestUnreadable = class extends Error {
8903
8936
  this.name = "PullRequestUnreadable";
8904
8937
  }
8905
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
+ };
8906
8947
  var ShapeError = class extends Error {
8907
8948
  };
8908
8949
  function obj(value) {
@@ -8941,8 +8982,12 @@ function timestamp(value) {
8941
8982
  function second(t) {
8942
8983
  return t.slice(0, 19);
8943
8984
  }
8944
- async function readPullRequest(client, repository, number4) {
8945
- const read = await client.read("pull_request", `/repos/${repository}/pulls/${number4}`);
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
+ );
8946
8991
  if (!read.ok) {
8947
8992
  throw new PullRequestUnreadable(
8948
8993
  `cannot read pull request ${repository}#${number4} (${read.error}); no record is written`
@@ -8954,6 +8999,7 @@ async function readPullRequest(client, repository, number4) {
8954
8999
  const state = str(pr.state);
8955
9000
  if (state !== "open" && state !== "closed") throw new ShapeError();
8956
9001
  const base = obj(obj(pr.base).repo);
9002
+ const baseRef = str(obj(pr.base).ref);
8957
9003
  return {
8958
9004
  evidence: {
8959
9005
  status: "ok",
@@ -8967,6 +9013,7 @@ async function readPullRequest(client, repository, number4) {
8967
9013
  changedFiles: int(pr.changed_files)
8968
9014
  },
8969
9015
  repository: str(base.full_name),
9016
+ baseRef,
8970
9017
  body: strOrNull(pr.body)
8971
9018
  };
8972
9019
  } catch (e) {
@@ -8978,6 +9025,20 @@ async function readPullRequest(client, repository, number4) {
8978
9025
  throw e;
8979
9026
  }
8980
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
+ }
8981
9042
  async function readAllPages(client, kind, pathFor, itemsOf, map2, options = {}) {
8982
9043
  const out = [];
8983
9044
  for (let page = 1; page <= MAX_PAGES; page++) {
@@ -9712,10 +9773,15 @@ function reportFromPullRequestBody(repository, pr) {
9712
9773
  }
9713
9774
  async function reportFromComment(client, repository, number4, login) {
9714
9775
  let latest;
9715
- for (let page = 1; page <= 100; page++) {
9776
+ for (let page = 1; ; page++) {
9777
+ if (page > MAX_PAGES) {
9778
+ throw new ReportUnavailable(
9779
+ `cannot list comments on ${repository}#${number4} (more than ${MAX_PAGES} pages)`
9780
+ );
9781
+ }
9716
9782
  const read = await client.read(
9717
9783
  "pull_request",
9718
- `/repos/${repository}/issues/${number4}/comments?per_page=100&page=${page}`,
9784
+ `/repos/${repository}/issues/${number4}/comments?per_page=${PER_PAGE}&page=${page}`,
9719
9785
  { record: false }
9720
9786
  );
9721
9787
  if (!read.ok) {
@@ -9735,7 +9801,7 @@ async function reportFromComment(client, repository, number4, login) {
9735
9801
  latest = { id, createdAt, body: typeof item.body === "string" ? item.body : "" };
9736
9802
  }
9737
9803
  }
9738
- if (read.json.length < 100) break;
9804
+ if (read.json.length < PER_PAGE) break;
9739
9805
  }
9740
9806
  if (latest === void 0) {
9741
9807
  throw new ReportUnavailable(`${login} has no comment on ${repository}#${number4}`);
@@ -9752,7 +9818,7 @@ async function reportFromComment(client, repository, number4, login) {
9752
9818
  // src/record/checker.ts
9753
9819
  import { readFileSync } from "node:fs";
9754
9820
  var CHECKER_NAME = "dunstan";
9755
- var CHECKER_VERSION = "0.1.9";
9821
+ var CHECKER_VERSION = "0.1.11";
9756
9822
  function checkerIdentity(artifact) {
9757
9823
  return {
9758
9824
  name: CHECKER_NAME,
@@ -9803,7 +9869,11 @@ var FILES = { "handback-block-0.1.schema.json": `{
9803
9869
  "additionalProperties": false,
9804
9870
  "properties": {
9805
9871
  "total": { "type": "integer", "minimum": 0 },
9806
- "allSucceeded": { "type": "boolean" }
9872
+ "allSucceeded": { "type": "boolean" },
9873
+ "noneFailed": {
9874
+ "description": "No counted check run concluded other than success, skipped or neutral, and at least one succeeded. Added in spec 0.1.6.",
9875
+ "type": "boolean"
9876
+ }
9807
9877
  }
9808
9878
  },
9809
9879
  "references": {
@@ -9925,7 +9995,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
9925
9995
  }
9926
9996
  }
9927
9997
  }
9928
- `, "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|checks_incomplete|no_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": `{
9998
+ `, "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": `{
9929
9999
  "$schema": "https://json-schema.org/draft/2020-12/schema",
9930
10000
  "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
9931
10001
  "title": "Dunstan record, DRAFT 0.2.0",
@@ -10411,7 +10481,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10411
10481
  },
10412
10482
  "readerReason": {
10413
10483
  "type": "string",
10414
- "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
10484
+ "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|workflow_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
10415
10485
  }
10416
10486
  }
10417
10487
  }
@@ -10702,7 +10772,8 @@ function templateBlock(options) {
10702
10772
  );
10703
10773
  }
10704
10774
  notes.push(
10705
- "references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged."
10775
+ "references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged.",
10776
+ "checks are not filled: declare them yourself once CI has finished. checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded."
10706
10777
  );
10707
10778
  return { block, branch, base, mergeBase, notes };
10708
10779
  }
@@ -10720,6 +10791,8 @@ import { parseArgs } from "node:util";
10720
10791
 
10721
10792
  // src/evidence/http.ts
10722
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;
10723
10796
  var EMPTY = new Uint8Array();
10724
10797
  var GitHubClient = class {
10725
10798
  sources = /* @__PURE__ */ new Map();
@@ -10756,10 +10829,19 @@ var GitHubClient = class {
10756
10829
  async read(kind, path, options = {}) {
10757
10830
  const method = options.method ?? "GET";
10758
10831
  const locator = options.locator ?? `${method} ${path}${options.locatorSuffix ?? ""}`;
10759
- const result = await this.#fetchWithRetry(method, path, options.requestBody, locator);
10832
+ const budget = kind === "artifact" ? MAX_ARCHIVE_BYTES : MAX_BODY_BYTES;
10833
+ const result = await this.#fetchWithRetry(method, path, options.requestBody, locator, budget);
10760
10834
  let read;
10761
10835
  if (result.kind === "network") {
10762
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
+ };
10763
10845
  } else if (result.status < 200 || result.status > 299) {
10764
10846
  read = {
10765
10847
  ok: false,
@@ -10796,7 +10878,7 @@ var GitHubClient = class {
10796
10878
  const source = this.sources.get(locator);
10797
10879
  if (source !== void 0) source.error = "parse";
10798
10880
  }
10799
- async #fetchWithRetry(method, path, body, locator) {
10881
+ async #fetchWithRetry(method, path, body, locator, budget) {
10800
10882
  const headers = {
10801
10883
  accept: "application/vnd.github+json",
10802
10884
  "x-github-api-version": "2022-11-28",
@@ -10821,10 +10903,10 @@ var GitHubClient = class {
10821
10903
  this.#warn(`${locator}: network error after ${attempt} attempts (${e.message})`);
10822
10904
  return { kind: "network" };
10823
10905
  }
10824
- const bytes = new Uint8Array(await response.arrayBuffer());
10825
10906
  const status = response.status;
10826
10907
  const wait = retryWaitMs(response, status, this.#now(), attempt);
10827
10908
  if (wait !== void 0 && attempt < this.#maxAttempts && wait <= this.#maxWaitMs) {
10909
+ await cancel(response.body);
10828
10910
  await this.#sleep(wait);
10829
10911
  continue;
10830
10912
  }
@@ -10832,10 +10914,48 @@ var GitHubClient = class {
10832
10914
  const why = wait > this.#maxWaitMs ? `rate limited for ${Math.ceil(wait / 1e3)}s` : `${attempt} attempts`;
10833
10915
  this.#warn(`${locator}: giving up with HTTP ${status} (${why}); recorded as unreadable`);
10834
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
+ }
10835
10923
  return { kind: "response", status, body: bytes, etag: response.headers.get("etag") };
10836
10924
  }
10837
10925
  }
10838
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
+ }
10839
10959
  function backoffMs(attempt) {
10840
10960
  return Math.min(1e3 * 2 ** (attempt - 1), 8e3);
10841
10961
  }
@@ -13066,13 +13186,34 @@ function serializeRecord(record2) {
13066
13186
  // src/pipeline/check-pull-request.ts
13067
13187
  function githubReaders(client) {
13068
13188
  return {
13069
- pullRequest: (repository, number4) => readPullRequest(client, repository, number4),
13189
+ pullRequest: (repository, number4, options) => readPullRequest(client, repository, number4, options),
13070
13190
  evidence: (input2) => readEvidence(client, input2),
13071
- pathObjects: (input2) => readPathObjects(client, input2)
13191
+ pathObjects: (input2) => readPathObjects(client, input2),
13192
+ restart: () => client.sources.clear()
13072
13193
  };
13073
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
+ }
13074
13207
  async function checkPullRequest(input2) {
13075
- const pr = input2.pullRequest ?? await input2.readers.pullRequest(input2.repository, input2.number);
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) {
13076
13217
  const repository = pr.repository;
13077
13218
  const ownRuns = await input2.afterPullRequest?.(pr) ?? [];
13078
13219
  const report = await input2.report(pr);
@@ -40706,7 +40847,7 @@ var Protocol = class {
40706
40847
  }
40707
40848
  };
40708
40849
  }
40709
- const cancel = (reason) => {
40850
+ const cancel2 = (reason) => {
40710
40851
  this._responseHandlers.delete(messageId);
40711
40852
  this._progressHandlers.delete(messageId);
40712
40853
  this._cleanupTimeout(messageId);
@@ -40740,10 +40881,10 @@ var Protocol = class {
40740
40881
  }
40741
40882
  });
40742
40883
  options?.signal?.addEventListener("abort", () => {
40743
- cancel(options?.signal?.reason);
40884
+ cancel2(options?.signal?.reason);
40744
40885
  });
40745
40886
  const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
40746
- const timeoutHandler = () => cancel(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
40887
+ const timeoutHandler = () => cancel2(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
40747
40888
  this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
40748
40889
  const relatedTaskId = relatedTask?.taskId;
40749
40890
  if (relatedTaskId) {
@@ -42858,7 +42999,7 @@ var InputError = class extends Error {
42858
42999
  };
42859
43000
  var INSTRUCTIONS = `Before you say a task is done: generate your dunstan-handback block with \`dunstan template\` and add your test and reference claims; call ${SUGGEST_TOOL_NAME} with your report and declare or reword each sentence it lists; then call ${TOOL_NAME} with your pull request and the block. If the verdict is not pass, report the rows as they are; never edit the block to make it pass without changing the work. The check reads only what the block declares; the prose is yours to bring into the block.`;
42860
43001
  var SUGGEST_DESCRIPTION = `List the sentences in your report's prose that look like claims your dunstan-handback block does not declare, before you say done. Give report, the report text. For each, declare it in the block, or reword it if it is not a claim about this PR. Offline: needs no pull request, reads nothing and writes nothing. Never a verdict: nothing passes or fails, and nothing is written to a record. The same text as \`dunstan suggest\`.`;
42861
- var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
43002
+ var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Of the checks claims, checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
42862
43003
  function reportOfBlock(block) {
42863
43004
  const text = canonicalize(block);
42864
43005
  return {
@@ -42938,7 +43079,7 @@ function handbackResult(record2, outPath, notes) {
42938
43079
  };
42939
43080
  }
42940
43081
  function toolError(error62) {
42941
- 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";
42942
43083
  const message = error62.message ?? String(error62);
42943
43084
  return {
42944
43085
  isError: true,
@@ -43077,14 +43218,20 @@ async function rerun(args, io) {
43077
43218
  warn: (message) => io.err(`dunstan: ${message}
43078
43219
  `)
43079
43220
  });
43080
- const pr = await readPullRequest(client, old.subject.repository, old.subject.pullRequest);
43081
43221
  const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
43082
- const evidence = await readEvidence(client, {
43083
- repository: old.subject.repository,
43084
- pullRequest: pr,
43085
- block: old.block.status === "found" ? old.block.value : null,
43086
- excludedCheckRunIds: [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])]
43087
- });
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
+ );
43088
43235
  const record2 = buildRecord({
43089
43236
  checker: checkerIdentity(io.artifact),
43090
43237
  report: old.report,
@@ -43403,7 +43550,7 @@ verify offline: recomputes claims, verdict and digests from the record and co
43403
43550
  rerun online: re-reads the sources for the recorded subject and block; writes a new record.
43404
43551
  template local git: prints a dunstan-handback block for HEAD, its files against the merge base
43405
43552
  with --base (default: the remote's default branch). Tests only from a JUnit file and
43406
- the CI record named with it; references and mergedAt never (docs/agents.md).
43553
+ the CI record named with it; references, checks and mergedAt never (docs/agents.md).
43407
43554
  suggest offline: lists sentences in the report's prose (a file, or stdin) that look like claims
43408
43555
  its block does not declare, to declare or reword. Never a verdict (docs/agents.md).
43409
43556
  mcp serves dunstan_check_handback and dunstan_suggest_declarations over stdio
@@ -43445,7 +43592,7 @@ async function main(argv, io) {
43445
43592
  io.err(`dunstan: ${e.message}
43446
43593
 
43447
43594
  ${USAGE}`);
43448
- } 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) {
43449
43596
  io.err(`dunstan: ${e.message}
43450
43597
  `);
43451
43598
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dunstan",
3
- "version": "0.1.9",
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",