Repository navigation
Description
What to build
Make a Command's complete record—the stamp, Command log, terminal output and Activity log—owned by one instance-testable implementation of the logs module. Preserve the record the Day shift sees today, including quiet skips, child Run relay and warnings when a log cannot be written.
Closed #344 established the production recording interface. This Ticket deepens its implementation: it removes the separate global lifecycle owners and the warning callbacks that re-enter the recording interface. Callers continue to report what began, was configured, started, was skipped or ended. Tests exercise those same operations on an isolated recording instance, without launching Git, GitHub or a Harness.
Acceptance criteria
- One owned recording implementation holds the stamp, configured logs root, child/first-start rules, held output, Command log state and path, started Activity log subject, and independent warning suppression. Its private helpers own no process-wide lifecycle state.
- The existing process-wide logs interface delegates to one synchronized instance of that implementation. Independent instances can be constructed in tests without resetting globals or changing process environment. The stamp query returns an owned string and its Session log and child Run consumers are adapted mechanically.
- Beginning, configuration, start, skip, end, held-output release, output and queries follow the decisions below. All existing filenames, timestamp formats, lines, newline/flush behavior, quiet-skip behavior and pre-configuration root-query refusal remain compatible.
- Internally generated progress and warning lines enter the owner's output path directly. No recording operation calls back through the process-wide progress/output interface while holding its recording lock. Concurrent output still has the same serialized terminal-and-Command-log order.
- Command log creation or append failure disables further Command log writes and warns once. Activity log failures warn once independently and remain nonfatal. Partial creation retains the same discoverable Command log path. Terminal failures remain ignored. An Activity log warning reveals held quiet-pass output, and a simultaneous Command log failure cannot recurse or deadlock.
- Skip suppression still consults the repository's shared Activity log tail, including other commands' lines and a cut first line; independent recording instances suppress the same repeated skip. An intervening start/end or changed reason records a fresh skip of that kind.
- Recording-interface tests cover every beginning/work kind, first-start-wins, child suppression and inherited stamps, complete held-output transitions, mixed output order, lazy stamp queries, no-work endings, isolated instances, and creation/append/Activity/terminal failure combinations. They assert observable output, file content, paths and warnings rather than private state or lock layout.
- Preserve CLI integration checks for event wiring, Session log stamps, relayed Ticket/Base fix output, dispatched Run suppression, notification links, Origin match, non-work commands, quiet skips, concurrent passes and real filesystem failures. Replace redundant private-helper and full-binary lifecycle permutations only after equivalent recording-interface coverage exists.
- Formatting, linting and the full test suite pass. No new user setting or domain term is introduced, and ADR 0011's logging behavior is preserved.
Blocked by
None (can start immediately).
Decisions
The design tree is: compatibility → owned implementation → recording interface, internal seam and concurrency → lifecycle, time, warnings and shared skip history → testing and delivery. The frontier was settled in three rounds, as follows.
-
Round 1 — Preserve the current recording contract. The production logs interface from Deepen the Command log and the Activity log into one logs module that owns a command's record #344 already gives callers depth. The friction is inside its implementation: one Command's lifecycle is split across the logs, Command log and Activity log owners. Preserve ADR 0011 and observable behavior so this Ticket can be verified as a refactor. Additional logging, scheduling, dashboards, cross-process locking and changes to Run notifications are outside this Ticket.
-
Round 1 — Keep process-wide access, with one owned implementation. A Command is one process, and output arrives from its main thread and its session/child reader threads. Retain one lazily initialized, mutex-protected production instance. Put all recording lifecycle state in that instance; private Command log and Activity log modules may retain formatting and file helpers. This concentrates the state and ordering without threading a recording handle through every caller.
-
Round 2, after ownership — Use the same recording interface in production and tests. The owned implementation exposes begin, configured, started, skipped, ended, show-held, stdout/stderr output and root/stamp/Command-log-path queries. The production facade delegates to it; tests construct it directly. Return an owned string from the stamp query because neither existing consumer requires a static reference, and cloning permits the stamp to live under the owner's lock. Preserve the root query's existing panic before configuration.
-
Round 2, after ownership — Add one private effect seam for the complete record. The production adapter supplies local time, terminal emission/flush, Command log opening/writing/path-existence observations, and Activity log tail reads/appends. A scripted memory adapter supplies deterministic time, output capture, shared file contents and controlled failures. The owner decides filenames, retention, skip suppression and warning policy; adapters perform effects. Command log writers may be adapter-provided owned writers. Open failures must retain whether the path already exists, and failures while flushing held lines must retain a created path. Two actual adapters justify this seam; it stays internal to the logs module.
-
Round 2, after the interface and seam — Serialize each operation and its emitted output. Keep terminal writes/flushes and Command log writes in the same recording lock, including concurrent relays. Finish an operation's generated lines before another operation enters. Neither adapters nor private helpers call the production logs/progress interface. Use a pure progress-line formatter for generated lines; ordinary and child-relayed lines supplied by callers remain byte-for-byte unchanged. This removes the current warning callback cycle instead of merely exchanging its locks.
-
Round 3, after ownership and the clock seam — Preserve time and path semantics. Obtain local time through the adapter. Filename stamps retain year/month/day, hours/minutes/seconds and numeric UTC offset; first lines retain local date and offset; Activity log lines retain local date and time; progress lines retain local time. Preserve lazy stamp creation before begin, and initialize a child Run's stamp from its parent's supplied value. The repository, command kind and issue still choose the existing log locations. A fresh recording instance has fresh state, while separate production processes still share the repository's Activity log file.
-
Round 3, after the interface — Preserve the complete lifecycle. Ordinary Commands hold lines for their future Command log from begin; passes also hold terminal output until configuration permits it or work/failure reveals it. A child Run emits no dated beginning and creates no Command or Activity log, while its output continues to its parent. The first started work wins; dispatched Runs' later starts do nothing. Starting work reveals held lines once, opens the Command log with all held lines, emits the logging/Harness lines, then records the Activity start naming that log. End reveals held lines and records an Activity end only for work that started. A failure before work and a non-work command keep no log. Do not add new idempotence or lifecycle refusals to existing operations.
-
Round 3, after serialized output — Route warnings without re-entry. Change the recording state before emitting a failure warning. Use an owner-local pending-output queue or equivalent iterative drain for generated warnings, so an Activity warning can encounter a Command log failure without recursion. Command log failure retains its discoverable path, stops file writes and emits its one warning; Activity failure keeps trying later Activity writes but emits only its first warning. These policies are independent. Terminal failures are ignored, as today. Activity warnings reveal held lines even on a quiet skipped pass. This preserves diagnostics while removing lock-release choreography from the private helpers.
-
Round 3, after the file seam — Keep skip suppression based on shared history. Read the same final 64 KiB of the repository's Activity log, discard an initial incomplete line when reading from mid-file, and compare the last line of the same command kind. Keep this decision in the owner, with tail I/O behind the adapter. An in-memory last reason cannot replace the file lookup because a previous process may have recorded a start/end or a different skip. Preserve the existing append behavior; stronger inter-process atomicity is a separate change.
-
Round 3, after lifecycle and failures — The interface is the test surface. Drive complete recording operations against the memory adapter and assert visible terminal output, stored files, query results and warning counts. Share the memory Activity log between distinct instances to verify cross-command skip history. Exercise simultaneous log failures, append failure after successful creation, output with no factory beginning, quiet warnings and repeated isolated instances. Existing scripted adapters in Setup and Sessions are prior art for deterministic behavior tests; test no private state enum or mutex choice. Add a focused threaded check for serialized output without asserting which thread wins.
-
Round 3, after the interface tests — Retain integration checks that cross real seams. Keep binary coverage for caller wiring, actual files and local timezone formatting, parent/child stamp propagation and relay, notification Command log paths, Origin match, non-work commands, dispatch suppression, quiet skips and concurrent passes. Move repetitive helper/lifecycle permutations to the recording interface once covered there; do not replace real filesystem checks with mock-only coverage. This gives test leverage without carrying duplicate tests of the same implementation detail.
-
Round 3, after scope and verification — Publish one standalone Ticket. The work is one cohesive ownership refactor inside the logs module with a few mechanical consumers and its tests, so one fresh context window is sufficient. It has no blockers and requires no Spec. It uses the existing Command, Command log, Activity log and Session log terms; no CONTEXT.md change or new ADR is needed because the domain behavior and ADR 0011 remain unchanged and the internal design is reversible.
Architecture review
Strength: Strong · Dependencies: local-substitutable
Files: src/logs.rs, src/logs/command_log.rs, src/logs/activity.rs, src/progress.rs, src/session.rs, src/child_run.rs, tests/command_logs.rs, tests/activity_log.rs
Before
flowchart LR
callers["Command callers"] --> logs["Logs module"]
progress["Progress module"] --> logs
logs --> record["Command record"]
logs --> command_log["Command log"]
logs --> activity["Activity log"]
activity -. "path and held output" .-> command_log
command_log -. "warning callback" .-> progress
activity -. "warning callback" .-> progress
classDef leak stroke:#dc2626,stroke-width:2px;
class record,command_log,activity leak
After
flowchart LR
callers["Command callers"] --> logs["Logs interface"]
progress["Progress module"] --> logs
logs --> record
subgraph recording["Owned recording module"]
record["Command record"] --> command_log["Command log"]
record --> activity["Activity log"]
end
record --> seam["Private effect seam"]
seam --> machine["File, console and clock adapter"]
seam --> memory["Scripted memory adapter"]
classDef deep stroke-width:3px;
class recording deep
Problem: the logs module is deep for callers, but its shallow internal lifecycle operations spread state and warning callbacks across three global owners, leaving complete verification to binary invocations.
Solution: deepen the recording implementation into one owner whose existing event/output interface hides the lifecycle and a private adapter seam.
Wins
- Locality: state and warnings concentrate
- Leverage: complete events tested in-process
- Interface owns output ordering
- Depth replaces callback choreography
- Independent adapters expose failure combinations
Deletion test: deleting the owner would redistribute held-output, lifecycle and warning ordering among the log implementations.
Covered by: no open issue; closed #344 established the facade and retained the separate global implementations.
Review evidence: reviewed main at e3c24b2; all 25 existing Command log and Activity log integration tests passed.
Activity