Conversation
afogel
force-pushed
the
slice/v8
branch
2 times, most recently
from
August 10, 2026 06:17
4bebe5c to
8d69dfc
Compare
afogel
force-pushed
the
slice/v8
branch
2 times, most recently
from
August 10, 2026 06:28
f8616c5 to
48d586a
Compare
afogel
force-pushed
the
slice/v8
branch
3 times, most recently
from
August 10, 2026 14:32
b18ba1f to
35f0222
Compare
afogel
force-pushed
the
slice/v8
branch
2 times, most recently
from
August 11, 2026 10:51
15c7957 to
b8917ae
Compare
afogel
commented
Aug 13, 2026
afogel
left a comment
Owner
Author
There was a problem hiding this comment.
Sandi Metz review — V8 scaffold
9-line README. Keep the Surface stem already in N45/N46; do not reuse V3’s drift or V7’s cell.
… gate `drift` is not a free word here. `policy/lib/drift.rego` is AGT's stock gate, `input.annotations.drift_score` is the input it reads, `drift.warn_threshold` under `data.agt.defaults.config` is the number it compares against, `drift_detected` is the reason it emits, and V3 shipped `policy/manifest.drift.yaml` so that gate could fire at all. All of that is about a model's behaviour moving. V8 is about the upstream contract moving -- AGT's schemas and enums at `main` against the ref `agt.lock` pins -- which is a different subject wearing the same English word, and the collision lands before any code exists to inherit it: the first file would be called `drift.ts` and read as the gate. The slice is now **Upstream contract watch** in its README H1, in the master doc's SS V8 heading and index row, and in the shaping doc's C6 part. U31 is the **surface-diff detail** in both affordance tables and in the breadboard's own node label, named for what it renders rather than for the phenomenon. P5's part list says "upstream contract check", and the root README's planned-claim row says an upstream contract change. C6 also stopped saying a red cell names the changed field. A cell is V7's (intervention point x AGT verdict); what this part produces is a surface diff, and the demo sentences follow in the next commit. No V3 use moved. `policy/manifest.drift.yaml` is a tracked file, the Rego reads `input.annotations.drift_score`, and requirements R2.5/R2.6 say "drift" because that is the phenomenon they were written about -- none of them names this slice or anything it builds. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
A schema or enum change is not a V7 coverage cell. A cell pairs an intervention point with an AGT verdict; N41-N44 measure those and N47 `renderCoverageMatrix()` renders them. What V8 produces is a named surface, a named field, and what that field was against what it is now -- a `SurfaceDiff`, rendered by N53 `renderUpstreamDiff()` into U31. The demo sentence said "turns a cell red" in three places (the index row, SS V8, this README) and the SS V5 residual that files a hookmap/registry check at V8 said it in a fourth. All four now say what actually happens, and SS V8 records the restatement rather than leaving it to a diff. **`PinnedSurfaces` and `UpstreamSurfaces` are frozen as a twin pair.** Same eight surfaces, two sources: the ref `agt.lock` records (S11) and `main` (S12). A single `Surfaces` type serving both is the shape in which a run that read the pinned side twice still reports a clean diff. **`diffSurfaces(pinned, upstream)` is told both snapshots.** It does not open `agt.lock`, resolve a ref, or reach the network. Both affordance tables now say so at the row, so an implementer meets the constraint where the function is defined. The precedent is in this stack: a review round ended with `governStep` being handed the tool its caller had already checked (`GovernStepInput.scopedTool`) instead of deriving it. **S11 was missing from SS V8's table, and it is the pinned half of that pair.** Detail C's shared-store row has named N46 a reader of `agt.lock` since the initial shaping commit, and the breadboard has carried `S11 -.-> N46` just as long -- only the slice table was silent, the same asymmetry SS V7 just closed for S10. The row lands in V7's form, and the README's affordance line carries all six with the store halves named: S12 declared here, S11 shared and read. Also: SS V8's note now states the README's affordance line as a fact, because as of this commit it is one. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
The rename reached four statements of the demo claim and missed a fifth in the V5 runbook, where the hookmap-tools residual is filed at V8 and described it as turning a cell red -- the conflation with V7's coverage cell that this slice's own commitment exists to prevent. The Fit Check note and the requirements-to-affordances row cite C6 and V8's chain, so they move with the slice; R2.5 and R2.6 themselves keep the word, which they use as ordinary English for divergence rather than as a name.
The requirement table still described this slice's subject with the noun the slice was renamed off. R2.5 and R2.6 are the two requirements the upstream contract watch exists to satisfy, and they read "drift surfaces as a failing case" and "a drift failure names what changed" -- the word that already belongs to AGT's behaviour gate, three files of which ship. They now say what moves: a contract surface. Every use that really is about the behaviour gate is untouched. The freeze gains a fifth commitment, because the conformance harness just spent the stem: it clones the PINNED ref for its policy-input schema check, so both names now live in one package. That clone is PINNED_AGT_CLONE, leaving upstream free for the store this slice fetches into -- a differ told "upstream" twice is the exact failure commitment 2 describes.
The plan projects §V8's six affordances into eight tasks. Planning measured AGT at the pinned ref rather than reading it, and that produced three corrections, all amended into the slices doc here rather than left for the implementer: Eight surfaces are not eight files. Two of them are enums living inside documents already fetched, and one has no document at all -- it is the set of cfg.<key> reads in agt_default.rego. A fetcher written from the prose alone cannot express three of the eight. The wire schemas exist in two copies at the pinned ref, byte-identical there. The watch reads the spec/ copy and a divergence between the copies is a recorded blind spot. The tools-registry cross-check cannot close the failure it was filed for. The manifest registers run_shell, Bash and bash -- one per host, deliberately -- so OpenCode's tools: [bash] recased to [Bash] passes it. The open question inverts: the manifest names more than any one host dispatches. Separately, R2.5 asks for a failing case, not a diff, and the chain §V8 describes delivers only the diff. The reachable half is re-asking V7's schema question of main; the matrix-against-main half needs the Node SDK built from source and is a slice of its own. The Fit Check now says so. Slice: #9 Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…ejection through the run Slice: #9
…unbook's own voice The prose stitching together the negative-control captures in docs/demos/v8-runbook.md carried an internal task reference and role name that had nothing for a published reader to resolve against. The genuine captured output -- the byte comparison lines, the watch's own run lines and tables, the exit codes -- is untouched and re-verified byte-for-byte against its source; only the narration around it is rewritten, restructured to match the prose-then-fenced-block pattern the rest of the file already uses. Slice: #9
…nt the compared refs readSurfaces' readJson moved JSON.parse outside its try/catch, so a malformed watched document surfaced as an anonymous JSON.parse error naming none of the five JSON surfaces it could have been. Both the read and the parse now share one catch that always names the surface, matching the fix already made for a malformed hookmap. The scheduled workflow piped watch:upstream into tee "$GITHUB_STEP_SUMMARY" with no shell key, so bash ran without pipefail and any shell-side failure (git fetch, a garbage-collected pinned ref, AGT renaming main, jq missing) left the job green with an empty summary and the real error stranded on stderr. Redirecting stderr into the pipe publishes that failure in the summary without adding pipefail or making the job fail -- this workflow reports and never refuses. A clean diff and a run that compared the wrong ref (or the same ref twice) used to read identically. scripts/run-upstream-watch.sh now resolves each clone's commit with git rev-parse HEAD and hands both SHAs to the runner by environment variable, the same way it hands in the clone paths; the run prints both, or a fixed line saying they were not supplied when they aren't -- never an empty or fabricated commit. The five test files this slice added never cleaned up their fixture directories, leaving hundreds of stale temp dirs behind. Every test file that creates one now trashes it in afterEach, adopting the trashDir convention policy-input-schema.test.ts already used. policy-input-schema.ts's own header claimed PinnedSurfaces and UpstreamSurfaces "will live in this package once its drift watch exists" -- both already live in this package, and this package's watch is about the upstream contract moving, not about drift. The sentence now says what is true, in the present tense. Slice: #9
The published captures were taken while the run still rendered one section, then two; the shipped run now always renders four when it completes, and a reader reproducing the enum edit got a hookmap line that appeared in none of the published blocks. Line 71's "nothing about the watch itself changed between any of the runs below" was false for the same reason. Every captured block below is pasted from a real run against the shipped build: the two clones unmodified, the upstream clone with one value added to the verdict enum, and the upstream clone with verdict.schema.json renamed -- each verified byte-for-byte against its raw command output before being pasted in. The live-run capture at the top of the file is also refreshed, since publishing the compared-refs line and fixing the workflow's pipe both changed what it prints. Slice: #9
The renamed-schema scenario's closing sentence claimed the run "never gets far enough to know which two commits it was comparing" -- false, since both SHAs arrive as environment variables before either clone is read. The run returns as soon as a side cannot be read, which is the real reason a read failure prints no compared-refs, schema, or hookmap line; the sentence now says that instead. The closing summary undercounted its own captures: the file pastes four run blocks verbatim, not three -- the live top-of-file capture and the unmodified re-run against the scratch clones are two separate clean captures, not one, alongside the enum-edit and renamed captures. Slice: #9
…he rule AGT writes verdict.message for an operator reading a log -- patterns.rego sprintf's "matched pattern <regex> at offset <n>" -- and policy/lib is vendored byte-identical from upstream. Rewording it there would fork the bundle this deployment's whole claim rests on, so the sentence a human reads is composed on this side of the boundary instead. mapping.yaml declares the wording, the same way it already declares every other field synthesis: a template, a summary per AGT rule, and how AGT's own message renders into the last clause. The pattern form's regex is the one part a reader gains nothing from, so the offset is kept and the regex dropped; every other message is passed through whole and attributed, so a shape this does not recognise is never lost. A rule with no summary takes the default rather than throwing -- an incomplete wording table is a gap in prose, and the Guardian's evaluation catch would turn a throw into a deny of a step AGT allowed. A modify now carries an explanation where it carried none. AGT supplies no message for a transform, so there was nothing to copy, and composing from the rule that decided closes the gap docs/demos/v4-runbook.md records as "the redaction reaches the model unexplained". Six tests pinned on that absence state the new premise instead. A mapping that declares no template still copies its source verbatim, which is what a source-only declaration asks for and what keeps the two fixture mappings two lines long. policy/lib is untouched and verify:pin passes. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…nfig Two files a runbook previously told a reader to create by hand. .claude/settings.json is hosts/claude-code/settings.json in the place Claude Code actually reads it, gating Bash at both the request and the result gate. opencode.json points OpenCode at the committed plugin and declares nothing else, so the model and provider come from whatever the operator already has configured. Two consequences, both deliberate. .claude/settings.json governs every Claude Code session in this repository, not only a demo one -- a Bash call made here now asks a Guardian. The negotiated posture is proceed, so a Guardian that is not running audits and proceeds rather than blocking work. And OpenCode's own permission.bash: allow is OpenCode's separate permission system, set only so it does not stop to ask before the call reaches the plugin hooks; every decision is still the Guardian's, downstream of it. The Claude Code half is verified end to end: a real headless session denies a destructive command and the envelope log carries the handshake and the decision. The OpenCode half is not -- a tool-calling run produces no output and writes no envelope, and that is still being chased. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
The entry point printed and exited 0 unconditionally, so "the watcher could not run" and "the watcher ran and found no drift" were the same answer at the only boundary CI can see. A clone that was never made, a surface relocated out from under readSurfaces, a fetch that never landed: all published as a clean weekly summary. `ran: false` now leaves the process non-zero. Drift, a schema rejection against main and an unregistered hookmap tool all keep exit 0, because each is a completed run with a finding for a human to weigh -- a gate that reddens on drift is a gate nobody can leave green. process.exitCode rather than process.exit, so stdout is flushed before the process ends: the workflow reads this run through a pipe into the step summary and a truncated summary is the same class of defect as a status that could never go red. Two header claims falsified by this and rewritten: the paragraph justifying the read-failure catch by the absence of pipefail downstream, and the closing line "nothing here fails a build".
GitHub's implicit shell for `run:` is `bash -e {0}`; pipefail is added only
when `shell: bash` is stated, and this file stated no shell. The pipeline's
status was tee's, which is 0 whatever happened on the left, so every hard exit
in scripts/run-upstream-watch.sh was thrown away -- missing agt.lock, missing
jq, missing trash, a fetch that never landed, the pinned ref gone from
upstream. Measured: `bash -e -c 'false | tee /dev/null'` exits 0 and
`bash -eo pipefail -c` on the identical line exits 1; with a git that fails
inside the script, the old form exits 0 and the new one exits 128.
The comment that defended the omission conflated a finding with a failure. It
now states the distinction it was fumbling: drift is this job's product and
exits 0, a watcher that could not run exits non-zero.
permissions: contents: read, rather than inheriting a repository default that
can be read-and-write. The step summary is a file named by an environment
variable, not an API call, so read is the whole requirement.
timeout-minutes: 10 on a job that does two shallow clones and a diff, in place
of the 360-minute default a hang would otherwise burn.
Both actions pinned to the commit their major tag resolved to, tag kept as a
trailing comment: checkout v4 -> 11d5960 (v4.4.0), setup-bun v2 -> 0c5077e
(v2.2.0), each read from the upstream repository's own refs. apt's trash-cli
stays unpinned and is now the only unpinned install left in the file.
.github held exactly one workflow and it ran none of the gates. bun test, typecheck, verify:pin, verify:zero-diff and conformance were human-typed only, so no commit in this stack had ever been verified by anything mechanical. Two jobs, split along the network. `suite` is hermetic: typecheck first because it is the cheaper answer, then bun test under `!cancelled()` so one push yields both results instead of one and a re-run. `pinned-gates` runs verify:pin and conformance, which shallow-clone AGT at agt.lock's pinned ref -- an immutable commit, so upstream cannot move under them, but they still need the network and still hard-exit without jq or trash. Kept separate so an outage on their side reddens only them and leaves the hermetic answer standing. submodules: recursive in both. validate-envelope.ts builds its Ajv registry from a path into spec/acs relative to import.meta.url, so an absent submodule is a hard ENOENT at request time rather than a self-skip. trash-cli in both too: five conformance test files clean their fixtures with `Bun.$`trash ...`` because this repo does not permit rm -rf, and Bun's shell throws on a command it cannot find. verify:zero-diff is deliberately left out. Its base is slice/v4 and it is a claim about that slice's own diff, not an invariant of later commits: run on this branch it reports fourteen changed frozen paths and exits 1, correctly, because the slices after v4 changed them. On push it would redden the whole stack for something that is not a defect, and the branch it resolves against is one this stack is designed to delete. push is narrowed to main because every branch here has an open pull request, which would otherwise pay for each commit twice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #9
Stacked on
slice/v7. Review the diff against that base, notmain. Slice 8 of 8.What this slice is for
Point the same harness at AGT’s
mainbranch. If an enum or schema field moved, the report names the surface and the field.Without it, V7 is a snapshot that silently rots the first time AGT ships a breaking change.
The finished MVP is the demo you can run, the matrix you can publish, and the watch that tells you when the matrix is lying. This slice is that watch.
What a reviewer is looking at
This PR does not yet fetch or diff those surfaces. It freezes the names so the first implementation file cannot inherit the wrong noun: “drift” is already AGT’s behaviour-drift gate (
drift.rego,policy/manifest.drift.yaml). This slice watches the upstream contract.PinnedSurfacesis the eight watched surfaces at the locked ref;UpstreamSurfacesis the same set atmain.diffSurfacesis told both snapshots and reads neither store itself. A changed enum is aSurfaceDiff, not a cell of V7’s 8×5 — V7 already split renderers so this output cannot arrive as a column of “does ACS v0.1.0 express AGT.”Surfaces to watch, once implemented
manifest.schema.json,policy-input.schema.json,verdict.schema.json,snapshot.schema.json, the intervention-point enum, the verdict enum,reserved-reasons.json, and the stock bundle’sdata.agt.defaults.configkeys.Slice notes:
slices/v8/README.md.