v1.91.265.0 docs: absorb four pstack principles into ETHOS.md, the observable-questions rule and candidate-side eval blinding (GSTA-124) (#3493)

* docs(ethos, plan-tune, eval-plan): absorb four pstack principles, the observable-questions rule and candidate-side blinding (GSTA-124)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Reword ETHOS sentences, scope the blinding rule, name the second candidate-visible root

Review pass 1 (frozen scope, five INFORMATIONAL findings, all in-diff, none
blocking): two ETHOS.md sentences in principles 6 and 7 were too close to the
credited source and are reworded; the Credits line names both pstack sources
(principle-* leaves and poteto-mode); eval-plan Step 1c rule 1 is scoped to
what the preregistration controls and exempts the skill text an arm loads;
the observable-questions rule in plan-tune and docs/proactive.md requires the
observation to be local, read-only and free and points at the one-way-door
rule; docs/TESTING_INTERNALS.md names skill-e2e-benchmark-* (test/skill-e2e-
deploy.test.ts:293) as a second candidate-visible root and states the audit's
matching method. SKILL.md regenerated for both unpinned skills.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: correct the candidate-visible root list and the one-way-door attribution

The blinding audit paragraph in docs/TESTING_INTERNALS.md and the CHANGELOG
entry claimed two candidate-visible temporary roots and that the other
skill-e2e-<case>-* roots carry no listed word; test/skill-e2e-test-value.test.ts:131
(skill-e2e-<id>- with ids carrying 'test') and test/skill-e2e-health.test.ts:23
(gstack-health-eval-) show otherwise, so both now list the roots the mkdtemp scan
found and say the list is not a proof of completeness. docs/proactive.md said the
one-way-door rule already covers spend and third-party reach; it now says the new
sentence extends that rule. The CHANGELOG bullet for the observable-questions rule
names its local, read-only and free qualifier.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-27 03:18:12 +00:00
1 parent c2e01e4ba7
commit a50cf58965
13 files changed
+283 -3

No files matched your search

+3
View File
@@ -155,6 +155,9 @@ When fixing failures or preparing `/ship`, follow this order:
only when that prose is the contract. Do not lower thresholds, increase model
budgets, skip cases, or rejudge a failure to manufacture a pass. A
pre-registered fixed panel is not rejudging.
For any measured speedup, regression or eval delta, name the limiter that
moved and the run-to-run variation; a gap smaller than that variation is no
difference.
For policy or validation repairs, exercise the actual registered callback with
representative native input and assert that it uses the helper’s result.
When renderer or parser failures recur at the same boundary, verify the
+15
View File
@@ -1,5 +1,20 @@
# Changelog
## [1.91.265.0] - 2026-10-27
**Four more principles, and three places gstack stops asking what it could observe (pstack absorb, lane C3, GSTA-124).** Ideas from Lauren Tan's pstack (MIT), written in gstack's words; NOTICE.md and the ETHOS.md credit line say so.
### Added
- **ETHOS.md principles 5 to 8**: Attack the Premise (two fixes that share a premise and fail the same gate indict the premise; write it down and falsify it before a third fix), Separate Before Serializing Shared State (a lock is a smell to check; remove the race before refereeing it), Make Operations Idempotent (design for the run that happens twice; ask what happens if the previous run died at every point), Explain the Number (a measurement carries what was measured, the limiter it moved and the run-to-run variation; a gap inside the variation is no difference). "How They Work Together" gains one line each and a Credits section names the source.
- **`/eval-plan` Step 1c, candidate-side blinding**: three preregistration rules (no eval word anywhere the candidate can read; project-shaped fixture roots; the judge sees `A`/`B` labels and grades chain-following from the files the transcript shows the candidate opened). `prereg --check` does not read fixture trees, and the step says so. `docs/TESTING_INTERNALS.md#candidate-side-blinding` states the same rules for gstack's own fixtures, lists the fixture directory names on the word list and the candidate-visible temporary roots it found (`arm-<fixture>-<arm>-*` from `test/helpers/arm-benchmark-harness.ts`, `skill-e2e-benchmark-*` from `test/skill-e2e-deploy.test.ts`, the `skill-e2e-<id>-*` roots of `test/skill-e2e-test-value.test.ts` whose ids carry `test`, `gstack-health-eval-*` from `test/skill-e2e-health.test.ts`; the list is a scan of `mkdtemp` prefixes in e2e tests, not a proof of completeness); no fixture is renamed here.
- **Observable questions are not the user's to answer**: a rule in `/plan-tune`'s Important Rules and a section in `docs/proactive.md`. A question a local, read-only, free command, render or measurement can settle is settled that way and reported (a call that changes state, reaches a live or third-party system or spends money stays a question); only product and preference calls reach the user, with the observation already made. The question-log audit treats an observable question as a defect in the asking skill rather than a preference to tune.
### Changed
- **AGENTS.md validation discipline step 4**: one sentence on measured deltas: name the limiter that moved and the run-to-run variation; a gap smaller than that variation is no difference. `agents-digest/gstack-AGENTS.md` is generated from `scripts/gen-agents-digest.ts` and `VERSION`, not from AGENTS.md, so the regenerated digest is unchanged.
### Gate integrity
- No threshold, trial count, case kind or judge prompt changed. Prose-only change: `plan-tune` and `eval-plan` are unpinned skills; sizes measured in the PR body.
## [1.91.264.0] - 2026-10-27
**/design-motion turns an approved design into a showcase loop for X and the website; adapted from Elaya's design-to-motion (GSTA-92 plan item 15, Garry 2026-10-25 "Do it").** gstack could design a screen and build it, but not show it moving: a screen recording drops frames, jumps at the loop seam, and nothing tells you a card drifted out of its container. /design-motion rebuilds the exact frame from a /design-shotgun PNG, a /design-html `finalized.html` or a URL (a Figma link when `FIGMA_TOKEN` is set), loads it from an empty state, gives each visual one story, records frames on a fake clock, and exports only after the loop is proven seamless.
+110
View File
@@ -173,11 +173,111 @@ against, and gstack's countermeasure for each, lives in
---
## 5. Attack the Premise
A fix is an argument: "the failure happens because X, so changing Y ends it."
When the gate rejects the fix, the usual move is to adjust Y and try again.
Do that once. When a second fix built on the same X also fails the same gate,
the suspect is no longer Y. It is X. Write the premise down as a sentence that
can be false ("the retry fires before the lock is released"), then design the
cheapest observation that would falsify it, and run that before any third fix.
Two failures that share a premise are evidence about the premise, and a third
attempt that keeps it is a guess wearing a fix's clothes.
The same test applies to the request itself. "Make this faster" assumes the
slow part is where the asker thinks it is; "add a retry" assumes the failure is
transient. Before building on a premise someone handed you, state it back and
check it against something observable: a profile, a log line, a reproduced
failure. Most wasted work in this craft is excellent work on a false premise.
**Anti-patterns:**
- "Third time's the charm." (Two shared-premise failures mean the premise is the bug.)
- Rephrasing the same fix as a different one. (Same X, same gate, same result.)
- Treating the task description as ground truth. (It is a hypothesis you were handed.)
---
## 6. Separate Before Serializing Shared State
When two writers collide, the reflex is a lock, a queue or a retry loop. Each
of those keeps the collision and pays for it forever: a lock serializes every
caller to protect the rare conflict, a queue adds a component that can fill, a
retry turns one race into many. So when the sentence "we need a lock" comes up,
hear it as a prompt to look at the design, not as the fix. Ask first whether the
writers need to share the state at all. One file per writer with a merge at read time, a per-unit
directory, an append-only log keyed by author, a partition by owner: each of
these removes the race instead of refereeing it, and leaves nothing to deadlock.
This is why a lane in a gstack crew owns its files rather than taking turns on
shared ones, and why the few shared files that remain (the changelog, the
notices) are append-only and merged by section. Serialize only when the state is
truly singular (a version counter, the merge onto main), and then say so and
name the single writer.
**Anti-patterns:**
- A mutex around a cache that could have been per-caller.
- A retry loop on a write conflict that a per-writer file would never produce.
- "Only one of these runs at a time" as an unwritten assumption.
---
## 7. Make Operations Idempotent
Any operation that touches state outside the process will one day run twice:
a retry after a timeout, an agent resumed from a checkpoint, a user pressing
the key again. Design for that run. An idempotent operation reaches the same
end state whether it runs once or five times, and a run that crashed halfway
can simply be run again rather than repaired by hand.
The working question: for each line of the operation, suppose the previous run
died right after it. What does this run find, and does it still converge? Walk
the operation line by line and answer that for every line. A create that
checks for the existing row, a write that lands in a temp file and renames, a
"done" marker written last, a cleanup that tolerates an already-missing file:
each answer removes a class of manual recovery. Where true idempotence is
impossible (sending an email, charging a card), make the operation
distinguishable instead: a key the receiver can deduplicate on, recorded before
the send.
**Anti-patterns:**
- A setup script that fails on its second run because the directory exists.
- A migration that appends on every run instead of converging.
- "Run it again" as advice without knowing what the first run left behind.
---
## 8. Explain the Number
A measurement reported without its mechanism is a rumor. "40% faster" is not a
result until you can say what got faster, why that change caused it, and what
the run-to-run noise was. If you cannot explain the number, you do not know
whether you measured your change, a warm cache, a quieter machine, or a
different input, and the next person cannot tell either.
So every number gstack reports carries three things: the thing measured and how
(the command, the sample size, the clock), the limiter it moved (CPU, I/O, a
model's output length, a sample that was too small), and the variation
(repeated runs, or an honest "one run, so no claim about the delta"). A gap
smaller than the variation is not a difference; say "no measurable change" and
mean it. A surprising number is a lead to investigate, not a headline to post:
the eval that jumps twenty points usually found a shortcut, not a capability.
**Anti-patterns:**
- A speedup from one run on each side.
- A pass rate without the trial count.
- "Faster" with no theory of what changed.
---
## How They Work Together
Boil the Ocean says: **do the complete thing.**
Search Before Building says: **know what exists before you decide what to build.**
Honest Work says: **the proof is the work, and a report is a claim until re-executed.**
Attack the Premise says: **when fixes keep failing, test the belief they share.**
Separate Before Serializing says: **remove the race before you referee it.**
Make Operations Idempotent says: **design for the run that happens twice.**
Explain the Number says: **a result you cannot explain is not yet a result.**
Together: search first, then build the complete version of the right thing.
The worst outcome is building a complete version of something that already
@@ -194,3 +294,13 @@ wanted it. Every feature was built because it was needed, not because it
was requested. If you're building something for yourself, trust that instinct.
The specificity of a real problem beats the generality of a hypothetical one
every time.
---
## Credits
Principles 5 through 8 (Attack the Premise, Separate Before Serializing Shared
State, Make Operations Idempotent, Explain the Number) are gstack's statement of
four ideas from Lauren Tan's pstack (https://github.com/cursor/plugins, its
`pstack/skills/principle-*` leaves and its `poteto-mode` skill, MIT). The words
here are gstack's; see NOTICE.md.
+1
View File
@@ -114,3 +114,4 @@ design is not a defect, missing context; independent agreement as the strongest
`NO FINDINGS` as a correct result) in `scripts/resolvers/review-army.ts` stage 4 and
`docs/review-policy.md` are written in gstack's words. No sentence is copied, so no license
text is carried; this entry is a courtesy.
Also ideas, not text, from `pstack/skills/principle-*`, `poteto-mode` and its eval playbook: ETHOS.md principles 5 to 8, the observable-questions rule in `plan-tune/SKILL.md.tmpl` and `docs/proactive.md`, the blinding rules in `eval-plan/SKILL.md.tmpl` and `docs/TESTING_INTERNALS.md`, and the limiter sentence in AGENTS.md step 4 (GSTA-124).
+1 -1
View File
@@ -1 +1 @@
1.91.264.0
1.91.265.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.264.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.265.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+47
View File
@@ -1527,6 +1527,53 @@ the `recordGraceMs` option of `test/helpers/office-hours-attempt.ts` (default
5000) and the `timeoutMs` argument of `clearCookieTargetStorage(page, origin,
timeoutMs)` (default 15000).
## Candidate-side blinding
A paid case has two readers that must not know what the other knows: the
candidate (the agent under test) must not know it is being graded, and the
judge must not know which arm it is grading. `/eval-plan` Step 1c states the
three rules a preregistration carries; this is what they mean for gstack's own
fixtures.
- **The candidate's view carries no eval word.** Nothing the candidate can read
(its working directory, file names, the task prompt, commit messages, the
environment) says `eval`, `test`, `judge`, `experiment`, `rubric`, `score`,
`compare`, `benchmark`, `candidate` or `arena`. The project's own `test/`
directory is exempt when the task is about those tests, and so is the skill
text an arm loads as its own instructions (a gstack SKILL.md and the ETHOS.md it
points at use these words freely); the rule governs what the case builds. The
hermetic home a case builds (`test/helpers/hermetic-env.ts`) is the boundary:
what is copied in is visible, what stays in the runner is not.
- **Fixture roots are project-shaped.** A fixture the candidate works in is
named like the project it imitates, with the files that project would hold.
Runner-side fixtures (captured transcripts, judge calibration sets, census
reports) are never shown to a candidate and may keep descriptive names.
- **The judge grades labels and reads, not claims.** A judge prompt names arms
`A` and `B`, never the model or run, and a chain-following dimension is scored
from the files the transcript shows the candidate opened, not from what the
candidate says it read.
An audit on 2026-10-26 (GSTA-124) found these fixture directory names on the
word list: `test/fixtures/arm-benchmark/` and its four children, `browse-judge/`,
`cso-eval/`, `eval-history/`, `judge-calibration/` and its children, two shard
names under `paid-report-census/` and the `test/` subdirectories of
`state-matrix-6792/`. All of these are runner-side: no candidate's working
directory is one of them. Candidate-visible gaps are in the temporary roots
the harnesses create: `test/helpers/arm-benchmark-harness.ts` copies each fixture
into `os.tmpdir()/arm-<fixture>-<arm>-*`, so the candidate's working directory is
named after the arm rather than after a project; `test/skill-e2e-deploy.test.ts`
runs the /benchmark case in `skill-e2e-benchmark-*`, a root that carries a listed
word; `test/skill-e2e-test-value.test.ts` creates `skill-e2e-<id>-*` roots as the
candidate's working directory, and two of its ids (`review-test-value`,
`test-audit-report-only`) carry the word `test`; and `test/skill-e2e-health.test.ts`
creates `gstack-health-eval-*`. That list comes from a scan of the `mkdtemp`
prefixes in the e2e tests, not from a proof that no other root carries a listed
word. The audit matched whole
lower-case words in path components under `test/fixtures/`; a substring match
(`factory-scorecard/`, `ios-qa/FixtureApp/Tests/`) was not counted. Renaming is a
separate change with its own touchfile updates; the list is here so that change
can be scoped.
## Running evals as an agent: detach
Moved verbatim from CLAUDE.md (#2096 size limit). The short rule stays in
+18
View File
@@ -56,3 +56,21 @@ if you stored `off` before that, it takes the answer as given and does not ask.
If collisions with other skill packs keep coming up under `core`, the fallback
Garry approved on 2026-10-25 is `off` by default.
## Questions a skill asks on its own
The setting above governs which skills gstack offers unasked. A second rule
governs what a running skill may ask you unasked: a question whose answer the
skill could observe is not yours to answer. Whether a change is faster, whether
two renders differ, whether an endpoint accepts a payload, whether a test still
fails: each of those is settled by running the thing (time it, render both,
call it, run the test) and reporting the result, not by a prompt. That holds
for observations that are local, read-only and free; a call that changes state,
reaches a live or third-party system or spends money is still yours to approve.
That extends the one-way-door rule in `/plan-tune`, which covers destructive,
architectural and security questions, to spend and third-party reach. The only
questions that come to you are product and preference calls (what the product
should do, what you want, what you are willing to spend or risk), and they
arrive with the observation already in hand so the choice is between measured
options. `/plan-tune` applies the same rule when it audits the question log: an
observable question is a defect in the asking skill, not a preference to tune.
+30
View File
@@ -427,6 +427,36 @@ newest first; `docs/eval-models.md` in gstack), `arms`, `dry-run`, `pilot start`
the default is `full`. A refused arm (`EVAL_MODEL_DENIED`, `EVAL_MODEL_OLD_GENERATION`,
`EVAL_MODEL_GENERATION_UNRESOLVED`) is replaced, never argued past.
### Step 1c: Candidate-side blinding
A candidate that can tell it is being graded behaves differently from one that
thinks it is at work, and a judge that knows which arm it is reading grades the
label. Three rules hold for every arm, written into the preregistration (a
`## Blinding` section naming the fixture root and the judge's labels) and checked
by reading the fixture tree before the freeze commit; `prereg --check` does not
read fixture trees, so this check is yours:
1. **Nothing the candidate sees carries an eval word.** No `eval`, `test`,
`judge`, `experiment`, `rubric`, `score`, `compare`, `benchmark`,
`candidate` or `arena` in any directory name, file name, prompt, commit
message or environment variable the preregistration controls and the candidate
can read. Two things are outside the rule: a project's own test directory, when
the task is about those tests, and the skill text an arm loads as its own
instructions (a gstack SKILL.md uses these words freely); the rule is about the
fixture and the task, which the eval builds.
2. **The directory looks like a project.** Name the fixture root the way the
project it imitates would (`billing-service/`, `docs-site/`), with the files
that project would plausibly hold, never `fixture-a/`, `case-7/` or the
model's name. A candidate working in `arm-b/` knows there is an `arm-a/`.
3. **The judge sees sanitized labels and the candidate's real reads.** The judge
gets `A` and `B`, never a model id, arm name, price or run number, and it
grades whether the candidate followed the chain (read the file the task
pointed at, then the file that one named) from the files the transcript shows
it actually opened, never from the candidate's own account of what it read.
A rule broken after a priced call is a new eval with a new preregistration, the
same as a changed bar.
## Step 2: zero-cost stub-model dry run
Run the pipeline over the real development rows with a stub model that costs nothing:
+30
View File
@@ -99,6 +99,36 @@ newest first; `docs/eval-models.md` in gstack), `arms`, `dry-run`, `pilot start`
the default is `full`. A refused arm (`EVAL_MODEL_DENIED`, `EVAL_MODEL_OLD_GENERATION`,
`EVAL_MODEL_GENERATION_UNRESOLVED`) is replaced, never argued past.
### Step 1c: Candidate-side blinding
A candidate that can tell it is being graded behaves differently from one that
thinks it is at work, and a judge that knows which arm it is reading grades the
label. Three rules hold for every arm, written into the preregistration (a
`## Blinding` section naming the fixture root and the judge's labels) and checked
by reading the fixture tree before the freeze commit; `prereg --check` does not
read fixture trees, so this check is yours:
1. **Nothing the candidate sees carries an eval word.** No `eval`, `test`,
`judge`, `experiment`, `rubric`, `score`, `compare`, `benchmark`,
`candidate` or `arena` in any directory name, file name, prompt, commit
message or environment variable the preregistration controls and the candidate
can read. Two things are outside the rule: a project's own test directory, when
the task is about those tests, and the skill text an arm loads as its own
instructions (a gstack SKILL.md uses these words freely); the rule is about the
fixture and the task, which the eval builds.
2. **The directory looks like a project.** Name the fixture root the way the
project it imitates would (`billing-service/`, `docs-site/`), with the files
that project would plausibly hold, never `fixture-a/`, `case-7/` or the
model's name. A candidate working in `arm-b/` knows there is an `arm-a/`.
3. **The judge sees sanitized labels and the candidate's real reads.** The judge
gets `A` and `B`, never a model id, arm name, price or run number, and it
grades whether the candidate followed the chain (read the file the task
pointed at, then the file that one named) from the files the transcript shows
it actually opened, never from the candidate's own account of what it read.
A rule broken after a priced call is a new eval with a new preregistration, the
same as a changed bar.
## Step 2: zero-cost stub-model dry run
Run the pipeline over the real development rows with a stub model that costs nothing:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.264",
"version": "1.91.265",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+13
View File
@@ -978,6 +978,19 @@ For background mode (e.g., the user wants to keep working):
- **The profile never changes defaults.** This skill inspects and configures.
Per-question preferences can auto-decide questions; the declared/inferred
profile is advisory only.
- **Observable questions are not the user's to answer.** A question whose
answer a command, a render or a measurement can produce is a question for the
machine: "is this faster" is two timed runs, "which layout reads better" is two
renders side by side, "does the API accept this shape" is one call against a
local or test endpoint. The skill that would have asked does the observation
and reports what it saw, as long as the observation is local, read-only and
free; one that changes state, reaches a live or third-party system or spends
money is a one-way door and stays a question (see the rule above). Only a
product or preference call reaches the user (what the product should do, what
they want, what they will spend or risk), and it arrives with the observation
already made. When the question log shows a prompt whose answer was observable,
the fix belongs in the asking skill, not in a never-ask preference: flag it in
the audit as an observable question and name the check that replaces it.
- **Completion status:**
- DONE — did what the user asked (enable/inspect/set/update/disable)
- DONE_WITH_CONCERNS — action taken but flagging something (e.g., "your
+13
View File
@@ -662,6 +662,19 @@ For background mode (e.g., the user wants to keep working):
- **The profile never changes defaults.** This skill inspects and configures.
Per-question preferences can auto-decide questions; the declared/inferred
profile is advisory only.
- **Observable questions are not the user's to answer.** A question whose
answer a command, a render or a measurement can produce is a question for the
machine: "is this faster" is two timed runs, "which layout reads better" is two
renders side by side, "does the API accept this shape" is one call against a
local or test endpoint. The skill that would have asked does the observation
and reports what it saw, as long as the observation is local, read-only and
free; one that changes state, reaches a live or third-party system or spends
money is a one-way door and stays a question (see the rule above). Only a
product or preference call reaches the user (what the product should do, what
they want, what they will spend or risk), and it arrives with the observation
already made. When the question log shows a prompt whose answer was observable,
the fix belongs in the asking skill, not in a never-ask preference: flag it in
the audit as an observable question and name the check that replaces it.
- **Completion status:**
- DONE — did what the user asked (enable/inspect/set/update/disable)
- DONE_WITH_CONCERNS — action taken but flagging something (e.g., "your