Skip to content
Merged
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
77 changes: 77 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -13081,6 +13081,83 @@ finding two genuinely stale surveys on its first live run. **Re-measuring those
not part of this change** — the loop reports staleness; a re-survey is its own work, with its own
live corpus.

#### 2026-08-17: the preflight states each seat's posture, from the room's own claim

**The gap.** Every council column carries a sandbox badge, and the help panel's posture page
carries the measured argument under each one (§9.13, §9.2's ruling that a claim you cannot see
is not a claim). Both of those are read **inside the room**, which is after the decision they
inform. A user picks a workspace and a posture *before* the room opens, and the only surface
that runs before the room opens said nothing about either. §9.17 settles that the fact belongs
here: what a vendor's own flags buy on this machine is true at launch and stays true, and it is
a property of the vendor and the OS rather than of a turn.

**One source, two surfaces.** Nothing in the block is written in `internal/doctor`.
`council.DoctorSeats` builds it from `postureClaim` — the same function the room's own columns
are built from — and hands over the badge word off `SandboxClaim.Badge()` and an evidence class
off the claim's `Level`. That routing is the whole design: a preflight with a per-vendor posture
table of its own would agree with the badges on the day it was written and diverge the day a
level moved, and a reader looking at two disagreeing surfaces has no way to tell which is
lying. The capability declaration and the survey pin are attached at the same seam, for the same
stated reason. `TestThePreflightPostureIsTheRoomsOwnBadge` pins it through *different*
construction paths on each side — `DoctorSeats` against the columns `stateWith` builds — because
comparing `doctorPosture` with `postureClaim` would be comparing a call with itself.

**The badge says what the posture IS; the evidence class says what it RESTS ON.** They are
different questions, and `unsandboxed` is the case that proves it: two seats reach that badge
because a live run **refuted** the flags and because **no flag was ever passed**, and a reader
deciding whether to point council at a worktree needs the second sentence. §4a.1's rule that two
kinds of nothing must not render alike is the same rule one level up.

| badge | evidence class |
|---|---|
| `ro:tools` | enforced by **construction** — the write and shell tools are absent from the session |
| `ro:enforced` | enforced by an **operating system** — the vendor's own sandbox |
| `ro:requested` | **asked for**, and never observed on this machine — weaker than either above, and says so |
| `unsandboxed` | **measured** not to restrict — refuted by a live run, not merely unestablished |
| `WRITES` | nothing was asked for at all |
| `gated` | **your keystroke** — the seat asks before every tool call that changes anything |

`evidenceClass` is a table keyed by level, so `TestEveryPostureLevelHasAnEvidenceClass` can walk
the type and fail the build the day a sixth level renders a badge with nothing to classify it —
the guard `helpBadgeGloss` already carries inside the room. `TestNoEvidenceClassSoftensItsBadge`
holds the other half on `TestThePostureLegendDoesNotSoftenAnyClaim`'s terms exactly: these
sentences classify evidence and never weaken it, and none of them may call a posture read-only,
safe or unable to write.

**The rows are the `--read` room, and the argv is on the block.** The room **WRITES by default**
and `--read` is the opt-out (`cmd/telltale`, and the legend inside the room was already corrected
once for crediting the retired `--write` flag). The rows report the `--read` posture because that
is the only one that is a fact about the *machine* — the default room's badge is a property of an
argv the reader has not typed yet, and five cells all reading `WRITES` would carry nothing per
seat. So the header names `telltale council --read` in its first clause, and **one closing
declaration** states the default: the room writes, *n* of *m* seats can be asked to ask first,
and what contains a writing room is the workspace, not any of these words. The gating half is
**counted off `canGate`**, never written down — that measurement has already moved once, when the
Cursor seat became a live process that can be asked and still does not ask about edits.

**It is a claim, and it is not one of the three states.** A posture was measured once against a
live run and written into this repository; nothing re-measures it on the reader's machine. So it
renders outside the status column, beside the capability line and the survey pin, and it is wrong
in *both* directions as a check: a `FAILED` would redden a working install over a vendor's own
design decision, and an `ok` would claim this preflight established a containment property it
never probed and could not probe without spending a turn. `TestAPostureIsNotACheck` pins that the
way `TestDriftIsNotAFailedCheck` does — the same seat with and without the data, the three counts
required to be identical — and `TestThePostureBlockCostsNoProbe` pins the other half: the block
adds **no probe, no network call and no login check**, because every string in it arrives with
the seat. The exit code is untouched.

**A seat council states no posture for gets `no claim`, not a missing row.** A seat absent from a
posture table reads as a seat with nothing to declare; this one has an unanswered question. The
word deliberately is not shaped like `not checked` — the three state words are spoken for, and a
fourth column borrowing one would put this block back inside the block it is outside of.

**Measured on the reference Mac, 2026-08-17.** The five rows read `claude ro:tools`,
`codex ro:enforced`, `agy unsandboxed`, `cursor ro:requested`, `grok unsandboxed`, and the
declaration reads *1 of the 5 seats above can be asked to ask first: claude*. The `codex` row is
the platform branch working: the same block on Windows reads `unsandboxed` there, because council
passes `-s danger-full-access` on that OS (ADR-008's twelfth amendment) — and it reads it from
`postureClaim`, not from a second platform test in the preflight.

<a id="s9-43"></a>

### 9.43 the agy seat stops pretending a lost thread resumed (2026-08-09)
Expand Down
89 changes: 89 additions & 0 deletions internal/council/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package council

import (
"path/filepath"
"runtime"
"strings"

"github.com/sanlee-ys/telltale/internal/adapter/pins"
Expand All @@ -28,6 +29,7 @@ import (
func DoctorSeats() []doctor.Seat {
reg := vendors.Registry()
infos := Detect()
windows := runtime.GOOS == "windows"
out := make([]doctor.Seat, 0, len(infos))
for _, info := range infos {
v := info.Vendor
Expand All @@ -40,6 +42,7 @@ func DoctorSeats() []doctor.Seat {
Note: info.Note,
Drivable: info.Avail == AvailInstalled,
Capability: declaredCapability(v, reg),
Posture: doctorPosture(v, windows),
VersionArgs: versionArgs(info),
}
if s.Drivable {
Expand Down Expand Up @@ -151,3 +154,89 @@ func declaredCapability(v model.VendorID, reg map[model.VendorID]vendors.Vendor)
}
return strings.Join(parts, "; ")
}

// doctorPosture is one seat's sandbox claim, flattened for the preflight's
// posture block (design.md §9.42, amended 2026-08-17).
//
// It goes THROUGH postureClaim rather than beside it, and that is the whole
// design. `postureClaim(v, windows, false, false, false)` is literally what
// `telltale council --read` builds for this column — same function, same
// arguments, same platform branch — so the badge the preflight prints and the
// badge the column wears are one value read twice. A preflight that grew its own
// per-vendor table would agree on the day it was written and diverge the day a
// level moved, and a reader looking at two disagreeing surfaces has no way to
// tell which one is lying. The capability declaration above is attached at this
// seam for the same stated reason.
//
// The `--read` room is the posture reported because it is the only one that is a
// fact about the MACHINE. The default room writes, and what it writes with is a
// property of the argv the reader has not typed yet — so it is stated once, in
// the block's closing declaration, rather than five times in a column where
// every cell would read the same word.
func doctorPosture(v model.VendorID, windows bool) doctor.Posture {
claim := postureClaim(v, windows, false, false, false)
return doctor.Posture{
Badge: claim.Badge(),
Evidence: evidenceClass(claim.Level),
// Off canGate, not off a list of vendor names. That measurement has
// already moved once — the Cursor seat became a live process that can be
// asked and still does not ask about edits — and a copy here would have
// gone on saying the old thing.
CanGate: canGate(v),
}
}

// evidenceClass is what KIND of evidence stands behind a badge, keyed by the
// level that renders it.
//
// The badge word says what the posture IS; this says what it RESTS ON, and the
// two are different questions with different answers. `unsandboxed` is the case
// that proves it: two seats can both fail to be read-only because a live run
// refuted the flags and because no flag was ever passed, and a reader deciding
// whether to point council at a worktree needs the second sentence, not the
// first. §4a.1's rule that two kinds of nothing must not render alike is the same
// rule one level up.
//
// A table keyed by level rather than prose, so a test can walk the type and fail
// the build the day a sixth level renders a badge with nothing here to classify
// it — see TestEveryPostureLevelHasAnEvidenceClass, which is the guard
// helpBadgeGloss already carries for the room's own legend, one surface out.
//
// NOTHING HERE MAY WEAKEN A CLAIM, on helpBadgeGloss's terms exactly. These are
// classifications of evidence, not softenings of it: `unsandboxed` still says
// nothing restricts the vendor, `ro:requested` still admits nobody observed what
// it enforces, and no line here may call any posture read-only, safe, or unable
// to write.
func evidenceClass(l SandboxLevel) string {
switch l {
case SandboxTools:
return "enforced by CONSTRUCTION — the write and shell tools are absent from that " +
"session, read off what the session reported about itself rather than off a flag. " +
"The residual is that a deny list cannot cover a tool a future release adds"
case SandboxEnforced:
return "enforced by an OPERATING SYSTEM — the vendor's own sandbox, and the one posture " +
"in this table that a flag is not the last thing standing behind"
case SandboxRequested:
// It does NOT say "weaker than the two above", which is how the room's own
// legend words it. That legend is ordered by level; these rows are ordered
// by seat, so "above" points at whatever vendor happens to sort first. The
// comparison is named instead of pointed at.
return "ASKED FOR, and never observed — the flag was accepted and what it enforces on " +
"this machine is not established. Weaker than a construction or an OS sandbox, " +
"and says so"
case SandboxNone:
return "MEASURED not to restrict — a live run refuted the flags rather than leaving " +
"them unestablished, so treat this seat as able to change your files"
case SandboxWrite:
return "nothing was asked for — this seat may edit and run, and no restriction was " +
"requested of it at all"
case SandboxGated:
return "YOUR KEYSTROKE — this seat asks before every tool call that changes anything, " +
"and nothing runs until you answer"
default:
// SandboxUnknown renders no badge, so there is nothing to classify. An
// invented sentence here would be a claim about a seat council makes none
// about; doctor prints its own honest blank instead.
return ""
}
}
119 changes: 119 additions & 0 deletions internal/council/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,122 @@ func TestTheDeclaredCapabilityMatchesTheRoomsOwnMeasurements(t *testing.T) {
t.Errorf("grok is a batch program per turn and does not say so: %q", got)
}
}

// TestThePreflightPostureIsTheRoomsOwnBadge is the "one source, two surfaces"
// assertion, and it is deliberately made through a DIFFERENT construction path
// on each side: DoctorSeats on one, the room's own stateWith on the other.
//
// Comparing doctorPosture against postureClaim would be comparing a call with
// itself. What can actually go wrong is that a preflight grows a per-vendor
// table of its own — right on the day it is written, and silently wrong the day
// a level moves. This walks the columns a `--read` room would open with and
// requires the same word on both surfaces.
func TestThePreflightPostureIsTheRoomsOwnBadge(t *testing.T) {
noVendorOverrides(t)
t.Setenv("PATH", t.TempDir())

// Write is false: the room `--read` opens, which is the room the preflight's
// rows describe.
room := map[model.VendorID]string{}
for _, c := range stateWith(Options{}, false).Columns {
room[c.Vendor] = c.Sandbox.Badge()
}
seats := DoctorSeats()
if len(seats) == 0 {
t.Fatal("the preflight lists no seats at all")
}
for _, s := range seats {
v := model.VendorID(s.Vendor)
want, ok := room[v]
if !ok {
t.Errorf("%s is in the preflight and not in the room", s.Vendor)
continue
}
if s.Posture.Badge != want {
t.Errorf("%s: the preflight says %q and the column says %q",
s.Vendor, s.Posture.Badge, want)
}
if s.Posture.Evidence == "" {
t.Errorf("%s carries the badge %q with no evidence class behind it",
s.Vendor, s.Posture.Badge)
}
// The gating fact is read off canGate on both surfaces, so the preflight
// cannot promise a gate the room will not open.
if s.Posture.CanGate != canGate(v) {
t.Errorf("%s: the preflight says gatable = %v, canGate says %v",
s.Vendor, s.Posture.CanGate, canGate(v))
}
}
}

// TestEveryPostureLevelHasAnEvidenceClass mirrors TestEveryBadgeIsExplained, and
// closes the same gap one surface further out: a badge that renders with nothing
// to say what KIND of evidence is behind it. That gap comes back the day a sixth
// level lands, which is why this walks the type rather than listing the five.
func TestEveryPostureLevelHasAnEvidenceClass(t *testing.T) {
seen := map[string]SandboxLevel{}
for l := SandboxUnknown; l <= SandboxGated; l++ {
b := SandboxClaim{Level: l}.Badge()
e := evidenceClass(l)
if b == "" {
// SandboxUnknown renders no badge, so there is nothing to classify —
// and it must not invent a sentence about a seat council makes no
// claim about.
if e != "" {
t.Errorf("%v renders no badge and still carries an evidence class: %q", l, e)
}
continue
}
if e == "" {
t.Errorf("the badge %q renders on a column and nothing says what evidence "+
"stands behind it", b)
continue
}
if prev, dup := seen[e]; dup {
t.Errorf("%v and %v share one evidence class, so the preflight cannot tell "+
"them apart: %q", prev, l, e)
}
seen[e] = l
}
}

// TestNoEvidenceClassSoftensItsBadge, on TestThePostureLegendDoesNotSoftenAnyClaim's
// terms exactly. These sentences classify evidence; they never weaken it. The two
// words that mean "this seat can change your files" keep meaning that, the weakest
// badge keeps admitting it is weak, and nothing here may call a posture read-only,
// safe, or unable to write — the badges break the `ro:` prefix on purpose, and a
// classification that put it back would undo that outside the room, where there is
// no legend to correct it.
func TestNoEvidenceClassSoftensItsBadge(t *testing.T) {
for _, tc := range []struct {
level SandboxLevel
want []string
}{
{SandboxNone, []string{"measured", "change your files"}},
{SandboxRequested, []string{"never observed"}},
{SandboxTools, []string{"absent"}},
{SandboxEnforced, []string{"operating system"}},
{SandboxWrite, []string{"edit and run"}},
{SandboxGated, []string{"asks before every tool call"}},
} {
got := strings.ToLower(evidenceClass(tc.level))
for _, w := range tc.want {
if !strings.Contains(got, strings.ToLower(w)) {
t.Errorf("the evidence class for %q dropped %q: %q",
SandboxClaim{Level: tc.level}.Badge(), w, got)
}
}
}
for l := SandboxUnknown; l <= SandboxGated; l++ {
if l != SandboxNone && l != SandboxWrite && l != SandboxGated {
continue
}
g := strings.ToLower(evidenceClass(l))
for _, forbidden := range []string{"read-only", "safe", "cannot write"} {
if strings.Contains(g, forbidden) {
t.Errorf("the evidence class for %q says %q: %q",
SandboxClaim{Level: l}.Badge(), forbidden, g)
}
}
}
}
19 changes: 18 additions & 1 deletion internal/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ type Seat struct {
// inventory already lives. Zero for a seat with no surveyed adapter behind
// it, which renders no pin line at all — see pin.go.
Pin Pin
// Posture is this seat's sandbox claim, and it is the third DECLARED field
// on this struct for the third time the same reason: council measured it
// once against a live run, council renders it on the column badges, and a
// second copy here would be a table that agrees today and drifts later. See
// posture.go.
Posture Posture
}

// ProbeResult is what one bounded version probe produced. Out is the vendor's
Expand Down Expand Up @@ -294,6 +300,10 @@ type SeatReport struct {
// why a staleness fact must not become a fourth state. Nothing in Tally or
// Ready reads it, so the counts and the exit code are unchanged by it.
Drifted bool
// Posture is the seat's sandbox claim, carried through unchanged from the
// Seat. Nothing in Tally or Ready reads it either, for posture.go's reason:
// a claim about what a vendor's flags buy is not a check on this machine.
Posture Posture
}

// Ready reports that every check that RAN on this seat passed. A seat with
Expand Down Expand Up @@ -385,7 +395,14 @@ func Run(seats []Seat, probe Probe) Report {
}

func runSeat(s Seat, probe Probe) SeatReport {
out := SeatReport{Vendor: s.Vendor, Label: s.Label, Capability: s.Capability}
out := SeatReport{
Vendor: s.Vendor, Label: s.Label,
Capability: s.Capability,
// Copied, never derived. runSeat is where a probe could be reached, and a
// posture that was computed here would be a posture this package invented
// — the one thing posture.go rules out.
Posture: s.Posture,
}

// binary — always runs. Detection is a stat, so there is no branch in which
// this question goes unasked.
Expand Down
Loading