Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion .abcd/development/release-gate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ against the exact commit to be tagged:
the Direction-A semantic half of the iss-35 graduation: the brief's surface
*prose* (flags, sub-verbs, exit codes, schema fields, counts) vs. the shipped
binary's actual behaviour. The deterministic Direction-B half is the
`surface_coverage` `record-lint` rule and already runs in CI.
`surface_coverage` `record-lint` rule and already runs in CI. Its scope and
depth are pinned by [`manifest.json`](manifest.json) — see *Pinned inputs and
tiered depth* below.

## Recording the semantic verdict

Expand Down Expand Up @@ -77,6 +79,60 @@ filename: the `<gate>.json` at `.abcd/work/reviews/<sha>/` must carry
being copied across every gate's path to satisfy them all — each gate needs its
own receipt from its own detector.

## Pinned inputs and tiered depth

The cross-check runs against a **committed input manifest**,
[`manifest.json`](manifest.json), which pins the reproducibility inputs: the
17-document brief-doc list, the two directions (A: brief→surface, B:
surface→brief), the checker count (22 = 17 brief docs + 5 surfaces), and the
prompt (context plus both direction templates, with the prompt's own
`sha256`). Two honest runs of the same tier therefore mean the same thing —
the maintainer no longer chooses the scope per run. The
[`brief-surface-crosscheck.js`](brief-surface-crosscheck.js) detector consumes
this manifest as its input rather than composing an ad-hoc list.

Depth is **tiered by the release's impact class**:

- **`full`** — both directions over the whole brief-doc list — is required for a
**feature (additive) or breaking** release.
- **`shallow`** — Direction B only — is sufficient for a **patch (fix/internal)**
release.

The impact class is derived at gate time from the shipped records — the same
signal the version itself derives from (changelog-driven versioning, adr-37).
The version *number* alone cannot carry it while abcd is pre-1.0: an additive
feature and a fix both bump the patch component at 0.x, so only the records'
`impact` frontmatter tells a feature release apart from a patch one. The gate
reads the strongest impact in the cut between the newest release tag older than
the current CHANGELOG version and `HEAD`.

A manifest-era receipt therefore carries two further fields alongside the ones
above:

- **`manifestHash`** — the `sha256` of `manifest.json` the run pinned its inputs
against.
- **`tier`** — `full` or `shallow`, the depth the run used.

`receipt_gate` adds three **procedural** refusals once the manifest exists in the
release's content tree:

1. `manifestHash` does not equal `manifest.json`'s actual hash — the run's pinned
inputs are not this release's.
2. `tier` is insufficient for the release's impact class — a shallow receipt on a
feature or breaking release.
3. A `failing` entry carries no `disposition` — an un-triaged finding.

None of these judges a finding's **content or severity**. Confirmed findings
route to the maintainer, whose PROMOTE with recorded dispositions is the gate
(verifier-selects-gates-decide); the gate does not hard-block on a confirmed
major and keeps no never-worse ratchet across tiers.

**Era gating.** The manifest's presence in the armed content tree is the era
marker. The three refusals above arm only when `manifest.json` is present at the
gate's armed commit, so receipts written before the manifest existed — the
receipts committed under `.abcd/work/reviews/` for earlier releases — stay valid
for their own commits, judged by the checks that predate the manifest.

The `receipt_gate` rule is **disabled by default** — it must never fire on
ordinary PRs/pushes, only at release time — and is armed by `release.yml`, which
supplies the tagged commit and the required-gate list from the workflow (the
Expand Down
42 changes: 18 additions & 24 deletions .abcd/development/release-gate/brief-surface-crosscheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
// VSA-shaped receipt (see this directory's README.md and the design of record,
// ../plans/2026-07-11-iss35-semantic-release-gate.md).
//
// Invoke with args = { briefDocs: [<repo-relative paths>],
// surfaces: [{ name, kind, probe }] }.
// Invoke with args = the pinned input manifest
// (.abcd/development/release-gate/manifest.json): { briefDocs, surfaces, prompt,
// ... }. The manifest is the reproducibility anchor (iss-122) — the doc list,
// the directions, the checker count, and the prompt (context + both direction
// templates) are fixed there rather than composed ad hoc here, so two honest
// runs of the same tier mean the same thing. The receipt echoes the manifest's
// sha256 as manifestHash and the depth it ran at as tier; receipt_gate refuses a
// receipt whose manifestHash mismatches or whose tier is too shallow for the
// release's impact class.
export const meta = {
name: 'iss35-brief-surface-crosscheck',
description: 'Bidirectional brief↔surface reconciliation detector (iss-35 semantic gate)',
Expand All @@ -35,31 +42,18 @@ const FINDINGS = {
}, required: ['where','claim','reality','class'] } },
}, required: ['item','discrepancies'],
}
const CTX = `Repo root: the current working directory — use repo-relative paths
throughout, never absolute local paths. Ground truth is the SHIPPED surface,
verified empirically: build the binary (make build produces bin/abcd-<goos>-<arch>)
and run it (\`abcd --help\` and \`abcd <verb> --help\`), and list commands/abcd/
and skills/. abcd currently ships ZERO skills — the whole /abcd: surface is
commands under commands/abcd/ (ahoy, capture, consult, ingest, prepare-this-repo,
docs, history, launch, memory, version); skills/ is empty or absent. The brief's
surface chapters are .abcd/development/brief/04-surfaces/*.md and
05-internals/08-skills.md. Report DISCREPANCIES ONLY — where record and reality
disagree, or one side is missing. A brief row explicitly marked staged (its
Status column is "staged") / probe-only / later-phase is NOT a discrepancy; an
unmarked claim about a surface that does not exist IS. Do not fix anything.`
// Prompt text comes from the manifest, not from here: the context and both
// direction templates are pinned in manifest.json (with their own promptHash) so
// the prompt cannot drift between runs. The templates carry ${...} placeholders
// as literal text; fill them per checker.
const CTX = input.prompt.context
const fill = (tmpl, subs) =>
Object.entries(subs).reduce((s, [k, v]) => s.split('${' + k + '}').join(v), tmpl)
const briefFindings = input.briefDocs.map(doc => () => agent(
`${CTX}\n\nDirection A. Read ${doc} fully. Extract every checkable claim
about the shipped surface (verbs, sub-verbs, flags, skill names, counts,
file layouts, "abcd ships N ..." statements) and verify each against
reality. Return item="${doc}" and the discrepancy list.`,
`${CTX}\n\n${fill(input.prompt.directionA, { doc })}`,
{ label: `brief:${doc.split('/').pop()}`, phase: 'CheckBrief', schema: FINDINGS }))
const surfFindings = input.surfaces.map(s => () => agent(
`${CTX}\n\nDirection B. The real surface "${s.name}" (${s.kind}) exists:
inspect it (${s.probe}). Search the brief's surface chapters for its
documented home (grep .abcd/development/brief/). If no brief row documents
it — or the brief documents it under a wrong name/shape — that is a
discrepancy. Return item="${s.name}" and the discrepancy list (empty if
properly documented).`,
`${CTX}\n\n${fill(input.prompt.directionB, { 's.name': s.name, 's.kind': s.kind, 's.probe': s.probe })}`,
{ label: `surface:${s.name}`, phase: 'CheckSurface', schema: FINDINGS }))
const all = (await parallel([...briefFindings, ...surfFindings]))
.filter(Boolean)
Expand Down
74 changes: 74 additions & 0 deletions .abcd/development/release-gate/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"schemaVersion": 1,
"detector": "iss35-brief-surface-crosscheck",
"_comment": "Pinned input manifest for the iss-35 brief<->surface cross-check (iss-122). This is the reproducibility anchor: two honest runs of the same tier mean the same thing because the doc list, the directions, the checker count, and the prompt are fixed here rather than chosen per run. A receipt echoes this file's sha256 as manifestHash; receipt_gate refuses a receipt whose manifestHash does not match this file's actual hash. Compute the hash with `printf 'sha256:'; shasum -a 256 .abcd/development/release-gate/manifest.json | cut -d' ' -f1`.",
"tiers": {
"full": "both directions over the full brief-doc list; required for a feature (additive) or breaking release",
"shallow": "Direction B only (surface->brief); sufficient for a patch (fix/internal) release"
},
"directions": [
{
"id": "A",
"name": "brief->surface",
"description": "per brief doc: verify every surface claim against the shipped binary/tree"
},
{
"id": "B",
"name": "surface->brief",
"description": "per real surface: find its documented home in the brief"
}
],
"briefDocs": [
".abcd/development/brief/04-surfaces/01-ahoy.md",
".abcd/development/brief/04-surfaces/02-disembark.md",
".abcd/development/brief/04-surfaces/03-embark.md",
".abcd/development/brief/04-surfaces/04-launch.md",
".abcd/development/brief/04-surfaces/05-intent.md",
".abcd/development/brief/04-surfaces/06-capture.md",
".abcd/development/brief/04-surfaces/07-memory.md",
".abcd/development/brief/04-surfaces/08-abcd.md",
".abcd/development/brief/04-surfaces/09-reflect.md",
".abcd/development/brief/04-surfaces/10-docs.md",
".abcd/development/brief/04-surfaces/11-history.md",
".abcd/development/brief/04-surfaces/12-version.md",
".abcd/development/brief/04-surfaces/13-consult.md",
".abcd/development/brief/04-surfaces/14-ingest.md",
".abcd/development/brief/04-surfaces/15-prepare-this-repo.md",
".abcd/development/brief/04-surfaces/16-audit.md",
".abcd/development/brief/05-internals/08-skills.md"
],
"surfaces": [
{
"name": "CLI verb tree",
"kind": "binary",
"probe": "build bin/abcd-<goos>-<arch>; run `abcd --help` and `abcd <verb> --help`"
},
{
"name": "plugin command surface",
"kind": "commands",
"probe": "list commands/abcd/*.md (README excepted)"
},
{
"name": "plugin agent surface",
"kind": "agents",
"probe": "list agents/*.md (README excepted)"
},
{
"name": "hook entrypoints",
"kind": "hooks",
"probe": "read hooks/hooks.json"
},
{
"name": "skills surface",
"kind": "skills",
"probe": "list skills/ (abcd ships zero skills; the directory is empty or absent)"
}
],
"checkerCount": 22,
"promptHash": "sha256:6b4a388e596e159a04f73417af8c2d08699968dd01031b9c08aeac65596d8064",
"prompt": {
"context": "Repo root: the current working directory — use repo-relative paths\nthroughout, never absolute local paths. Ground truth is the SHIPPED surface,\nverified empirically: build the binary (make build produces bin/abcd-<goos>-<arch>)\nand run it (`abcd --help` and `abcd <verb> --help`), and list commands/abcd/\nand skills/. abcd currently ships ZERO skills — the whole /abcd: surface is\ncommands under commands/abcd/ (ahoy, capture, consult, ingest, prepare-this-repo,\ndocs, history, launch, memory, version); skills/ is empty or absent. The brief's\nsurface chapters are .abcd/development/brief/04-surfaces/*.md and\n05-internals/08-skills.md. Report DISCREPANCIES ONLY — where record and reality\ndisagree, or one side is missing. A brief row explicitly marked staged (its\nStatus column is \"staged\") / probe-only / later-phase is NOT a discrepancy; an\nunmarked claim about a surface that does not exist IS. Do not fix anything.",
"directionA": "Direction A. Read ${doc} fully. Extract every checkable claim\nabout the shipped surface (verbs, sub-verbs, flags, skill names, counts,\nfile layouts, \"abcd ships N ...\" statements) and verify each against\nreality. Return item=\"${doc}\" and the discrepancy list.",
"directionB": "Direction B. The real surface \"${s.name}\" (${s.kind}) exists:\ninspect it (${s.probe}). Search the brief's surface chapters for its\ndocumented home (grep .abcd/development/brief/). If no brief row documents\nit — or the brief documents it under a wrong name/shape — that is a\ndiscrepancy. Return item=\"${s.name}\" and the discrepancy list (empty if\nproperly documented)."
}
}
8 changes: 5 additions & 3 deletions .abcd/development/release-gate/receipt.example.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"_comment": "Example semantic-pass receipt (a Verification Summary Attestation). One file per gate, stored at .abcd/work/reviews/<commit-sha>/<gate>.json. The receipt_gate record-lint rule verifies, at release time, that subject.digest.gitCommit matches the target commit, verificationResult is PROMOTE, and judgeModel is a pinned snapshot (never a floating alias). A missing, mismatched, malformed, HOLD, or model-less receipt fails the release closed. In phase 4 the receipt is cosign-signed and the signature verified in-Go.",
"_comment": "Example semantic-pass receipt (a Verification Summary Attestation). One file per gate, stored at .abcd/work/reviews/<commit-sha>/<gate>.json. The receipt_gate record-lint rule verifies, at release time, that subject.digest.gitCommit matches the target commit, verificationResult is PROMOTE, judgeModel is a pinned snapshot (never a floating alias), and policy.detector names this gate. Once the pinned input manifest (manifest.json) exists in the release's content tree — the era marker — three further PROCEDURAL checks arm: manifestHash must equal manifest.json's actual sha256; tier must be sufficient for the release's impact class (a shallow receipt is refused on a feature/breaking release); and every entry in failing must carry a disposition. The gate never judges a finding's content or severity — confirmed findings route to the maintainer, whose PROMOTE with recorded dispositions is the gate. A missing, mismatched, malformed, HOLD, or model-less receipt fails the release closed. In phase 4 the receipt is cosign-signed and the signature verified in-Go.",
"subject": {
"digest": { "gitCommit": "0123456789abcdef0123456789abcdef01234567" }
},
"verifier": { "id": "maintainer@release" },
"timeVerified": "2026-07-11T12:00:00Z",
"timeVerified": "2026-07-25T12:00:00Z",
"verificationResult": "PROMOTE",
"judgeModel": "claude-opus-4-8",
"tier": "full",
"manifestHash": "sha256:c74d40824c9d39689715b735f894b6bc4ea08c2891c510b34f4c8f1a7742d6f8",
"policy": {
"detector": "iss35-brief-surface-crosscheck",
"version": "1",
"promptHash": "sha256:",
"promptHash": "sha256:6b4a388e596e159a04f73417af8c2d08699968dd01031b9c08aeac65596d8064",
"briefVersion": "…"
},
"categories": { "false-claim": 0, "undocumented-surface": 0, "fictional-layout": 0, "criterion-violation": 0, "stale-count": 0 },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
schema_version: 1
id: "iss-130"
slug: "manifest-deletion-disarms-crosscheck-refusals"
severity: "minor"
category: "tech-debt"
source: "impl-review"
found_during: "iss-122 implementation review (2026-07-24 run queue, burst 6)"
found_at: ".abcd/development/release-gate/manifest.json"
---

era-gating tradeoff residue from iss-122: deleting release-gate/manifest.json from the content tree disarms the three new procedural refusals (manifest presence IS the era marker, per the decided design F) — the backstop belongs in release.yml (require the manifest to exist for any release newer than v0.4.0), which is CI config and needs maintainer sign-off; until then a committer who removes the manifest silently reverts the gate to pre-manifest rules
12 changes: 12 additions & 0 deletions .abcd/work/issues/open/iss-131-receipt-gate-hardening-nitpicks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
schema_version: 1
id: "iss-131"
slug: "receipt-gate-hardening-nitpicks"
severity: "nitpick"
category: "tech-debt"
source: "impl-review"
found_during: "iss-122 reviews (2026-07-24 run queue, burst 6)"
found_at: "internal/core/lint/lint.go"
---

gate-hardening nitpicks from the iss-122 reviews, none blocking: (1) the manifest read in receipt_gate uses unbounded os.ReadFile where the sibling convention is fsutil.ReadGuarded (trusted committed file, self-DoS only — but it is the unhardened sibling the bughunt spine says to sweep); (2) receipt.example.json's manifestHash is not test-pinned against manifest.json so a manifest edit silently stales the example; (3) receipt parsing tolerates duplicate JSON keys (Go last-wins) — an audit-legibility gap under the attestation trust model, not an escalation
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ category: "tech-debt"
source: "agent-observation"
found_during: "v0.4.0 release gate"
found_at: ".abcd/development/release-gate/brief-surface-crosscheck.js"
resolution: "Pinned the iss-35 crosscheck gate's scope and depth: a committed input manifest (manifest.json) fixes the 17 brief docs, both directions, 22 checkers, and the prompt hash; receipt_gate gains additive tier and manifestHash fields and three procedural refusals (manifestHash mismatch, tier insufficient for the release's impact class, undispositioned finding), all era-gated on the manifest's presence so historical receipts stay valid. Impact class derived at gate time from the shipped records (changelog-driven versioning), since the version number alone cannot tell a pre-1.0 feature from a fix."
impact: internal
---

The iss35 crosscheck's scope and depth are unpinned, so the gate is not reproducible: v0.3.0's receipt records zero findings four days ago while a full-depth run (17 brief docs, both directions, 22 checkers) returns 102 discrepancies, and the receipt's own promptHash field is the literal 'no-pinned-prompt' admission. The maintainer choosing briefDocs per run means two honest runs of the same gate can disagree by two orders of magnitude; the gate needs a pinned input manifest and depth so a PROMOTE means the same thing every release.
Expand Down
Loading