Orchestrata Ultimate › Plans › claude-surface-pipeline

Claude surface: dev → test → prod

Source: plans/claude-surface-pipeline.md at 8a2cb42ad · last changed 2026-08-23 · first committed 2026-08-23 · 292 lines.

Prose pending.

Plan text

Claude surface: dev → test → prod

Status: written 2026-08-23. Supersedes plans/claude-surface-promotion-plan.md, which rested on a false premise — that the project Claude surface had no deployed copy — and specified an elaborate source relocation to fix a problem that does not exist. That work item is deleted; see §5. Every claim below was measured against the live system.

The model this plan implements, stated by the user:

DEV = where we develop. TEST = the environment that tests the code. PROD = a copy of the dev, versioned after tests pass, copied and registered, so we can then keep updating and testing files after deployment.


1. What exists today — every row measured, not assumed

1.1 There are TWO Claude surfaces, not one duplicated one

This is the answer to "why does the claude folder have a second copy of the code?" — it is not a copy. They are different surfaces with different scopes, different deploy targets, and different lifetimes.

claude/ — USER surface .claude/ — PROJECT surface
hooks 42 24
tools 26 0
agents 7 8
skills 8 6
commands 14 0
scope every project on this machine this checkout only
deployed to ~/.claude/* + the plugin cache travels inside the checkout
deploy driver deploy/install-claude-config.sh scripts/promote_env.py

1.2 The three environments

DEV   ~/workspace/orchestrata-ultimate-architecture   143310ec  master    12 dirty
TEST  Incus project orchestrata-test, app @ 10.53.2.13          — NO host tree
PROD  ~/prod/orchestrata-ultimate                     143310ec  detached   0 dirty

TEST has no tree to advance, by design. promote_env.py::_promote_test_container:

"The test env has NO host tree to advance: its kernel + gateway run in the app container of the Incus orchestrata-test project against a READ-ONLY bind-mount of this checkout … ~/test/orchestrata-ultimate — a tree that has never existed on this machine (measured 2026-08-14); every promote to test was a guaranteed failure against a phantom target."

Consequence for this plan: the Incus test fleet cannot be the test stage for the Claude surface. It has no host tree and hosts no Claude session. §4 (W3) uses the seam that can.

1.3 PROD is already a copy — for both surfaces

Project surface — promoted by tree advancement. promotion/stages.py::install_assets:

"The hooks and skills travel WITH the checkout — they live under .claude/ in the tree, so advancing the tree already deployed them. What does not travel is the executable bit … and the systemd units."

Measured, dev vs the prod checkout: hooks 24/24, skills 6/6, agents 8/8. The prod checkout is at 143310ec detached, 0 dirty. install_assets FAILS the deploy if any non-_-prefixed hook arrives without +x, and refuses to repair it on the target ("fix the mode in git, not on the target"). Its glob is .claude/hooks/*.sh; all 24 project hooks are .sh, so coverage is total.

User surface — install-claude-config.sh:

That last check exists because of a measured incident: "REGISTERED IS NOT COPIED, AND A PIN IS NOT CONTENT" — the pin read c24da189 for five days while the content was 19c26470, and the ten stale files in the live cache included the context-injection machinery that runs on every prompt.

Surface parity today, dev vs plugin vs ~/.claude: hooks 42/42/42, tools 26/26/26, agents 7/7/7, skills 8/8/8, commands 14/14/14. The only difference in the whole tree is __pycache__, correctly excluded.

1.4 The four-way identity invariant holds right now

Measured 2026-08-23 — this is what ties the two surfaces to one version:

commit
origin/master 143310ec6d63
prod checkout HEAD 143310ec6d63
~/.claude/VERSION 143310ec6d63
installed plugin gitCommitSha 143310ec6d63

All four agree. Both surfaces are provably at one version, and that version is on the remote.

1.5 One driver runs both halves

deploy/ship.sh is the single entry point:

line action
492 install-claude-config.sh — user surface
543 install-claude-config.sh --verify
580 promote_env.py promote --to prod --yes --skip-ingest — project surface
5a fails unless $PROD_ROOT HEAD == origin/master
5b fails unless every orchestrata-* unit's ExecStart points into $PROD_ROOT

5a/5b verify the outcome independently rather than trusting an exit code, because "the promotion ledger recorded DEPLOYED for a tree that then sat still for four days."

1.6 Verdict on the ladder

rung project surface user surface
versioned after tests pass ✅ ship runs the suite, then promotes ✅ pinned SHA, ancestor-checked
copied ✅ 24/6/8 present in the prod checkout ✅ rsync + per-file byte compare
registered n/a ✅ installed and enabled, both verified
executes ❌ unproven ❌ unproven

The copy, version and register rungs the user asked for are already built and currently green. What is missing is the last rung, plus two structural defects that make it hard to prove. That is the entire remaining scope.


2. W1 — make the plugin self-contained (10 of 31 hook commands)

The defect

gen_plugin.py rewrites hook commands to ${CLAUDE_PLUGIN_ROOT}/… so the plugin is relocatable. The regex only matches the literal prefix:

CLAUDE_HOME_PREFIX = "/home/mohamed/.claude/"
_SCRIPT_PATH_RE = re.compile(r"(?<!\S)" + re.escape(CLAUDE_HOME_PREFIX) + r"(\S+)")

Measured over the 31 declared commands in claude/settings.hooks.json:

The 10 break down as 8 distinct hook scripts (ast_sync_hook.py, platform_conventions_prompt.py, agent_mail_inbox.py ×2, project_context_refresh.py, workflow_context_inject.py, convention_guard.py, task_context_hook.py) plus tools/claude_turn_capture.py, registered twice.

Nothing here runs from the dev tree — ~/.claude/hooks is itself a deployed copy, rsynced from a pinned git archive. The defect is narrower and more structural: the plugin, the artifact whose whole purpose is to be self-contained and independently verifiable, silently depends on a separate rsync into a machine-specific path having already happened. One surface, two delivery paths, two verification paths.

The fix

Extend the pattern to the $HOME / ${HOME} spellings so all 31 rewrite. The _rewrite_command docstring currently documents the pass-through as intentional — that note gets replaced, not silently contradicted.

The guard

A test asserting 31 of 31 generated commands are plugin-relative and none contains .claude/. A count, not a spot check, so a hook added later in the $HOME spelling fails the gate instead of quietly taking the second path.

Ordering

W1 lands before W2 — it changes what W2 has to assert.


3. W2 — prove the executes rung

The gap

The hook tests are better than expected: tests/claude/ runs hooks as real subprocesses with real hook JSON on stdin, which is exactly how Claude invokes them. test_plugin_path_independence.py says so explicitly and builds a from-scratch environment mapping per call.

But every one of them executes claude/hooks/… — the source path — and path-independence is proven for 2 representative hooks of 42. That is the precondition for deploying anywhere. It is not evidence that the deployed copy runs. Nothing in the tree executes ~/.claude/hooks/x.py or the plugin-cache copy.

The fix

A smoke suite that reads the generated hook declaration, and for each registered command:

  1. executes the command string as written (so ${CLAUDE_PLUGIN_ROOT} resolves against the real plugin root, and any surviving path is exercised as-is);
  2. feeds a minimal valid payload for that hook's event type;
  3. asserts the exit-code contract for its category — a gate may block (exit 2), an injector must not.

Zero commands executed is a FAILURE, not a pass. This is the recurring defect class in this repo — the serializer reported what it wrote, not what it lost; the syncer asked its cache, not its destination; /code read silence as absence. An empty smoke run must be loud.

Reuse the environment-construction helper from test_plugin_path_independence.py rather than writing a second one.


4. W3 — a real test stage for the user surface

Why the Incus fleet cannot be it

§1.2: no host tree, no Claude session. Pointing this surface at 10.53.2.x would be the phantom-target failure again, in a new place.

The seam that already exists

install-claude-config.sh already takes --prefix (default $HOME/.claude). The surface can be installed, in full, into a throwaway root. That is the test environment for this surface — the same artifact, the same installer, the same per-file byte comparison, a different destination.

The stage

dev  →  install --prefix <staging>  →  W2 smoke suite against <staging>  →  install to ~/.claude

Wire it into ship.sh between the suite and line 492. A staging install that fails the smoke suite fails the ship, and ~/.claude is never touched.

This makes dev → test → prod literal for the user surface using machinery that is already written and already verified, rather than inventing a fourth environment.


5. Deleted: the source relocation (old W5)

The superseded plan proposed moving the project Claude surface into claude/project/ and generating .claude/ from it, on the belief that the project surface had no deployed copy and therefore no dev/prod separation.

That belief was wrong. /home/mohamed/prod/orchestrata-ultimate exists, is pinned at 143310ec detached with 0 dirty, carries its own .claude/ (24 hooks, 8 agents, 6 skills — matching dev exactly), and the systemd units run from it. Checkout-level separation already provides everything the relocation was meant to provide, and install_assets already gates the one thing that does not travel with a tree.

Deleted, not deferred. There is nothing to do here.


6. Deferred — recorded, not silently dropped


7. Order of work

# item gate
0 commit the 12 dirty files tree clean before W1
1 W1 regex + docstring + count test 31/31 plugin-relative
2 W2 smoke suite every registered command executed; 0 executed = FAIL
3 W3 staging prefix wired into ship staged smoke failure blocks the ship

Each lands independently and each is verified by a test that fails before the change and passes after — no item is complete on a claim.