Orchestrata Ultimate › Plans › briefs › spec-110-s4-builder

SPEC-110 packet S4 — the CLI connects to the daemon it can start (REQ-110-140) + the docstring sweep sweeps docstrings (C6/C5): builder brief (orchestrator's picks, 2026-10-07)

Source: plans/briefs/spec-110-s4-builder.md at 8a2cb42ad · last changed 2026-10-07 · first committed 2026-10-07 · 622 lines.

Prose pending.

Plan text

SPEC-110 packet S4 — the CLI connects to the daemon it can start (REQ-110-140) + the docstring sweep sweeps docstrings (C6/C5): builder brief (orchestrator's picks, 2026-10-07)

Design authority: plans/contracts/spec-110-codemod-engine.contract.json entry D29 (committed daec60c25) for the daemon routing; specs/110-codemod-engine/spec.md REQ-110-010 verbatim for the sweep ("Vocabulary replacement in docstrings AND comments"). Plan: plans/spec-110-completion-plan.md § "S4 measured state" (S4-a…d, C5, C6) — read it first. Decision D4's condition (P4 merged) is met. A fork not settled below is a STOP-and-report; nothing here is a new decision.

1. Exact source at a793777cc

Read with .venv/bin/python scripts/serialize_codebase_ast.py source <qname> --no-daemon (qualified names omit orchestrata_ultimate.; the CLI module is codemod, e.g. codemod.main, codemod._cmd_run; test modules e.g. scripts.test_codemod_cli._run_codemod_cli, refactor.codemod.test_ipc_protocol.TestClientServerIntegration; --residue for module level).

CLI, scripts/codemod.py:

Daemon side:

Sweep (C6/C5), refactor/codemod/passes.py:

Tests that exist:

2. The change — part A, daemon routing (contract D29, REQ-110-140)

A1. Connection decision, scripts/codemod.py main() after parsing, before dispatch:

mode = "library"; client = None
if args.command in ("run", "analyze") and not args.no_daemon:
    repo = args.repo.resolve()
    if args.auto_start: (existing _auto_start_daemon call, unchanged semantics)
    socket_path = repo / ".codemod_daemon" / "socket"
    client = _connect_daemon(socket_path)        # CodemodClient().connect(...) or None
    if client is None: print(WARNING, file=sys.stderr) else: mode = "daemon"

_connect_daemon returns None on FileNotFoundError, ConnectionRefusedError, OSError (socket file present but no listener, or no file). The warning is exactly one stderr line: codemod: no daemon serving <repo> (socket <socket_path>); library mode, parsing on every invocation. Start it with --auto-start or systemctl --user start orchestrata-codemod-daemon@<instance>; --no-daemon selects library mode without this warning. where <instance> is CodemodDaemon()._unit_instance(repo) (read daemon.py:79-99 for the exact escaping; reuse it, do not re-implement). load-bench and report never connect and never warn. --no-daemon is silent. args.auto_start with --no-daemon keeps today's behaviour (no start, no connect). The client, when connected, is passed to _cmd_run(args, client) / _cmd_analyze(args, client) and closed in main's finally.

A2. Pass specs: split _resolve_pass(name, args) into _pass_spec(name, args) -> tuple[str, dict] (the pass CLASS name as passes exports it, e.g. "DocstringSweepPass", and the JSON-serializable constructor kwargs exactly as built today) and _resolve_pass(name, args) = getattr(passes, cls_name)(**kwargs). rename-symbol keeps its exit-2 message. Unknown names keep the existing message and exit 2.

A3. Server: pending runs by reference (server.py):

A4. Client (client.py): run_pass(pass_name, params, run_id=None, observe=False) -> dict (the server's dict; the hollow ChangeSet return is replaced — update its two existing tests), commit(run_id, *, dry_run=True, baseline="delta", observe=False) -> CommitReport built from result["report"] via a CommitReport.from_json/from_dict (add it next to to_json if absent; round-trip must be exact for success, files_written, findings, duration_seconds, dry_run, diffstat, legacy_findings), resolve_nodes(node_ids) -> list[dict], query_cycles(), query_topological_order(), query_centrality(top_k, exact), query_impact_score(node_id), query_coupling_score(node_id), query_abstraction_level(node_id) (whatever _cmd_analyze needs that the client lacks; the server already serves them).

A5. CLI in daemon mode:

A6. Proof tests (new module tests/scripts/test_codemod_cli_daemon.py; RUL-021 file budget): real CodemodServer on a tmp repo socket (the TestClientServerIntegration pattern; no systemd, no deployment; fixture_package/_analysis_repo-style fixture with ≥ 3 modules, one import cycle, one docstring containing OldWord), in-process codemod.main([...]) with --repo <tmp> and capsys:

@traces: the new CLI functions cite REQ-110-140; server/client additions cite REQ-110-138 (the method list) and REQ-110-139; CodemodWorkspace.restore cites REQ-110-137. Checklist: specs/110-codemod-engine/checklists/requirements.md:124 row REQ-110-140 flips from [ ] to [x] ONLY after the proof tests pass, and the section total at :160 ("7 COMPLETE, 1 NOT IMPLEMENTED") is updated with it. Docs: a new docs/codemod-daemon.md (sibling of docs/codemod-analysis.md, codemod-observability.md, codemod-review-engine.md; no daemon doc exists today — docs/development-and-testing.md mentions --auto-start only in passing): the connection decision, the warning, by-reference runs, the forest restore, the open _PASS_QUERIES item, and C6's docstring-only scope.

3. The change — part B, the sweep sweeps docstrings (C6, C5)

B1. C6. In _sweep_docstrings_and_comments, the string branch applies ONLY to docstrings: before the walk, collect docstring_nodes = {id(body[0].value) for owner in ast.walk(module.tree) if isinstance(owner, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and owner.body and isinstance(owner.body[0], ast.Expr) and isinstance(owner.body[0].value, ast.Constant) and isinstance(owner.body[0].value.value, str)} (this is ast.get_docstring's definition; bytes constants are never docstrings); in the walk continue unless id(node) in docstring_nodes. The comment branch is unchanged. Update the function's and the class's docstrings ("string literals" → "docstrings").

B2. C5a — keep the prefix. _parse_string_token returns (prefix, quote, content); the edit writes prefix + quote + new + quote. A raw docstring stays raw; a u prefix stays. (f-strings never reach here; bytes are excluded by B1.)

B3. C5b — the implicit-concatenation span. With B1 a docstring that is an implicit concatenation ("""a""" "b") is still skipped by _is_simple_string_literal (two STRING tokens); a test pins that the file is returned unchanged (None) and no byte of it moves. No span code changes; if the builder finds the passes.py:779 span defect reachable for a docstring shape, STOP and report the shape.

B4. Tests (tests/refactor/codemod/test_transform_passes.py, or a new test_docstring_sweep_scope.py if the module is near the RUL-021 budget):

4. Gates and done-when

5. Fix round 1 (reviewer NO-GO on 293afd07b..3a29ee3d0, 2026-10-07; commits on top of 3a29ee3d0, no rebase or amend)

Reviewer evidence: scratchpad probe_lock.py, probe_forest.py, probe_cli.py, probe_refuse.py, probe_sweep.py with .out files; s4_rev_tests.log (116 passed), s4_rev_cli.log (57 passed). Everything not listed here was CONFIRMED correct (lock ownership, restore after 1- and 2-pass dry runs with the pre-run originals, commit by reference writes, review engine with the requested baseline refusing identically in both modes, the warning on every socket-path shape, the ten sweep edge cases, hygiene). Each item below is an operational refinement of contract D29, recorded by the orchestrator in the plan; none is a new decision.

F1 — BLOCKER: daemon mode ignores --include. main() (scripts/codemod.py:~1075) picks daemon mode whenever a socket answers and _run_via_daemon (:~458) never sends args.include; the deployed daemon loads ("src", "scripts", "tests") (_daemon_entry.py:main). Probe: with a daemon serving, codemod --include src run --pass docstring-sweep … --apply wrote 3 files (tests/test_t.py, scripts/s.py too) where library mode wrote 1. Fix: status already runs on connect — add "include": list(ws.include) to the server's status result (read how the workspace records its roots: CodemodWorkspace.load(include=…) — if the roots are not stored on the instance, store them as self.include at load, one attribute, no other change); in _connect_or_warn, after connecting, compare args.include (when given) as a sorted tuple with the daemon's include; on mismatch close the client and run library mode with exactly one stderr line: codemod: daemon serves <daemon roots> but --include asks for <cli roots>; library mode for this run. (no --auto-start hint). When --include is not given, the daemon's roots are the roots (the CLI default and the daemon default are both src, scripts, tests). Tests: mismatched roots → library mode, the notice, files outside --include untouched, pending_runs 0; matching explicit roots → daemon mode, no notice; both-modes equality tests gain a non-default --include src case whose server was loaded with ("src",) AND a default-roots case whose server was loaded with all three.

F2 — MAJOR: the inotify reload swaps the workspace without the write lock. _daemon_entry._on_file_change assigns server._workspace = new_ws (REQ-110-137 says reload takes the exclusive lock). Reviewer probe: a reload landing between pass 1 and pass 2 makes pass 2 run on the fresh disk tree (disk ends at NewWord, library gives ThirdWord); a reload during a dry run followed by commit's restore writes stale trees into the new forest and every later --apply on that file fails ConcurrentEditError. Fix: CodemodServer.replace_workspace(new_ws: CodemodWorkspace) -> None acquires _write_lock (so it waits for a pending run to commit or its connection to close), swaps, releases; _on_file_change calls it instead of assigning. The load itself stays outside the lock (it is seconds long). Tests (in-process server, real thread): replace_workspace blocks while a run is pending and completes after the commit; a swap between two run_pass calls of one run cannot happen (the second pass still sees the first's output); _daemon_entry has no direct test harness — if its _on_file_change is not reachable in a test without systemd, cover replace_workspace and keep the entry change a one-line call (say so).

F3 — pending runs get a lease. _handle_client's recv timeout continues, so an idle connected client holds the write lock forever and every other writer blocks with no error (reviewer: B blocked until A committed). Fix: _PendingRun.touched = time.monotonic() refreshed on each run_pass/commit of that run; a server-side lease_seconds (default 120.0, constructor parameter) — handle_request for a WRITE method from ANOTHER connection that finds the lock held by a pending run older than its lease expires that run (restore originals, drop, release lock) and proceeds; the owner's later commit → -32602 Run expired after <lease>s idle. Also: the blocked writer's acquire() is bounded: acquire(timeout=lease_seconds + 5); on timeout → -32000 Write lock held for more than <n>s. Tests: lease 0.5 s, A run_pass, idle 0.7 s, B run_pass succeeds and A's commit is refused with the expired message and the forest is A's pre-run state. Docs: replace "a crashed CLI cannot wedge the daemon" with the lease rule.

F4 — commit failures keep their cause in daemon mode. _dispatch's catch-all turns ConcurrentEditError and every commit exception into -32603 Internal error; the CLI dies with an uncaught CodemodRpcError traceback. Fix: in _commit, catch Exception from ws.commit, restore (already), release, and return -32000 with f"{type(exc).__name__}: {exc}" as the message; the CLI catches CodemodRpcError in _cmd_run and prints Error: <message> to stderr, exit 1 — the same line library mode prints for the same exception (make library mode print it through the same function; today it propagates). Test: an external disk edit between run_pass and commit --apply → identical stderr line and rc 1 in both modes, forest restored.

F5 — --auto-start waits for the socket. CodemodDaemon.start returns on MainPID; the entry loads the workspace before serve() binds, so the first --auto-start run connects before the socket exists and falls back with the misleading warning. Fix: in _auto_start_daemon, after start, poll daemon.status()["socket_exists"] every 0.2 s up to CODEMOD_AUTO_START_WAIT seconds (env seam, default 300, REQ-110-133's 120-s parse plus margin); on timeout return the text daemon <instance> started but not serving after <n>s (printed, exit 1 as today's failures are). Test with the existing test_auto_start_daemon_starts_the_unit_once_then_finds_it_running pattern (a transient unit whose ExecStart creates the socket file after 1 s): the CLI connects after the wait and prints no warning; a unit that never creates it → the timeout text (use a 1-s CODEMOD_AUTO_START_WAIT).

F6 — test gaps. (a) a two-pass CLI run whose passes touch the SAME file (docstring-sweep old=OldWord new=NewWord then docstring-sweep old=NewWord new=ThirdWord) ends at ThirdWord in both modes; (b) a second writer blocks until the first commits (two clients, timing asserted: B's run_pass returns only after A's commit); (c) F1 and F2 tests above.

F7 — C7, the replace pass keeps the string prefix (plan § "S4 measured state", C7): _replace_string_literals (passes.py:~181) unpacks _prefix, quote, raw_value and still writes quote + new + quote. Fix: write prefix + quote + new + quote; when the prefix contains b/B and the new value is not pure ASCII (new.isascii() False), leave the literal unchanged. Tests: b"x", r"x", u"x", rb"x", Rb"x" keep their prefix bytes after replace-string old=x new=y; b"x" with new=é is unchanged and the file is otherwise rewritten.

F8 — a replacement carrying the literal's own quote. Reviewer probe (case l): a sweep whose new value contains the docstring's quote character produces invalid source and the CLI dies with SystemError … tokenize; the old code did the same. Fix in BOTH _sweep_docstrings_and_comments and _replace_string_literals: if the new content contains the delimiter character (quote[0] unescaped — for a triple-quoted literal, three consecutive delimiter characters) or ends with an odd run of backslashes, leave that literal unchanged. Test: old=OldWord new='Old"Word' on a """OldWord""" docstring (unchanged, file valid, CLI rc 0 with Files mutated: 0) and on a 'OldWord' literal via replace-string (unchanged); a replacement with a quote that is NOT the delimiter (' inside "…") is applied.

F9 — docs and checklist honesty. docs/codemod-daemon.md: remove "run and analyze print the same bytes whichever mode serves them" in favour of "with matching roots" + the F1 notice; add the lease rule, the reload lock, the F4 error path, and the REQ-110-137 read note (reads use the CIG and stats(), never the mutated forest, so a pending run cannot leak through a query; the inotify reload now waits for pending runs). Checklist row REQ-110-140 (checklists/requirements.md:124) reads [x] with the qualifier sentence: "connects whenever a daemon serves the repo with the requested roots; breaking_changes, complexity, test_coverage, dead_code, all-passes parse locally with a notice (S4 backlog); a root mismatch runs locally with a notice". The tests/scripts/test_codemod_cli.py header's stale residual lines ("cycles can never materialize", _resolve_pass fallthrough) are deleted.

Gates and commit rules as § 4; groups as § 4 plus tests/refactor/codemod/test_file_watching.py and test_watcher_coverage.py in group 3. Report the exact strings each new refusal/notice produced and the timing of the F6(b) and F3 tests.

6. Fix round 2 (the round-1 builder's own "left alone" list, 2026-10-07; commits on top of 6d2fea688)

Round 1 (162145987, 2cfa5c2c5, 779766945, 6d2fea688) closed F1–F9; the F8 rule as implemented (three consecutive delimiter characters for a triple-quoted literal; the single-character delimiter for a single-quoted one) is the correct one and § 5's Old"Word example was wrong — keep the rule and its tests. The builder listed four gaps it did not touch; RUL-024 says a diagnosis is not a disposition, so they are closed here:

G1 — reload RPC hardcodes the roots. CodemodServer._dispatch branch method == "reload" calls CodemodWorkspace.load(ws.root, include=("src", "scripts", "tests"), parallel=True), so a daemon loaded with narrower roots widens on reload, after which the F1 roots comparison lies. Fix: reload with include=tuple(ws.include_roots) (an empty tuple means the whole repo and is passed through as the workspace stores it) and the same cache_dir the daemon used if the workspace records one (read CodemodWorkspace.load for where cache_dir lives; if it is not stored, add self.cache_dir at load — one attribute). Test: a server loaded with ("src",) reloads and status()["include"] still reads ["src"]; a file under tests/ added between the two is not in the forest.

G2 — a raw newline in the new content of a single-quoted literal. The F8 guard checks the delimiter and a trailing backslash run; new="a\nb" written into '…' is invalid source. Fix: the shared guard also refuses (leaves the literal unchanged) when the new content contains \n or \r and the literal is not triple-quoted. Tests in both passes.

G3 — connect between the server's bind and listen. A client connecting in that window gets ECONNREFUSED on an existing socket file and the CLI falls back to library mode with the warning. Fix in _connect_or_warn: when the socket FILE exists and connect raises ConnectionRefusedError, retry 3 times at 50 ms before giving up (a stale socket still ends in the warning, 150 ms later). Test: a listener that binds, sleeps 60 ms, then listens — the CLI connects (daemon mode, no warning); a stale socket file still produces exactly one warning line.

G4 — pyright in tests/scripts/test_codemod_cli.py:220, 225. Two _replacements attribute accesses on a TransformPass-typed value. Fix mechanically: narrow with assert isinstance(instance, DocstringSweepPass) before the access (no # type: ignore, no getattr). uv run pyright tests/scripts/test_codemod_cli.py reports 0 errors afterwards.

Gates and commit rules as § 4; run groups g1 and g2 (G1–G4 touch only those) and g4 for the G2 pass tests. Report shas, counts with logs, and the G3 timing.

7. Fix round 3 (confirmation review GO with six MINORs on 3a29ee3d0..3d96149d6, 2026-10-07; commits on top of 3d96149d6)

Reviewer probes: scratchpad rev2_cli.py/.out, rev2_server.py/.out, rev2_stale.py; g1 124 / g2 115 passed. All six prior findings are closed. These six are closed before the ship:

H1 — a reload can install a workspace older than a commit. _on_file_change loads outside the lock (correct) and replace_workspace then waits for a pending run; if that run's commit WROTE disk during the load, the swap installs a pre-commit tree (probe rev2_stale.py: disk NewWord, forest OldWord; a dry run reports file_count 0 wrongly; an apply is refused ConcurrentEditError; heals on the next inotify event). Fix: CodemodServer.commit_generation (int, incremented under _write_lock after every ws.commit that wrote, i.e. not dry_run and report.success); replace_workspace(new_ws, loaded_at_generation: int | None = None) drops the swap (returns False, logs one line) when loaded_at_generation is not None and loaded_at_generation != self.commit_generation; _on_file_change reads the generation before load and passes it; a dropped swap is fine because the commit's own writes queue another inotify event. Fix the test test_a_swap_cannot_land_between_two_passes_of_one_run so it asserts forest == disk after the apply (not server._workspace is fresh), and add test_a_reload_loaded_before_a_commit_is_dropped.

H2 — expiry can end a run whose owner went busy in the gap. _expire_stale_runs builds the stale list under _pending_lock, releases, then _end_run(expire=True) pops without re-checking (probe with a 0.4-s sleep in _end_run: A's commit succeeded while B's expiry restored A's trees and released A's lock — mutual exclusion broken). Fix: in _end_run(expire=True) re-check run is self._pending.get(run_id) and not run.busy and now - run.touched > lease UNDER _pending_lock before popping; if the check fails, do nothing. Test: a subclass test hook is not allowed (no doubles) — prove it with the real race shape: lease 0.2 s, A idle 0.3 s, then A's commit and B's run_pass started from two threads at the same instant 20 times; after each pair either A committed and B ran AFTER (disk == forest, one restore at most), or A got Run expired and B ran — never both A success and B concurrent (assert pending_runs == 0, forest == disk, no ConcurrentEditError).

H3 — invalid escape sequences and NUL still produce invalid source. replace-string new=\N, \x, \u12 into 'a' → uncaught SyntaxError; a sweep turning """C:\OldWord""" into \N…; regex new=\0 → NUL → SystemError: tokenize. Fix: the shared guard (_fits_between_quotes) finally validates the candidate literal with ast.literal_eval(prefix + quote + content + quote) inside try/except (SyntaxError, ValueError) and refuses on failure (keeps the explicit delimiter/newline/backslash checks as fast paths). Tests: the five probe cases above are left unchanged (file unchanged, CLI rc 0, Files mutated: 0 when nothing else matched) in both passes; a legitimate \\n and \t are applied. Daemon and library agree (the daemon no longer sees these as -32603).

H4 — flake margin. test_a_connect_between_the_servers_bind_and_listen_succeeds has ~90 ms slack (0.06-s gap vs 0.15-s retry budget). Fix: gate the listener's listen() on the first refused connect instead of a fixed sleep (bind; wait on a threading.Event the test sets after its first refused attempt is observed via a probe socket, or simply listen() 0.02 s after bind) so the margin is ≥ 120 ms; state the margin in the test docstring.

H5 — --auto-start keeps waiting on a dead unit. The wait polls only socket_exists. Fix: in the wait loop also read daemon.status(); break with the text daemon <instance> failed before serving (<active_state>) when running is False after the first 1 s or active_state is failed; a leftover socket file from a killed daemon (socket exists, running False) also takes that branch instead of ending the wait "successfully". Test with the transient-unit pattern of the existing auto-start tests: a unit whose ExecStart exits 1 → the failed text within 2 s, exit 1.

H6 — docs. docs/codemod-daemon.md: the lease paragraph states the re-check (H2); the sweep paragraph states the guard validates the literal (H3) and the newline rule (G2); add the reload-generation rule (H1); add the sentence "without --include, the daemon's roots are the roots — a daemon loaded narrower than the three defaults is used as-is" (reviewer finding 6).

Gates and commit rules as § 4; run g1, g2, g3 (watcher modules included), g4. Report shas, counts with logs, the 20-pair outcome table of H2, and the H3 cases' before/after.

8. Fix round 4 (the round-3 builder's deviation 1, 2026-10-07; commits on top of dff2c63f6; wirer scope)

GOAL: CodemodDaemon.start() reports a launcher that dies before it is observed as a failed start at once, with the same text shape _auto_start_daemon already uses, instead of waiting out MAINPID_TIMEOUT (10 s) and raising … reported no MainPID within 10s (ActiveState=activating).

Measured defect (round 3, H5 under load): start() runs systemctl --user start <instance>, then _wait_for_main_pid polls only MainPID. The unit is Type=simple, Restart=on-failure, RestartSec=15. A launcher that exits before the first show leaves MainPID=0 with the unit in activating (auto-restart) for 15 s (exit ≠ 0), inactive (dead) (exit 0) or failed (start limit hit). The loop waits the full 10 s and raises the vague text; _auto_start_daemon returns that text verbatim, so codemod --auto-start prints a 10-s-late message that does not match the documented daemon <instance> failed before serving (<state>), and test_auto_start_daemon_stops_waiting_when_the_unit_dies_before_serving is a race (passes only when the 0.5-s launcher is observed alive).

FILES: src/orchestrata_ultimate/refactor/codemod/daemon.py (_wait_for_main_pid only), tests/refactor/codemod/test_daemon_lifecycle.py (one new test in TestStart), docs/codemod-daemon.md (one sentence in the auto-start paragraph).

The change (_wait_for_main_pid):

§8 amendment (measured by the wirer, 2026-10-07): the start tests live in TestDaemonStart (not TestStart). And the dying launcher lands in a SECOND window: right after systemctl start returns, the first show still reports the dead launcher's non-zero MainPID (systemd has not reaped it yet), so the rule above returned the pid and start() recorded a dead process as a success (10 of 10 runs). Resolution — start() settles before it reports success, the same shape as the promoter's P6b verify hold: after _wait_for_main_pid returns a pid, a new _settle_after_start(instance, pid) polls ActiveState/SubState every 0.1 s for START_SETTLE_SECONDS = 1.0 (module constant) and raises RuntimeError(f"daemon {instance} failed before serving ({ActiveState})") the moment ActiveState is not active or SubState is auto-restart; a unit still active after the settle is a started daemon and start() continues to _write_pid. The settle adds at most 1 s to every start, which _auto_start_daemon's 300-s socket wait absorbs. The zero-MainPID branch in _wait_for_main_pid stays as written (it covers the window the round-3 builder measured). FILES gain nothing; the settle lives in daemon.py beside _wait_for_main_pid. Second amendment (wirer measurement): with the settle, _auto_start_daemon returns the dead-unit text at ~0.8 s, so test_auto_start_daemon_stops_waiting_when_the_unit_dies_before_serving's lower bound 1.0 <= elapsed (which encoded the CLI's own 1-s grace) is stale and fails 5/5 in isolation. FILES gain tests/scripts/test_codemod_cli.py for that one assertion: assert elapsed < 5.0 with the comment rewritten (the wait ends as soon as start() or the status poll sees the unit leave active; the bound leaves room for load). The six-line show parser is shared: _unit_show(instance, *props) -> dict[str, str] in daemon.py, used by _wait_for_main_pid and _settle_after_start.

Test (TestDaemonStart.test_start_reports_a_launcher_that_dies_before_serving, uses the live fixture): overwrite deployed_prefix / "bin" / "codemod-daemon" with the dying launcher "#!/usr/bin/env bash\nexit 1\n" (mode 0o755) BEFORE daemon.start(daemon_repo); assert pytest.raises(RuntimeError, match=r"^daemon .* failed before serving \("), elapsed < 3.0 s, unit.active_state(instance) in {"activating", "failed", "inactive"}, and no PID file written. Run it 10× in a loop (for i in $(seq 10)) and report 10/10.

CONSTRAINTS: §4 rules (no doubles, no skip, no monkeypatch, uv only, Mohamed is the sole author, no "should", no -> in commit messages, @traces(spec="110", story="US-110-013", reqs=["REQ-110-135"]) on the new test). Change nothing else in daemon.py; status(), _refuse_if_running, stop() are out of scope. Do not touch scripts/codemod.py. DESIGN_WAIVER: RUL-022 — this round executes contract D29 refinement H5 (same failure text on the library path the CLI already documents); no new design surface.

DONE WHEN: the new test passes 10/10; tests/refactor/codemod/test_daemon_lifecycle.py, tests/refactor/codemod/test_daemon_coverage.py and tests/scripts/test_codemod_cli.py -k auto_start are green (report counts from the logs); H5's test test_auto_start_daemon_stops_waiting_when_the_unit_dies_before_serving passes 5/5 in a loop; uv run ruff check and uv run black --check clean on the two files; daemon.py coverage ≥ 90 % (--cov=orchestrata_ultimate.refactor.codemod.daemon).

VERIFY: uv run pytest tests/refactor/codemod/test_daemon_lifecycle.py -p no:cacheprovider -q (background, log to the scratchpad, wait on rc=); the 10× loop on the new test by node id; uv run pytest tests/scripts/test_codemod_cli.py -k auto_start -q 5×; the coverage run above. Commit as Mohamed, message SPEC-110 S4 fix round 4: start() reports a launcher that dies before serving at once. Report the sha, the 10/10 and 5/5 loop outputs, and the before/after text of the failure message.