Skip to content

Risky changes need a person, loopstra retro, and acceptance tests from Done when - #91

Merged
jflavan merged 25 commits into
mainfrom
feat/risk-retro-acceptance
Oct 28, 2026
Merged

jflavan merged 25 commits into
mainfrom
feat/risk-retro-acceptance

Conversation

@jflavan

@jflavan jflavan commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Three features that make the loop safer to leave alone and easier to tune. Design: docs/superpowers/specs/2026-10-27-risk-retro-acceptance-design.md; plan: docs/superpowers/plans/2026-10-27-risk-retro-acceptance.md; each call is a bullet in docs/decisions.md.

Risky changes need a person

  • gates.merge.risky: { paths, lines, human }. A change that touches a matching file (Bun.Glob, renames count under either name), or changes more than lines lines, waits for a person even with gates.merge.human: none.
  • Decided once, in passMergeGate, on the code that will merge. It is recorded as a runtime merge_gate: pr | status frontmatter field in the same record as the move to merge-review, plus a merge/risk gate row. Its note names up to 3 files.
  • changeGates gives every reader the change's own gate: waitsForPerson, isRunnable, offers, attention, mayMergeFirst, the merge stage, and Per-change resources and automatic conflict resolution #90's kept-mergeable check.
  • Off by default. loopstra setup github suggests paths from the files in the repo.

loopstra retro [--days N]

  • A read-only plain-text report over the trace: what reached main, where changes blocked (classified by the runtime's own note constants), gate first-time pass rates, spending by kind of step, how long people kept changes waiting.
  • Ends with suggestions, each with its evidence and the setting to change: cheaper judges, fix loops, timeouts, review rounds, spending limit, a person who may not be needed, risky paths, acceptance tests.

Acceptance tests from Done when

  • stages.build.acceptance_tests (schema default off; loopstra init turns it on). Before each build, a fresh acceptance session writes tests from the owner's Done when list.
  • The tests are committed on the branch and recorded as intent/<slug>/acceptance.md. The protect-tests hook keeps build/fix/reconcile/revise off them, and the done-check reads them as evidence.
  • A failed acceptance step never blocks. A wrong acceptance test is named in the block note.
  • New step key acceptance (model follows the build stage), steerable like build.

Review

I ran /code-review high on the branch and fixed these findings:

  • The acceptance step held only test paths, and dropped test-command output before committing. The test-path rule now lives in src/test-paths.ts, shared by the hook, which now also allows fixture folders.
  • The done-check ignores a stale acceptance.md.
  • Risk is not scored when a person is already on the merge gate.
  • The risk note says how to stop the change and gives way to a pull request's wait.
  • Retro leaves merge/risk out of gate lines.
  • Retro's waits are found in one pass.
  • The hook comment is up to date.

Left as designed:

  • merge_gate stays on a merged change as a record.
  • buildRetro keeps its own window filter, since it is a pure function over all-time status moves.

Merged with main after #90; staleWhileWaiting now reads each change's own gate.

Test plan

  • bun run check: typecheck, lint, dead-code, and 1953 tests pass, 0 fail
  • New tests: tests/unit/risk.test.ts, tests/integration/risk-gate.test.ts, tests/unit/retro.test.ts, the acceptance cases in stages-build/stages-verify/hook, the risky kept-mergeable case in conflicts
  • CI on ubuntu, macOS and Windows

🤖 Generated with Claude Code

jflavan and others added 25 commits October 27, 2026 21:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, gate rows, phases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stages.build.acceptance_tests (off by default; init's template turns it
on), the acceptance step under build, its envelope and prompt, a
steerable work session, and the yes/no question in setup gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gates.merge.risky names patterns of files (Bun.Glob, repo paths with /),
an optional size in lines, and who decides (pr or status). src/risk.ts
scores a branch against main from where they parted, outside intent/,
from git diff --numstat -z -M: binary files add no lines, a rename
counts under whichever of its names a pattern matches. It also words
the gate row's evidence and the owner's note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reach

The acceptance step may write only tests and test fixtures; build, fix,
reconcile and revise may not edit the files in LOOPSTRA_ACCEPTANCE, and
each refusal is logged for the block note. The fix rule is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frontmatter gains merge_gate (pr or status; blank reads as absent and
gets no line). changeGates gives one change's gates: the config's, with
merge_gate on the merge gate when the config has nobody there; it never
takes a person off. waitsForPerson, isRunnable and offers apply it
themselves, so the scheduler, queue.md, status, the console and chat
all see a risky change as waiting for a person; apply's approve and
send-back check, the attention note and plan-ahead's mayMergeFirst use
it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rds it

passMergeGate scores the change in its worktree once the merge checks
pass, on the code that will merge, and writes a merge/risk gate row
(pass "nothing risky", or waiting with the files and size) when the
config names anything risky. With nobody on the config's merge gate, a
risky change gets merge_gate (gates.merge.risky.human) in the same record
as its move to merge-review, with a note naming up to three files; a
change that is not risky loses the field in that record (or in the
record of its merge). Without a remote, merge_gate pr acts as status.

mergeHuman gives a change's own merge gate; the pull request watcher,
the no-remote merge-review wait and mergeRetry read it. setStatus takes
frontmatter fields to set or remove in the same record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es, spending, waits, and suggestions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When stages.build.acceptance_tests is on, a fresh acceptance session
writes tests from Done when before the build session. The runtime
commits them on the branch, records acceptance.md on main, and keeps
the files in the run folder's acceptance marker, once per build. Every
session gets them in LOOPSTRA_ACCEPTANCE; a refusal is named in the
block note, and the drift check leaves them out. A failed session does
not block: its files are dropped and the build goes on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With nobody on the merge gate, loopstra setup's GitHub section asks
which files need a person before merging, suggesting .github/**,
**/migrations/** and the dependency and build files at the top of the
repository; the file's own list is kept as the suggestion, - empties it,
and --defaults never adds one.

The README covers gates.merge.risky under GitHub and Configuration and
merge_gate under When something needs a person; intent/README.md tells
owners what merge_gate means; the config template shows a commented
example; decisions.md records each call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The done-check prompt gets {{acceptance}}: acceptance.md's Covered by
tests list, or a sentence saying none were written. A passing acceptance
test meets its criterion. A repository's older done-check.md without the
variable gets the list appended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/decisions.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README (How it works, the step tables, Configuration, Steering and the
test-protection hook), the owners' guide on acceptance.md, and the
decisions behind them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With acceptance_tests the flow map passed the YAML line width, so the
stamped config no longer read back exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	README.md
#	docs/decisions.md
…one when

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	README.md
#	docs/decisions.md
#	src/config.ts
#	src/models.ts
#	src/phases.ts
#	src/prompts.ts
#	src/stages/merge.ts
#	templates/config.yaml
#	tests/unit/console-agent-settings.test.ts
#	tests/unit/models.test.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k ignores a stale acceptance.md

Review findings: the acceptance step protected any file it touched or named,
fixture folders like testdata/ were refused, and the done-check read
acceptance.md from an earlier build. The test-path rule now lives in
src/test-paths.ts, shared by the hook and the step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Risk is scored only when nobody is on the config's merge gate, so no
  waiting row is written that changes nothing.
- The risky note says how to stop the change, and a pull request's wait
  stands in for it like the other merge-gate notes (isRiskNote).
- Retro leaves merge/risk out of its gate lines: waiting there is a person
  deciding, not a failed check.
- Retro finds each move's next move in one pass instead of a scan per move.
- Stale-while-waiting (from #90) reads each change's own merge gate, so a
  risky change waiting without a remote is kept mergeable too.
- hookSettings' comment names every phase the hook acts in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jflavan
jflavan merged commit 3cbe687 into main Oct 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant