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:
_build_parser(:26-…): global flags--repo(default$CODEMOD_ROOTor.),--include(repeatable; defaultsrc, scripts, tests),--parallel,--auto-start("Auto-connect to daemon or start it if not running"),--no-daemon("Force library mode (parse on every invocation)"); subcommandsload-bench,run(--passrepeatable,--arg key=valuerepeatable,--apply,--dry-run,--observe,--baseline,--output),report,analyze(--query,--module,--top-k,--exact, …).main(:791-813): parses;if args.auto_start and not args.no_daemon: failure = _auto_start_daemon(args.repo.resolve()); if failure is not None: print(failure, file=sys.stderr); return 1; then dispatches to_cmd_load_bench/_cmd_run/_cmd_report/_cmd_analyze._auto_start_daemon(repo_root, *, tools_prefix=None, unit_name=None) -> str | None(:201-235): buildsCodemodDaemon(tools_prefix=…, unit_name=…)fromCODEMOD_TOOLS_PREFIX/CODEMOD_DAEMON_UNIT(defaults/opt/orchestrata-tools,orchestrata-codemod-daemon), setsdaemon._repo_root = repo_root,if not daemon.status()["running"]: daemon.start(repo_root); returns theRuntimeErrortext orNone._resolve_pass(name, args) -> TransformPass(:244-305): kebab registryreplace-string,add-import,remove-unused-import,docstring-sweep,rename-symbol→ classes; builds constructor kwargs per name (replace-string:old, new, regex, keep_patterns;add-import:module, name, alias;remove-unused-import: none;docstring-sweep:replacementsfromold_<k>/new_<k>pairs orold/new);rename-symbolexits 2 ("requires a WorkspaceEditPlan")._cmd_run(:344-414):CodemodWorkspace.load(root, include=include, parallel=args.parallel);pass_instances;observers = default_observers() if args.observe else None;changes = ws.run(pass_instances, observers=observers);correlation_id = f"codemod-{time.strftime(...)}- {uuid4().hex[:8]}";report = ws.commit(changes, dry_run=not args.apply, observers=observers, review_engine=InMemoryReviewEngine(baseline=args.baseline)); closes observers infinally; printsreport.diffstat,Passes run: …,Files mutated: {changes.file_count}(dry) orFiles written: {len(report.files_written)},Findings: {errors} errors, {warnings} warnings,Duration: {report.duration_seconds:.1f}s; writespayload = json.loads(report.to_json()); payload["baseline"] = args.baselinetoargs.outputorroot/reports/{correlation_id}.json; printsCommit report: {path}; exit 1 ifnot report.success or (errors and not dry_run)._cmd_analyze(:696-788): loads the workspace;_PASS_QUERIES = ("breaking_changes", "complexity", "test_coverage", "dead_code")plus"all-passes"→_cmd_analyze_passes;_ROUTED_QUERIES = ("blast_radius", "coupling_score", "abstraction_level")→_cmd_analyze_routed; the rest render fromws.cig:dependencies/dependents(printcig.nodes[d].qualified_name),cycles(' -> '.join(names)),topological_order(first 20 names),centrality(cig.centrality(top_k, exact)pairs → names),impact_score(cig.pagerank_score)._resolve_cig_node(cig, module) -> int | None(:461-471)._cmd_load_bench(:319-341) loads twice (serial, parallel) and measures — library by nature._cmd_report(:417-458) reads a JSON file — no workspace.
Daemon side:
refactor.codemod.serverresidue:READ_METHODS(:26-41:query_dependencies,query_dependents,query_blast_radius,query_breaking_changes,query_cycles,query_topological_order,query_affected_tests,query_coupling_score,query_abstraction_level,query_centrality,query_impact_score,status),WRITE_METHODS(:43-49:run_pass,commit,reload).CodemodServer.__init__(:77-93):_write_lock = threading.Lock(),_client_lock,_client_count.handle_request(:147-177): unknown method → -32601; WRITE methods runwith self._write_lock: return self._dispatch(...); READ methods dispatch unlocked._handle_client(conn)(:209-257): newline-delimited JSON loop per connection,finallydecrements_client_countand closes._dispatch(:267-449):run_pass→_resolve_pass(pass_name)(class NAME lookup inpasses,:461-470) →pass_cls(**pass_params)→ws.run([pass_instance])→{"file_count", "paths"};commit→ readschanges_data = params.get("changes"),dry_run, thenws.commit(ChangeSet(entries=()), dry_run=dry_run)(S4-b) →{"success", "files_written", "findings": [{severity, rule, path, line, message}], "duration_seconds", "dry_run"};reload→CodemodWorkspace.load(ws.root, include=("src","scripts","tests"), parallel=True);status.refactor.codemod.client.CodemodClient(client.py:26-…):connect(socket_path) -> CodemodClient,close, context manager,query_dependencies(node_id) -> list[int],query_dependents,query_blast_radius(node_id, hops=3) -> dict,run_pass(pass_name, params) -> ChangeSet(hollow),commit(changes, reviews=None, dry_run=True) -> CommitReport,status,reload,_call(method, params),_recv_response(expected_id).refactor.codemod.workspace.CodemodWorkspace.run(passes, observers=None) -> ChangeSet(:560-614): for each passresult = pass_.apply(self),composed = composed.merge(result),self._forest[entry.path] = entry.mutated_treefor every entry (S4-c).commit(changes, reviews=None, dry_run=True, observers=None, review_engine=None, run_review_engine=True) -> CommitReport(:631-…): reviews the mutated forest, writes atomically unless dry_run; restores nothing.load(root, include=…, parallel=False, cache_dir=None, observers=None)(:303).report.CommitReporthasto_json();report.ChangeSet(entries)with.paths,.file_count,.merge;report.ChangeEntry(path, original_tree, mutated_tree, pass_name, description).refactor.codemod.daemonresidue:DAEMON_DIR = ".codemod_daemon",SOCKET_NAME = "socket";CodemodDaemon.status()(:333-401) returnsrunning,pid,socket_path,socket_exists,unit,active_state,identity.
Sweep (C6/C5), refactor/codemod/passes.py:
_sweep_docstrings_and_comments(module, replacements) -> LosslessModule | None(:729-779):for node in ast.walk(module.tree): everyast.Constantwithstr/bytesvalue →span = module.index.span_for(node)(skip onNodeSpanError) → skip unless_is_simple_string_literal(module.stream, span)→text = module.stream.slice(span.start, span.end);quote, raw_value = _parse_string_token(text); apply replacements;edits.add(span.start, span.end, quote + new_value.encode("utf-8") + quote)(the string PREFIX is dropped: C5a); then everyTokenKind.COMMENTtoken is rewritten._is_simple_string_literal(stream, span)(:121-146): true only when exactly one STRING token starts inside the span._parse_string_token(text)(:94-118):prefix_len = _STRING_PREFIX_RE.match(text).end(), strips the prefix, returns(quote, content).DocstringSweepPass(:782-836,@traces(spec="110", story="US-110-002", reqs=["REQ-110-010"])):apply(ws)overws.forest,apply_file(path, tree, ws)→_sweep_docstrings_and_comments,ChangeEntry(... description=f"Swept {len(replacements)} vocabulary replacements").- Measured (S3 reviewer): over
src/orchestrata_ultimate/toolsa{X: Y}sweep changed 23 lines, 12 of them not docstrings and behaviour-changing (remote_trigger_tool.py,schedule_cron_tool.py:1245).
Tests that exist:
tests/scripts/test_codemod_cli.py:_run_codemod_cli(args, prefix)(:1117-1128, subprocesssys.executable scripts/codemod.py …withCODEMOD_TOOLS_PREFIX/CODEMOD_DAEMON_UNITset),_analyze(repo, query, *extra, include=…)(:1298-1304, in-processcodemod.main([...])),_analysis_repo(tmp_path),_pkg_repo,_big_repo,_write_repo; auto-start tests at:1131-1260(test_no_daemon_overrides_auto_start, …).tests/refactor/codemod/test_ipc_protocol.py::TestClientServerIntegration(:221-381): the real-socket pattern —ws = CodemodWorkspace.load(repo_root, include=("src",));server = CodemodServer();threading.Thread(target=lambda: server.serve(socket_path, ws), daemon=True).start();time.sleep(0.5);CodemodClient().connect(socket_path);finally: server.stop(); t.join(timeout=3.0). Fixturessocket_path,fixture_package(conftest).tests/refactor/codemod/test_transform_passes.py(DocstringSweepPass tests),tests/refactor/codemod/test_review_engine_commit_wiring.py,test_observability_wiring.py.
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):
self._pending: dict[str, _PendingRun]where_PendingRunis a small dataclass(changes: ChangeSet, originals: dict[str, LosslessModule], conn_id: int);conn_idis a per-connection integer the handler assigns (itertools.count), threaded intohandle_requestand_dispatchas a keyword (defaultNonefor the existing directhandle_requestcallers in tests).run_pass(pass_name, params, run_id=None, observe=False):run_id = run_id or uuid4().hex; if the run_id is new,self._write_lock.acquire()is taken by this run (andhandle_requestmust NOT also take it for arun_pass/committhat names an existing pending run_id owned by thisconn_id— restructure:handle_requesttakes the lock for WRITE methods only when the request does not name a pending run; a pending run holds the lock from its firstrun_passto itscommitor its connection's close); before mutating, recordoriginals[path] = ws.forest[path]for every path not yet recorded under this run_id (snapshot of the whole forest keys is NOT needed:ws.runreports the touched paths inresult.entries; take the originals AFTER the run fromentry.original_treefor paths not yet inoriginals);result = ws.run([pass_instance], observers=default_observers() if observe else None);pending.changes = pending.changes.merge(result); return{"run_id", "file_count": pending.changes.file_count, "paths": list(pending.changes.paths)}.commit(changes, reviews=None, dry_run=True, baseline="delta", observe=False):changesmust be a dict carrying"run_id"that names a pending run of THIS connection, else-32602 "Unknown or foreign run_id";report = ws.commit(pending.changes, dry_run=dry_run, observers=default_observers() if observe else None, review_engine=InMemoryReviewEngine( baseline=baseline)); observers closed (close()where present) infinally; whendry_run, restorews._forest[path] = originalfor everyoriginalsentry (a dedicatedCodemodWorkspace.restore(originals: Mapping[str, LosslessModule]) -> Nonemethod, so the server does not touch the private dict); drop the pending run; release the write lock. Result: the existing keys plus"report": json.loads(report.to_json())and"baseline": baseline.- Connection close (
_handle_client'sfinally): every pending run owned byconn_idis restored and dropped and its lock released. A pending run also expires if its connection's 300-s recv timeout fires with the socket closed (same path). resolve_nodes(node_ids: list[int]) -> list[{"id": int, "qualified_name": str, "path": str}]added toREAD_METHODS; unknown ids are omitted (the CLI already skipsNonenodes).statusgains"pending_runs": len(self._pending).
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:
_cmd_run(args, client):run_id = None; for name in args.passes: cls_name, kwargs = _pass_spec(name, pass_args); r = client.run_pass(cls_name, kwargs, run_id=run_id, observe=args.observe); run_id = r["run_id"];report = client.commit(run_id, dry_run=not args.apply, baseline=args.baseline, observe=args.observe); the printed lines, the report file (payloadfromreport.to_json()+baseline) and the exit code are produced by ONE shared function used by both modes so they cannot drift.Files mutated:usesr["file_count"]of the lastrun_pass._cmd_analyze(args, client): every query renders from RPC results +client.resolve_nodes; the rendering functions are shared with library mode (take(ids → names)callables, not a CIG).--moduleresolution in daemon mode:resolvehappens server-side inside each query (ws.cig.resolve(node_id)already accepts the module key) — for an unknown module the server returns an error; the CLI prints the SAMEError: module '<m>' not found in CIGand exits 1._PASS_QUERIES(breaking_changes,complexity,test_coverage,dead_code,all-passes) run analysis passes over the workspace: in daemon mode they go throughquery_breaking_changes/… only where the server has a method;complexity,test_coverage,dead_code,all-passeshave no RPC today — they stay library mode WITHOUT the warning when a daemon is connected (print one stderr linecodemod: '<query>' has no daemon method; parsed locally) — this is the one place D29 leaves open; record it in the report as S4 backlog, do not add RPC methods for them.
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:
test_every_analyze_query_prints_the_same_text_in_both_modes: parametrized overdependencies,dependents,cycles,topological_order,centrality,impact_score,blast_radius,coupling_score,abstraction_level(+--modulewhere needed): stdout in library mode (--no-daemon) == stdout in daemon mode (server running), stderr empty in both.test_run_dry_run_prints_the_same_text_and_report_in_both_modesandtest_run_apply_writes_the_same_files_in_both_modes:run --pass docstring-sweep --arg old=OldWord --arg new=NewWord(dry, then--applyon a fresh copy each); stdout equal after maskingDuration:andCommit report:lines; report payloads equal minusduration_seconds/correlation fields; files on disk byte-identical.test_a_dry_run_through_the_daemon_leaves_the_forest_unchanged: after the daemon-mode dry run a second client'squery_dependenciesand astatus()["file_count"]are unchanged AND a directserver._workspace.forest[path]is the original object;pending_runs == 0.test_a_disconnect_with_a_pending_run_restores_the_forest: raw socket,run_pass, close without commit → forest restored,pending_runs == 0, next client'srun_passacquires the lock (completes within 5 s).test_a_foreign_run_id_is_refused: client B commits client A's run_id → -32602.test_the_library_mode_warning_is_exact_and_once(no server; stderr == the one line),test_no_daemon_is_silent,test_load_bench_and_report_never_connect(a socket path that is a directory would raise on connect; neither command touches it; stderr empty).test_auto_start_then_connectstays out of scope (needs the deployed unit; the existing auto-start tests cover--auto-startitself).- Server/client unit tests in
tests/refactor/codemod/test_ipc_protocol.py:resolve_nodes,run_pass+commitby reference (commit writes the file in a tmp repo whendry_run=False; S4-b closed),commitwith no/foreign run_id,status.pending_runs; the two existingrun_pass/commitclient tests updated to the new return types. - Existing modules that must stay green:
tests/scripts/test_codemod_cli.py,tests/refactor/codemod/test_ipc_protocol.py,test_concurrency.py,test_daemon_lifecycle.py,test_daemon_coverage.py,test_client_coverage.py,test_server_coverage.py,test_review_engine_commit_wiring.py,test_observability_wiring.py.
@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):
- module, class, function, async-function and nested-function docstrings are rewritten;
- a plain string assigned to a variable, a dict key, a call argument, a
__all__entry, an f-string part, a bytes literal and a string in areturnare NOT rewritten (assert the exact bytes of those lines are unchanged); - a raw docstring
r"""\d OldWord"""becomesr"""\d NewWord"""(prefix kept, backslash intact); - a docstring with an escaped quote and a triple-quoted single-quote docstring round-trip;
- comments are still rewritten; an implicit-concatenation docstring is left alone;
- the whole-tree measurement from the plan: over
src/orchestrata_ultimate/toolsa{"the": "the"}no-op produces zero entries, and the reviewer's{X: Y}shape (pick a vocabulary word that appears in both a docstring and a string literal intools/remote_trigger_tool.py) changes only docstring/comment lines — assert by classifying every changed line throughast. scripts/codemod.py run --pass docstring-sweepend to end on the fixture in both modes (part A).
4. Gates and done-when
- Pytest only as
OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 PYTHONPATH=scripts timeout 300 .venv/bin/python -m pytest <modules> -p orchestrata_pytest_observer -p orchestrata_ultimate.core.threading.pytest_live_observer -q -p no:cacheprovider -rs, background + LITERAL log under/tmp/claude-1001/-home-mohamed/42ea036c-e296-4b26-b447-2f612e9dd49a/scratchpad/s4_<name>.logecho "rc=$?" >> <log>, waited withtimeout 290 bash -c 'until grep -q "^rc=" <log>; do sleep 10; done'. Groups under the 300-s cap: (1)tests/scripts/test_codemod_cli.py tests/scripts/test_codemod_cli_daemon.py; (2)tests/refactor/codemod/test_ipc_protocol.py test_concurrency.py test_client_coverage.py test_server_coverage.py; (3)tests/refactor/codemod/test_daemon_lifecycle.py test_daemon_coverage.py test_daemon_entry_coverage.py; (4)tests/refactor/codemod/test_transform_passes.py test_review_engine_commit_wiring.py test_observability_wiring.py test_workspace_load.py. Zero skips.
- Coverage ≥ 90% on
scripts/codemod.py,server.py,client.py, the changed region ofpasses.py(report the numbers;--cov=orchestrata_ultimate.refactor.codemod --cov=codemod). uv run ruff check,uv run black --check,uv run pyrighton changed.py(zero new errors);uv run python scripts/check_spec_authority.pyrc 0;uv run python scripts/gate.py runrc 0;.venv/bin/python scripts/serialize_codebase_ast.py check-deps --include-testsrc 0.- The purity audit forbids
monkeypatch, mocks,skip,xfail, module/class-scope fixtures. Real sockets, real server threads, real files. Thecapsysfixture is allowed. - Litter: restore
docs/CODEBASE_MAP.md docs/CODEBASE_MAP_FULL.md plans/ast-ultimate.json plans/ast-deep-comparison.json plans/ast-tests.json reports/.ship-verified reports/run-tests-latest.jsonby name before committing; the formatter hook pretty-printsplans/contracts/spec-110-codemod-engine.contract.jsonafter an Edit — do not edit it (the orchestrator owns D-entries). - Commits as
git -c user.name=Mohamed -c user.email=msamy@sufigeeks.org commit, on top of a793777cc, several small commits (part A server/client, part A CLI, part B, tests, docs) rather than one, no AI attribution, no "should" anywhere, no->in messages. Never rundeploy/ship.sh,promote_env.py, orsystemctlagainst anyorchestrata-*unit; the only daemon you start is the in-testCodemodServer. - Report: shas; per-group counts with log paths; coverage numbers; the warning line verbatim as
the test observed it; the both-modes equality evidence (which queries, which files); the
_PASS_QUERIESbacklog note; deviations. A fork not settled above is a STOP-and-report.
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):
- Each iteration reads
MainPID,ActiveStateandSubStatein ONE callsystemctl --user show -p MainPID -p ActiveState -p SubState <instance>(parsekey=valuelines; keep_unit_propertyfor its other callers). MainPID != 0→ return it (unchanged).- Else
ActiveState in {"failed", "inactive"}orSubState == "auto-restart"→ raiseRuntimeError(f"daemon {instance} failed before serving ({ActiveState})")immediately. Rationale in the docstring: forType=simple,systemctl startreturns once the main process is forked, so a zeroMainPIDwith one of those states means the first process is already gone; a unit inauto-restartwill fork again afterRestartSecbut that is a second start, not this one. - Otherwise keep polling to the existing deadline; the deadline text becomes
f"daemon {instance} failed before serving (no MainPID within {MAINPID_TIMEOUT:.0f}s, " f"ActiveState={state})"so every start failure shares the prefix the CLI documents.
§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.