Skip to content

feat(052): stop means stopped, and the terminal can say so (US2) - #281

Merged
guan4tou2 merged 3 commits into
mainfrom
feat/052-us2-terminal-control
Oct 8, 2026
Merged

guan4tou2 merged 3 commits into
mainfrom
feat/052-us2-terminal-control

Conversation

@guan4tou2

Copy link
Copy Markdown
Owner

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.

redlog status     recording on/off, mode, bound project
redlog stop       stops recording IN THIS TERMINAL, durably
redlog start      starts again
redlog mode auto|manual
redlog class list|add <class> <cmd>|remove <cmd>

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 preexec runs 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,
contracts/shell-commands.md rule 2).

The per-prompt read is grep on that file, not a python3 spawn. 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 ask
for 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 stop and redlog start 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 timelineSessionBands.ts draws a band from it; reusing it
for 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 manual writes ~/.redlog/terminal-mode and applies to the
terminal it was typed in. An operator who types it means now, not "from the
next terminal I open". auto is what an install leaves behind: a feature whose
point 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 class writes ~/.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.

list prints the two classes that are lists and says what the third one is:
relayed is 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.ts runs thirteen sequences through
both 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 manual happens in a
terminal RedLog knows nothing about, and a panel showing a stale auto is
worse than one showing nothing.

redlog itself runs while stopped. An operator who has stopped recording
must still be able to see that they have, and to start again — so preexec's
gate lets redlog … through and nothing else.

Found while building, fixed here

  • The agreement tests earned their place twice more. Making the class
    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. Python has no typecheck; this test
    is the one.
  • write_json never created its parent directory, so the first terminal
    on a machine wrote no state file at all and redlog status answered "not
    enrolled" forever.
  • A Python-scripted edit rewrote hooks/shell-zsh-hook.zsh as CRLF
    (write_text is text mode on Windows). zsh then failed with
    command not found: ^M and the adapter never loaded. .gitattributes
    already pins *.zsh/*.sh/*.py to LF, so a clean checkout was never
    affected — but it cost three rounds to find, and it is in the session memory
    now.
  • The harness unsets PROMPT_SP. zsh prints a reverse-video % when
    output 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 typecheck
  • npm run verify:specs
  • npm run verify:architecture
  • npm run verify:i18n
  • npm test — 3126 passed, 25 skipped (after merging current main)
  • npm run build
  • full e2e — 101 passed, 4 skipped (5.0 min), run locally because CI skips
    e2e 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

guan4tou2 and others added 3 commits October 7, 2026 17:27
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>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e5099578-d0e1-41be-b15a-adbe0fc658d5
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@guan4tou2
guan4tou2 merged commit eb3bfef into main Oct 8, 2026
5 checks passed
@guan4tou2
guan4tou2 deleted the feat/052-us2-terminal-control branch October 8, 2026 02:57
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)
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