Repository navigation
feat(052): stop means stopped, and the terminal can say so (US2) - #281
Merged
Merged
Conversation
An audit logger that cannot be stopped from inside the terminal it is logging is one that gets uninstalled before the engagement that needed it. US2 is the half that makes an operator willing to leave the first half on. `redlog status|start|stop|mode|class` — one function, so the installed block defines exactly one name, which is what makes uninstall verifiable (FR-016). An unknown subcommand prints the list and exits 2, so a typo is not silently a no-op. The stop is durable because it is a file. A stop held in a shell variable survives neither a subshell nor the next prompt, and the next `preexec` is in a shell that has kept no memory of the last one (FR-022). It is the same file RedLog reads for the capture card, so the terminal and the card cannot disagree (FR-023). The per-prompt read is `grep` on that file rather than a python3 spawn: same answer, same place, and not a cost paid on the operator's prompt forever. A stopped terminal emits NOTHING for the commands after it -- not rows marked `not-captured`. An operator who stopped recording did not ask for a record of what they did with the recording off. The `redlog stop` itself is recorded, and that is what accounts for the gap (FR-012). Mode is machine-level and applies to this terminal immediately: an operator who types `redlog mode manual` means now, not "from the next terminal I open". `auto` is what an install leaves behind, because a feature whose point is that there is nothing to type is not one you opt each terminal into. `redlog class` edits an overlay at ~/.redlog/command-class.json, never the shipped hooks/command-class.json -- an install must be able to replace that file without taking the operator's choices with it. Adding to the pty class warns that local suspension is lost (FR-028), because that cost is not obvious and is paid mid-engagement on the command the operator cares most about. The state machine now exists twice: in TypeScript for the card and Settings, in Python for the prompt, and neither can call the other. test/terminal-enrollment-agreement.test.ts runs thirteen sequences through both and compares -- the same arrangement that caught `classify` reading its own `--` separator, and it earned its place again immediately: making the policy overlay-aware left `classify` reading an `opts` it never bound, so every call raised NameError. The adapter suppresses the relay's stderr, so every command would have classified as `native` and capture would have stopped entirely, with no error anywhere. Also: the harness unsets PROMPT_SP. zsh prints a reverse-video `%` when output does not end in a newline, which is a feature for a human and noise for a test comparing a one-word file's contents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e shell (T034-T035) T035, FR-012. "No events for twenty minutes" reads very differently as "the operator stopped recording" than as "the operator was reading", and a reader a year later cannot tell either from silence. `redlog stop` and `redlog start` now write `shell.capture_stopped` / `capture_resumed`, carrying the terminal's session id and the reason, so the gap has two ends and an owner. Not `system.recording_paused`. RedLog already brackets its GLOBAL pause with that pair and the timeline draws a band from it; reusing it for one terminal would paint a paused band across an engagement that never stopped recording. The test asserts the bracket means something -- the two unrecorded commands fall between the rows -- rather than that two rows exist. T034. Settings now answers "what will my shell do": the mode `redlog mode` last wrote and the class lists `redlog class` last edited, read from the files the shell reads. Read-only, on purpose. The lists are edited from the terminal, which is where the operator is standing when they find out something went through a relay; a second editor in Settings would be a second place for the policy to change and a second thing to keep in step. Not cached either: `redlog mode manual` happens in a terminal RedLog knows nothing about, and a panel showing a stale `auto` is worse than one showing nothing. An older preload leaves the note out rather than throwing -- a renderer can outlive its bridge, and that needs a full reload rather than HMR. The merge of defaults-plus-overlay is a third place the policy is reasoned about, so test/command-class.test.ts now also compares mergeClassPolicy against the shell's own `policy --action list` after two real edits. That family of test has caught two real divergences on this branch already. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
8 tasks done
guan4tou2
added a commit
that referenced
this pull request
Oct 8, 2026
feat(052): stop means stopped, and the terminal can say so (US2)
guan4tou2
added a commit
that referenced
this pull request
Oct 8, 2026
feat(052): stop means stopped, and the terminal can say so (US2)
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.
What changed
Spec 052's US2: the operator can see what the terminal is recording, and stop
it — from inside the terminal, durably.
US1 (#276) removed the prefix. This is the half that makes an operator willing
to leave that on. An audit logger that cannot be stopped from inside the
terminal it is logging is one that gets uninstalled before the engagement that
needed it.
One function, so the installed block defines exactly one name — which is what
makes uninstall verifiable (FR-016). An unknown subcommand prints the list and
exits 2, so a typo is not silently a no-op.
Why
The stop has to be a file
A stop held in a shell variable survives neither a subshell nor the next
prompt, and the next
preexecruns in a shell that has kept no memory of thelast one (FR-022). It is the same file RedLog reads for the capture card, so
the terminal and the card cannot disagree (FR-023,
contracts/shell-commands.md rule 2).
The per-prompt read is
grepon that file, not apython3spawn. Same answer,same place, and not a cost paid on the operator's prompt forever.
A stopped terminal emits nothing
Not rows marked
not-captured. An operator who stopped recording did not askfor a record of what they did with the recording off.
…but the gap is still accounted for (FR-012)
"No events for twenty minutes" reads very differently as "the operator stopped
recording" than as "the operator was reading", and a reader a year later
cannot tell either from silence.
redlog stopandredlog startwriteshell.capture_stopped/capture_resumedcarrying the terminal's session idand the reason, so the gap has two ends and an owner.
Not
system.recording_paused. RedLog already brackets its global pausewith that pair and
timelineSessionBands.tsdraws a band from it; reusing itfor one terminal would paint a paused band across an engagement that never
stopped recording.
Mode is the machine's, and it means now
redlog mode manualwrites~/.redlog/terminal-modeand applies to theterminal it was typed in. An operator who types it means now, not "from the
next terminal I open".
autois what an install leaves behind: a feature whosepoint is that there is nothing to type is not one you opt each terminal into
(research.md D4).
Class edits go in an overlay
redlog classwrites~/.redlog/command-class.json, never the shippedhooks/command-class.json— an install must be able to replace that filewithout taking the operator's choices with it. Adding to the
ptyclass warnsthat local suspension is lost (FR-028), because that cost is not obvious and
is paid mid-engagement on the command the operator cares most about.
listprints the two classes that are lists and says what the third one is:relayedis not a list, it is what a command is when it is in no other one,and printing "everything else" as a list would be a lie the moment the
operator ran something new.
Deliberate calls worth a reviewer's attention
The state machine now exists twice, in TypeScript for the card and
Settings and in Python for the prompt, and neither can call the other.
test/terminal-enrollment-agreement.test.tsruns thirteen sequences throughboth and compares — including the four that matter most, where nothing talks a
project-switched terminal back into recording (FR-010).
Settings is read-only about the policy. The lists are edited from the
terminal, which is where the operator is standing when they find out something
went through a relay; a second editor in Settings would be a second place for
the policy to change and a second thing to keep in step. What Settings owes is
the answer to "what will my shell do", from the file the shell reads
(research.md D7). It is also not cached:
redlog mode manualhappens in aterminal RedLog knows nothing about, and a panel showing a stale
autoisworse than one showing nothing.
redlogitself runs while stopped. An operator who has stopped recordingmust still be able to see that they have, and to start again — so
preexec'sgate lets
redlog …through and nothing else.Found while building, fixed here
policy overlay-aware left
classifyreading anoptsit never bound, soevery call raised
NameError. The adapter suppresses the relay's stderr, soevery command would have classified as
nativeand capture would havestopped entirely, with no error anywhere. Python has no typecheck; this test
is the one.
write_jsonnever created its parent directory, so the first terminalon a machine wrote no state file at all and
redlog statusanswered "notenrolled" forever.
hooks/shell-zsh-hook.zshas CRLF(
write_textis text mode on Windows). zsh then failed withcommand not found: ^Mand the adapter never loaded..gitattributesalready pins
*.zsh/*.sh/*.pyto LF, so a clean checkout was neveraffected — but it cost three rounds to find, and it is in the session memory
now.
PROMPT_SP. zsh prints a reverse-video%whenoutput does not end in a newline — a feature for a human, noise for a test
comparing a one-word file's contents.
Verification
npm run typechecknpm run verify:specsnpm run verify:architecturenpm run verify:i18nnpm test— 3126 passed, 25 skipped (after merging currentmain)npm run builde2e when an earlier gate fails and this branch changes the adapter
Tasks T029–T035 of 47; Phase 4 (US2) complete. Next is Phase 5 (US3,
T036–T038+): honest degradation — RedLog unreachable, the stand-down path, and
the command's own exit status when the relay dies mid-command.
🤖 Generated with Claude Code