Skip to content

Deepen Harness process ownership across every Model/Effort check #452

Closed

Description

@JacobStephens2

What to build

Stopping a Command during a Harness's Model/Effort check must stop the check and its owned child processes before any Claim, worktree or Command log is created. Gather Claude's minimal Model check and the free Codex, agy and Grok catalog reads into the existing deep Harness process module, which already owns sessions, Muse/OpenCode checks and OpenCode exports.

The same ownership must work when Setup calls a check, both through thirdshift setup and the first Run's offer. Cancelling a question or an in-flight check ends Setup with interrupted, leaves unanswered settings and Credentials unwritten, restores terminal echo, and never retries the cancelled check.

This is one standalone Ticket: four caller migrations, the terminal cancellation needed to make those migrations safe, and their behavior tests fit one fresh context window.

Acceptance criteria

  • Claude's minimal Model check and the Codex, agy and Grok catalog commands use the existing captured-output operation of the Harness process module. Remove their independent spawn/write/wait plumbing; leave no forwarding-only compatibility module. Keep catalog parsing and Model/Effort rules in their respective Harness implementations.
  • Each migrated check remains interruptible while its CLI is running and after CLI exit while captured output is still held open. On interruption or transport failure, use the existing adapter-directed stop path, reap the owned CLI and join its I/O workers before returning. An observed SIGINT, SIGTERM or SIGHUP reports interrupted.
  • Preserve each check's arguments, current directory, environment, prompt content/transport, refusal diagnostics and success policy. Claude keeps its plain-text minimal prompt on stdin, written once and closed; catalogs keep null stdin. Preserve each Harness's default/alias/canonicalization behavior, catalog cache location and refresh ordering, and which choices skip or require a check. Prompt-write errors are reported through the owner rather than discarded.
  • Production Setup starts persistent interruption recording before its first terminal question, including in the first-Run offer. Command startup can reuse that installation without clearing a recorded request or adding duplicate signal actions. Unattended Setup, scripted Setup tests, help/version/update and email-test acquire no new signal behavior through this change.
  • Ordinary and hidden terminal input observe interruption without waiting for another line. Preserve defaults, trimming, UTF-8 handling, pasted multiple lines, EOF and partial final lines. Restore the exact saved terminal attributes on success, EOF, error and interruption; remove the competing fatal-signal handlers from the hidden-input implementation.
  • All six Harnesses' Setup I/O refusal/fallback paths propagate observed interruption before retrying a Model/Effort question or keeping settings after an unreadable catalog. Preserve retries and catalog-failure fallback for ordinary errors. Check for recorded interruption before the first settings write; cancelling questions/checks changes neither an existing User config nor Credentials and starts no factory work or notification.
  • Add process-isolated regressions for the four newly owned Command checks: interruption during a live check, an owned detached descendant, and a finite same-group pipe holder after CLI exit. Verify failure before the Claim/worktree/Command log and termination of exact recorded owned processes. Exercise the three termination signals across the cases; do not infer success from error strings alone.
  • Add terminal regressions for explicit Setup and the first-Run offer, covering both catalog and minimal-call cancellation. Cover all six Harness I/O catch paths without turning interruption into a refusal/retry/fallback. After successful and refused/repeated checks, cancelling an ordinary question or hidden key input still exits, preserves files and restores echo. Include pasted multi-line input and partial-line EOF regressions at the terminal input interface.
  • Retain existing shared-process, session/process-tree, Resume, recovery, Harness-choice, Setup and Setup-offer behavior tests; replace obsolete caller-lifetime assertions once the owning interface tests cover them. Formatting, linting and the full test suite pass on the supported Linux/macOS platforms. Test cleanup uses exact recorded PIDs and finite commands.

Blocked by

None (can start immediately).

Decisions

The design tree is settled in three rounds: scope and constraints; ownership, signal lifecycle and input; error propagation and verification. The dependent decisions below leave no open frontier.

  1. Round 1 — scope and ranking. Deepen the existing Harness process module to own the four remaining preflight child launches, with the Setup cancellation those callers require. Reason: recent changes concentrated in Harness execution and Setup; the code still carries four independent child lifetimes, and a measured interruption gap makes this the highest-ranked uncovered Strong recommendation. A catalog read is still a Harness child even though it costs no model turn.

  2. Round 1 — constraints. Preserve the existing Harness/Model/Effort selection, check frequency, CLI arguments, fixed environments, current directories, trust/approval flags and adapter stop signals/grace periods. Add no timeout, setting, new Harness, catalog unification or session protocol change. Reason: the adapters already encode real differences, and ADRs 0012 and 0013 settle unattended session execution. Ownership can deepen without reopening them.

  3. Round 1 — seam and deletion test. Reuse the private Harness process seam and its existing six adapters; introduce no second process trait. The dependencies are local-substitutable: real local child processes and fake CLI executables. Reason: this is an existing real seam with multiple adapters. Deleting the deepened module would spread concurrent prompt/output I/O, interruption polling, child reaping, worker joining and tree cleanup back into sessions and at least four more check callers, demonstrating depth, locality and leverage.

  4. Round 2 — interface, dependent on 1–3. Keep the existing captured-output operation: selected adapter, prepared process command and optional stdin text in; standard process output or transport error out. Its existing streaming operation remains as it is. Reason: every newly migrated caller already needs captured bytes and exit status; none needs a child handle or a new lifetime choice. Adding a second ownership mode would make callers learn lifecycle rules again.

  5. Round 2 — implementation split, dependent on 4. Callers prepare their own check arguments and interpret status/output. The owner applies fixed environments, isolates the process group, drains stdout/stderr concurrently, writes/closes optional stdin, supervises completion and cleans up errors. Grok still refreshes its cache before reading metadata. Reason: lifecycle knowledge gains locality while Harness-specific validation and diagnostics remain where they are already understood. Claude's synchronous write and ignored write error disappear behind the same implementation used by other captured callers.

  6. Round 2 — Setup lifecycle, dependent on 4–5. The production terminal adapter initializes interruption recording once before its first ordinary or hidden question and keeps it for the remainder of the process; Command startup reuses an idempotent installation. Leave nonterminal Setup and scripted adapters alone. Keep the request sticky. Reason: Setup can invoke these checks before Command startup installs its handler, and an isolated child would otherwise survive a terminal interrupt that kills only its parent. Starting at terminal input also avoids changing the earlier nonterminal preparation path.

  7. Round 2 — rejected temporary-handler design, dependent on 6. Do not install and unregister a temporary flag handler around each check, restore raw signal dispositions behind the signal registry, or add an earlier default-emulation handler. Reason: the pinned signal-hook registry leaves its handler installed after unregistering the last action, so later termination signals can be ignored; retained registry entries also make raw restoration unsafe for the next registration. Actions run in registration order, so an earlier default-emulation action can terminate before hidden-input echo restoration. Persistent recording and normal cleanup avoid all three ordering traps.

  8. Round 2 — terminal input, dependent on 6–7. Keep terminal reading within Setup's production adapter. Poll stdin with a bounded 100 ms interval, checking interruption before waiting, after readiness and before returning an answer. Read bytes through the same descriptor without Rust stdin read-ahead; one byte at a time through newline is sufficient for these short answers and leaves pasted later lines available for the next question. Preserve partial-line EOF and convert UTF-8 before the existing trim. Handle interrupted waits by checking the request; report other descriptor/read errors. Reason: the current buffered line read can remain blocked under recorded signals, while polling behind the existing Outside interface adds no reader thread or new adapter and keeps buffered input from making the next question hang.

  9. Round 2 — hidden input, dependent on 8. Start signal recording before suppressing echo. Keep a guard owning the exact saved terminal attributes, restore them when it drops, and remove hidden input's separate handlers that restore echo then emulate fatal signals. Reason: the interruptible input operation now returns an error promptly, so ordinary unwinding through the guard restores echo on every ending and no fatal handler bypasses cleanup.

  10. Round 3 — interruption propagation, dependent on 5–9. Before ordinary retry/fallback in Claude's test-call catch, Codex/agy/Grok catalog catches and Muse/OpenCode check catches, consult the recorded interruption predicate and return interrupted when set. Do not match diagnostic prose. Preserve ordinary validation errors, repeated questions and catalog-failure fallback; pure catalog validation stays free of process state. Reason: otherwise cancellation becomes another refused answer, or Setup keeps the settings and continues. Existing OpenCode after-exit reading already propagates recorded interruption and retains that rule.

  11. Round 3 — persistence and completion, dependent on 10. Propagate cancellation out of the questions and check the request again before the first settings write, including a first-Run offer declined after signal recording begins. Cancellation during questions/checks is fatal for the offer, not a write/email warning. Return exit status 1 with interrupted; do not re-raise a termination signal after cleanup. Reason: the existing contract writes nothing before the final answer, and the Command must not proceed from a cancelled offer. A normal error return allows child and terminal cleanup to finish.

  12. Round 3 — ownership limits and scope, dependent on 4–11. Retain existing process-tree discovery, stop grace and escalation. Keep process-status success separate from protocol success; keep all session recovery, warnings, summaries, final text and once-only Resume behavior. Do not broaden this Ticket to child Runs, arbitrary GitHub/git lookups, tracking escaped descendants already reparented before discovery, or killing extra processes on ordinary successful completion. Reason: those have different lifetimes; this change concentrates the check lifetimes that the existing owning interface can actually guarantee.

  13. Round 3 — test surface, dependent on 4–12. Retain shared-process tests through its captured/streaming interface for concurrent I/O, blocked/broken stdin, worker errors and cleanup. Add new caller regressions through real thirdshift child processes and its terminal interface, using fake Harness executables as local adapters. Reason: the interface is the test surface; parsing-only tests cannot prove that a real check crosses the owning seam, and the process-wide signal flag cannot safely be reset by concurrent in-process tests.

  14. Round 3 — regression choices, dependent on 13. Cover all four migrated Command checks and all six Setup I/O catches, with both Setup entry paths represented for catalog reads and minimal calls. Use finite pipe-holder commands with a deadline comfortably below their natural expiry; record exact child PIDs for cleanup even on assertion failure. Verify a later prompt after a successful check and a refused/repeated check, hidden-input echo, pasted input and EOF. Reason: these tests exercise the lifecycle transitions most likely to regress and distinguish prompt termination from child-tree cleanup. Existing session and captured-check/export regressions remain useful across the reused seam.

  15. Round 3 — documentation and size, dependent on 1–14. Publish one standalone Ticket with no blocking edges. No glossary or ADR change is needed: Harness, Model, Effort, Setup, User config and Credentials retain their meanings, and the ownership/input changes are reversible implementation choices. Update implementation documentation where it describes fatal Setup signals or synchronous check I/O as part of this Ticket. Reason: one owner is reused rather than replaced, and there is no new domain concept or hard-to-reverse trade-off to record separately.

  16. Validation of the settled design. Reviewed Base branch main at b0edfbecfdcfdd9b6f60a359709dfef0c83065b2, equal to origin/main. A throwaway process-isolated probe measured SIGINT waiting 4.040 seconds for a finite Claude Model check. Routing it through the existing owner reduced that to 0.23 seconds. A throwaway production-terminal change using persistent recording, bounded input polling and echo restoration on drop stopped explicit/offered Setup checks in 0.27/0.29 seconds, wrote neither file, and preserved cancellation at the next question after a successful check. The three probe tests plus the existing Setup/offer tests passed, and 72 existing Harness-choice/session/process-tree tests passed with the four migrations. The probe and prototype were removed; 39 selected existing tests passed again on the restored head, the worktree is clean, and both diagrams passed Mermaid parsing before publication. Reason: this verifies the ownership and terminal decisions against actual child processes without publishing implementation work.

Architecture review

Strength: Strong · Dependencies: local-substitutable · Covered by: none among all 46 open issue titles and bodies, rechecked before publication.

Files: src/harness/process.rs, src/harness/claude.rs, src/harness/codex/catalog.rs, src/harness/agy/catalog.rs, src/harness/grok/catalog.rs, src/harness/codex.rs, src/harness/agy.rs, src/harness/grok.rs, src/harness/muse.rs, src/harness/opencode.rs, src/setup.rs, src/interrupt.rs, tests/process_tree.rs, tests/setup.rs, tests/setup_offer.rs, the existing fake Harness executables and terminal test support.

Before

flowchart LR
  R[Command preflight] --> C[Claude Model check]
  R --> L[Codex agy Grok catalogs]
  S[Setup] --> C
  S --> L
  S --> T[Terminal input]
  M[Muse OpenCode checks and export] --> P[Harness process module]
  C -. independent lifetime .-> X[Child processes]
  L -. independent lifetime .-> X
  P --> A[Harness adapters]
  A --> X
  I[Interruption] --> P
  classDef leak stroke:#dc2626,stroke-width:2px
  class C,L,T leak
Loading

After

flowchart LR
  R[Command preflight] --> C[Claude Model check]
  R --> L[Codex agy Grok catalogs]
  S[Setup] --> C
  S --> L
  S --> T[Terminal input]
  M[Muse OpenCode checks and export] --> P[Harness process module]
  C --> P
  L --> P
  P --> A[Harness adapters]
  A --> X[Child processes]
  I[Interruption] --> P
  I --> T
  classDef deep stroke-width:3px
  class P deep
Loading

Problem: shallow check-lifetime plumbing bypasses the owning seam (Claude, Codex, agy, Grok), so recorded interruption does not stop these checks promptly.

Solution: deepen the Harness process module's existing interface to absorb every check's lifetime implementation, with Setup input participating in recorded interruption and each adapter retaining its protocol behind the seam.

Wins

  • Depth: four more owned callers
  • Locality: one lifetime implementation
  • Leverage: existing interface reused
  • Tests cross the owning seam
  • Setup cancellation restores echo

Activity

  1. added
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    architect-planAn Architect plan: the Spec or Ticket an Architecture review published
    in-progressA Claim: thirdshift has taken this issue
    and removed
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    architect-planAn Architect plan: the Spec or Ticket an Architecture review published

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions