Orchestrata Ultimate › Plans › briefs › 2026-10-13-daemon-publish-without-chdir
Brief: the AST daemon publishes without changing the process working directory
Source: plans/briefs/2026-10-13-daemon-publish-without-chdir.md at 8a2cb42ad · last changed 2026-10-13 · first committed 2026-10-13 · 39 lines.
Prose pending.
Plan text
Brief: the AST daemon publishes without changing the process working directory
Date: 2026-10-13 · DESIGN_WAIVER: ADR-0097 (Accepted: the resident AST daemon publishes the snapshot from its warm cache) and ADR-0067 R1 (Accepted: a daemon instance runs from /opt/orchestrata-tools; the instance parameter is data; the identity verifier judges the running process) · Origin: adversarial review findings D9 and Q10 of 2026-10-13, promoted from "ticket" to a fix by an observed failure the same evening · Executor: Kimi in worktree /home/mohamed/workspace/orchestrata-ultimate-worktrees/review-fix-chdir on branch fix/daemon-publish-no-chdir off origin/master bae8403fd · Design settled below; if a fork appears, STOP and report.
GOAL: a publish never changes the daemon's working directory, the published artifact stays byte-identical to a CLI run from the repository root, and scripts/verify_deployment_identity.py passes for an instance while it is publishing.
VERIFIED (coordinator, origin/master bae8403fd and the live host at 20:11 on 2026-10-13):
scripts/ast_daemon_publish.pyL211-221,SnapshotPublisher._publish_once: wrapstool.load_or_analyze(self._name, source, rel_output, monorepo=False, extra_roots=extra_roots)inwith contextlib.chdir(self._repo_root):; the comment says the serializer's relative-root and relative-file_path conventions andload_or_analyze's root-identity check are cwd-relative and that "every other thread in this daemon uses absolute paths".load_or_analyzeissrc/orchestrata_ast/analyzer.pyL1464.- Observed on the host: the instance
orchestrata-ast-daemon@…worktrees-mcp\x2dgateway.service(unitWorkingDirectory=/opt/orchestrata-tools, MainPID 2905710 started 19:11) had/proc/2905710/cwd=/home/mohamed/workspace/orchestrata-ultimate-worktrees/mcp-gatewayat 20:11 while every thread sat in epoll or futex waits (idle), 12 s after its last publish.os.chdiris process-global;contextlib.chdirrestores the directory it saw on entry, so two overlapping publishes (or a publish overlapping any otherchdir) leave the process in the wrong directory for good. - Consequence observed:
deploy/install-orchestrata.sh --ref bae8403fd --verifyfailedverify_deployment_identity.pywith…mcp\x2dgateway.service: /proc/2905710/cwd: expected /opt/orchestrata-tools, got /home/mohamed/workspace/orchestrata-ultimate-worktrees/mc…and rolled back, leaving/opt/orchestratawith a.venv/bin/orchestrata-mcpentry point from the new lock over the old checkout. A restart of the instance returned its cwd to/opt/orchestrata-tools. - Tests for the publisher live in
tests/scripts/test_ast_daemon_publish.py; the daemon's existing tests undertests/scripts/andtests/claude/use real processes (RUL-002: no mocks, no monkeypatch, no skips; RUL-001: no xdist).
FILES (repo-relative): Modify:
- src/orchestrata_ast/analyzer.py —
load_or_analyze(L1464) gains an explicitroot: Path | None = Nonekeyword; when given, every path it relativises (the source roots,extra_roots, the output path, the root-identity check) is computed againstroot, neveros.getcwd(). DefaultNonekeeps today's cwd behaviour for the CLI. Name every helper you had to threadrootthrough. - scripts/ast_daemon_publish.py —
_publish_oncedropscontextlib.chdir; it passesroot=self._repo_rootand absolutesource/extra_roots/output paths. Nothing else in the file changes. - plugins/orchestrata/scripts/ast_daemon_publish.py only if a byte-identical plugin copy exists (check with
git ls-files plugins | grep ast_daemon_publish); keep the copies identical. Create: - tests/scripts/test_ast_daemon_publish_keeps_cwd.py — a real
SnapshotPublisherfor a scratch git repository under/var/tmp/ct-chdir/<name>, started from a different working directory: after two publishes (the second triggered while the first runs, through the publisher's own trigger path),os.getcwd()is unchanged and/proc/self/cwdresolves to the start directory; the publishedplans/ast-<name>.jsonequals byte-for-byte the artifactscripts/serialize_codebase_ast.py serialize …produces from the repository root for the same tree. - tests/scripts/test_identity_verifier_during_publish.py — against the same scratch publisher process,
scripts/verify_deployment_identity.py's cwd judgment (call its judging function directly, or run the script with--rootand--patternnarrowed to the scratch process) reports no mismatch while a publish is in flight. Read-only: docs/adr/0097-.md, docs/adr/0067-.md, scripts/verify_deployment_identity.py, tests/scripts/test_ast_daemon_publish.py.
CONSTRAINTS:
- Read Python through the AST toolkit (
direnv exec <worktree> uv run python scripts/serialize_codebase_ast.py source <qualified_name>); a plain text tool on a.pyfile is blocked here. Markdown and shell with any tool. direnv exec /home/mohamed/workspace/orchestrata-ultimate-worktrees/review-fix-chdir uv run …for everything (never.venv/bin/..., never system python, never pip).- Tests observed:
timeout 300 direnv exec <worktree> uv run python -m pytest <modules> -p orchestrata_ultimate.core.threading.pytest_live_observer -q --basetemp=/var/tmp/ct-chdir/v(mkdir -p /var/tmp/ct-chdir first). Never re-run an unchanged red. - No doubles. Do not restart, stop or start any
orchestrata-ast-daemon@instance on this host; the scratch publisher is your own process. - Real pre-commit on commit, no
--no-verify; commits by Mohamed, stage by name, no attribution lines; never commitplans/ast-*.json,docs/CODEBASE_MAP*.mdor.claude/. - No deploy; the coordinator deploys through
deploy/install-tools.sh --refafter ship.
DONE WHEN:
- The two new tests and
tests/scripts/test_ast_daemon_publish.pyare green undertimeout 300, observed. grep -n "chdir" scripts/ast_daemon_publish.pyprints nothing.- ruff clean on the changed files;
mypy src/orchestrata_ast/analyzer.py scripts/ast_daemon_publish.pyclean. git status --shortempty; one commit naming the review findings and the observed rollback.
VERIFY: the pytest summary line; the empty grep; the ruff and mypy lines; git log --oneline -1.
Report back with those observed outputs and anything that stopped you.