ho-04.7 — sharibako run feedback (status line + signal-shutdown countdown)
- Ho-04.7-AT-01.md
Give run a voice at the two moments it currently runs silent: startup and Ctrl-C shutdown. Feedback only — no change to what run does.
This is the CLI-polish the Phase 3 → Phase 4 called for. Driving run on real secrets surfaced one class of friction, and it is entirely about feedback, not behavior. Fixing it here keeps it out of the Workshop (Phase 4), where the same silence would have to be re-solved in a GUI idiom.
Out of scope:
- Any change to
run's behavior — spawn, env merge, exit-code mapping, signal forwarding, the 5-second grace → SIGKILL, no-new-process-group all stay exactly as ho-04.5 shipped them. materialize/ingest/syncterseness. The practitioner hitrun; the others are a followup, not built here.- A live progress display during the run. The child owns the terminal (inherited stdio); sharibako cannot animate over a dev server's output. The only new in-run output is the fixed startup line before the child takes over.
- Secret values or key names in the feedback. The startup line reports a count only. Names remain available via
run --dry-run.
Problem
run is silent where a developer wants to know what is happening. Two moments, both raised from real use:
-
Startup is blank.
rundecrypts, injects, and spawns with no output of its own. The practitioner's read: "everything was fine just blah… it was just utilitarian." There is no confirmation of which scope resolved, how many secrets went in, or what is being launched — the first visible line is the child's own output. -
Ctrl-C shutdown is silent and feels slow. On a signal, ho-04.5's
SignalForwarderforwards to the child, waits the 5-second grace, then sends SIGKILL — and prints nothing across that entire window. When a child dies instantly on the tty-delivered signal this is invisible; when a child drags or catches the signal, the terminal hangs with no indication that sharibako is waiting or about to escalate. The practitioner's read: "the Ctrl-C was slow with no feedback — it kind of sucked."
Both are feedback gaps, not defects. run does the right thing; it just does it wordlessly.
Solution
Add two stderr-only feedback surfaces to run, gated on stderr being a TTY so scripted and piped use sees nothing new. No behavioral change.
-
Startup status line (before spawn): names the resolved scope, the count of secrets injected, and the command being launched. One line, to stderr. It subsumes the existing empty-scope note (a zero count says the same thing).
-
Signal-shutdown feedback (on SIGINT/SIGTERM/SIGHUP): an immediate
forwarding <signal> to child…line, then a plain integer countdown — one line per second across the existing 5-second grace (5,4,3,2,1), thenchild unresponsive — SIGKILLif the child outlives the window. New line per tick, no cursor control, so it survives scrollback. When the child dies promptly (the common terminal case) the countdown is cancelled the instantwaitUntilExit()returns, so a fast exit shows only theforwarding…line and returns to the prompt.
Everything sharibako prints goes to stderr, never stdout — $(sharibako run …) and pipes stay byte-clean. Emission is gated: on when stderr is a TTY, off when it is not (piped/scripted) or when --json is set, forced on by --verbose (the escape hatch for capturing feedback in a log).
The feedback logic — line formatting, countdown strings, the TTY/flag gate — sits behind an injectable sink, mirroring run's existing RunOutcome / lineReader seams, so it is unit-tested without a live process or a real terminal. The live signal plumbing in SignalForwarder stays coverage-excluded as it already is; only the message strings it emits become testable.
Changes
Single — the work is one bounded conversation across RunCommand, its feedback helper, and SignalForwarder. → /agent-tasks/Ho-04.7-AT-01.md
- New:
Sources/SharibakoCLI/Support/RunFeedback.swift— the feedback sink (aSendablewrapper over a line emitter), the pure formatters (startupLine,forwardingLine,countdownLine,sigkillLine), and the TTY/flag gate (isatty(STDERR_FILENO)+--json/--verbose). Production sink writes to stderr; the gate decides whether it emits at all. - Modify:
Sources/SharibakoCLI/Commands/RunCommand.swift— build the sink fromglobal+ TTY state, emit the startup line beforespawnAndWait, thread the sink intospawnAndWaitand the forwarder. The existing empty-scope stderr note folds into the startup line. - Modify:
Sources/SharibakoCLI/Support/SignalForwarder.swift— take the sink; emitforwardingLineon receipt, run a per-second countdown timer alongside the grace window, emitsigkillLineon escalation, and cancel the countdown inteardown()so a prompt exit prints nothing stray. - Tests:
Tests/SharibakoCLITests/RunFeedbackTests.swift(new) — the formatters (exact strings, count/scope present, no secret values or names), the countdown sequence, and the gate (TTY on / non-TTY off /--jsonoff /--verboseforced on) via an injected capturing sink.Tests/SharibakoCLITests/RunCommandTests.swift— assert the startup line emits through the injected sink on a real_run; update the existing empty-scope test to expect the zero-count startup line instead of the old note; confirm stdout stays clean (feedback never reaches it).
Results
Shipped in 93b9bc8 (Ho-04.7-AT-01). Build clean, 375 tests green (+13 new), both linters strict-clean. Dogfooded against the real diary scope through the signed install and a real terminal — startup line, stdout-clean-under-pipe, and the live SIGINT countdown all confirmed by the practitioner ("all good").
What the showed:
- Startup line renders as designed:
sharibako: scope 'diary' — 12 secrets → sh -c echo hi, to stderr, before the child's own output. - stdout stays clean.
sharibako run … | catpiped only the child'shi; the startup line went to stderr, never into the pipe. Note the gate follows stderr's TTY-ness, not stdout's — piping stdout (| cat) leaves stderr on the terminal, so feedback still shows there while stdout stays uncorrupted. Full suppression is2>/dev/null(or--json/ a non-terminal stderr). - Countdown fires correctly against a SIGINT-ignoring child:
forwarding SIGINT to child…→waiting for child to exit… 4·3·2·1→child unresponsive — sending SIGKILL. A prompt-exiting child (plainsleep 30) shows only theforwarding…line and returns, no stray ticks.
Two decisions worth recording against the shipped behavior:
- The countdown reads seconds-until-SIGKILL —
4·3·2·1, not5·4·3·2·1. With the first tick at +1s and SIGKILL at the 5s grace boundary, each tick truthfully names the seconds remaining before escalation (4 down to 1). This also keeps a fast-dying child quiet —teardown()cancels the timer before the first tick, so a normal Ctrl-C shows onlyforwarding…. The ho's earlier "5, 4, 3, 2, 1" was illustrative of "a plain integer countdown"; the shipped truth is4·3·2·1. - The passthrough
--is stripped for display only..captureForPassthroughkeeps--in the command array; the startup line drops a leading--, while the spawn still passes the raw array to/usr/bin/env(which swallows it). Surfaced by the first_runtest render.
Done means:
runagainst a TTY prints a one-line startup status (scope, secret count, command) to stderr before the child runs; stdout is untouched.$(sharibako run … )andsharibako run … | catsee no sharibako feedback — the gate holds on non-TTY.- Ctrl-C on a slow/uncatching child shows
forwarding…, a plain integer countdown5…1, then the SIGKILL line; a prompt-exiting child shows onlyforwarding…and no stray ticks. - No secret value or key name appears in any feedback line (count only).
run's behavior is byte-identical to ho-04.5: exit codes, signal forwarding, grace → SIGKILL, no new process group all unchanged; the existingruntest matrix stays green.- Feedback formatters and the gate are covered ≥ 90%; the live signal plumbing stays coverage-excluded by comment.
swift build,swift test,swift-format lint --strict,swiftlint --strictall clean.
Flagged to the practitioner (not built here):
- The ho-overview lists eleven hos across seven phases;
ho-04.7is an insert at the Phase 3 → Phase 4 checkpoint, not yet reflected there. The overview update isho-kamae-4-overview-collaborator's territory — surfaced, not edited from this ho. materialize/ingest/synccarry the same terseness; a later polish ho can address them if real use asks for it.
Authored: 2026-07-03.
Executed and closed: 2026-07-03. Commit: 93b9bc8.
Rendered from the corpus, verbatim · source on GitHub →