Files
ThothII/docs/superpowers/plans/2026-08-10-p3-effective-config-fingerprint.md
T

155 KiB
Raw Blame History

P3 Effective Configuration Fingerprint and Revision Isolation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Make DWH-derived preprocessing safely reusable across semantically equivalent Git revisions while keeping curated artifacts, corpus state, and schema/Evidence vectors revision-pinned and Memory workspace-global, with explicit compatibility migrations from the P2 layout.

Architecture: The Python harness owns one versioned canonical effective-DWH binding computed from an allowlist of non-secret, output-affecting values. Both the P2 dedicated one-shot maintenance process and session runtime consume the same rendered config and ask the harness for that binding; neither hashes temporary YAML paths independently. thothctl launches only the profile-gated workspace-maintenance Compose job: that process owns client-addressed durable run creation/replay, owner-qualified quiescence, the exclusive reader gate, and the inherited P2 writer-lock capability without requiring Fastify or stopping core. A binding-keyed workspace-global DWH cache feeds verified immutable snapshots into revision-qualified runtime roots, while explicit migrations copy and verify schema-v1 owners and legacy Memory state without reinterpreting or merging it in place. Qdrant partitions identity by (workspace_id, workspace_revision) for schema/Evidence and by workspace_id alone for Memory/solved records.

Tech Stack: Python 3.12, Pydantic v2, Typer, pytest, Node.js 22, TypeScript, Vitest, repo-owned Node-API v8 workspace-fs-at with a closed typed lock API and wrapper-internal exact fs-ext@2.1.1 for flock(2) only, Go 1.26.5 thothctl (matching go.mod toolchain go1.26.5), Docker Compose, Qdrant, Ollama, Bash, Git, YAML/JSON, SHA-256, UUIDv5, POSIX *at/flock/fsync/atomic rename.


Scope, dependency gate, and invariants

This is P3 / D3 only. P2 must already be implemented, automatically green, manually accepted, and present in this worktree. P3 extends—without renaming, wrapping in a parallel tree, or duplicating—P2's exact frozen handoff:

  • backend/src/workspaces/runtime-config-lease.ts / WorkspaceRuntimeConfigLeaseFactory.acquireSession and .acquireMaintenance;
  • backend/native/workspace-fs-at/{workspace_fs_at.cc,binding.gyp}, backend/src/native/workspace-fs-at-binding.d.ts, backend/src/workspaces/workspace-fs-at.ts, and backend/scripts/build-workspace-fs-at.mjs / the repo-owned Node-API v8 WorkspaceFsAtV1 seam with typed openat/mkdirat/no-follow fstatat/directory-fsync/close ownership, exact LockFileName = "writer.lock" | "session-readers.lock", openOrCreateLockAt(...), and flockOwnedLock(handle, ownership, wait); exact fs-ext@2.1.1 remains wrapper-internal and flock(2)-only;
  • backend/src/workspaces/workspace-lock-root-lease.ts / CanonicalWorkspaceLockRootInput, BorrowedVerifiedWorkspaceLockRootLease, VerifiedWorkspaceLockRootLease, WorkspaceSessionReadersLockLease, VerifiedWorkspaceLockRootLease.acquireSessionReadersShared(), VerifiedWorkspaceLockRootLeaseFactory.acquire/.acquireOrProvision, and the retained root identity, all consuming only that typed FD-relative seam;
  • backend/src/workspaces/preprocessing-state.ts / PreprocessingStateStore, BorrowedWorkspaceSessionReadersExclusiveLockLease, WorkspaceWriterLockCapability.runUnderSessionReadersExclusive(...), OrderedWorkspaceWriterCapabilitySet, runUnderWorkspaceWriterLock, runUnderOrderedWorkspaceWriterLocks, and the exact inherited writer FD 3/root FD 4 contract;
  • backend/src/workspaces/registry-publication.ts / the exact RegistryAddressedRequestV1 bootstrap/pull union, RegistryAddressedPlanV1 bootstrap/pull union, RegistryAddressedPublicationStateV1 bootstrap/pull union, RegistryBootstrapRecoveryIdentityV1, RegistryBootstrapRecoveryScanLimitsV1, RegistryActiveSnapshotV1, and exact RegistryEnsureBootstrapAddressedResultV1 (already_active snapshot or bootstrap_terminal result), phases request_claimed through terminal_durable, AddressedWorkspacePublicationLeaseV1, CapabilityAwareRegistryPublicationParticipant, CapabilityAwareRegistryPublicationSynchronizer, and CapabilityAwareRegistryPublicationLifecycleOwner.run;
  • backend/src/workspaces/registry.ts / WorkspaceRegistry.ensureBootstrapAddressed as the sole bounded automatic bootstrap selector and WorkspaceRegistry.publishAddressed for explicit addressed mutation;
  • backend/src/routes/workspaces.ts and backend/src/app.ts / inspect, lazy bootstrap, and status routed only through ensureBootstrapAddressed, pull and author publication routed only through publishAddressed, with no old bootstrap/pull/activate/direct-pointer escape;
  • backend/src/workspaces/preprocessing-service.ts / WorkspacePreprocessingService.execute;
  • the single backend/src/workspace-maintenance.ts / exported main entrypoint;
  • tools/thothctl/internal/workspaceops/operations.go / ParseWorkspaceCommand and Run;
  • P2 tests workspace-fs-at-native.test.ts, workspace-lock-root-lease.test.ts, workspace-session-readers-lock.test.ts, workspace-runtime-config-lease.test.ts, workspace-preprocessing-state.test.ts, workspace-registry-addressed-publication.test.ts, workspace-registry-addressed-process.test.ts, routes-workspaces.test.ts, app.test.ts, workspace-preprocessing-service.test.ts, and workspace-maintenance.test.ts;
  • scripts/p2-acceptance.sh.

No backend/src/workspace-maintenance/ directory, WorkspaceMaintenanceOperator, tools/thothctl/internal/workspace package, workspace.Parse, or workspace.Run may be introduced: those names are nonexistent and conflict with the P2 handoff.

Do not start P3 against the current pre-P2 tree. Do not implement P4 collection creation/rebuild, P5 Git annotation synchronization, P6 filesystem Evidence materialization, PSD migration, SSH runtime transport, GUI/API preprocessing, or aggregate P2–P6 verification.

Preserve these invariants throughout:

  1. P2 reads/writes the existing OWNER.json schema-v1 contract unchanged until the explicit P3 migration code and tests exist. Capture a valid P2 schema-v1 fixture before changing the writer.

  2. The reusable digest excludes secrets, credential values/files, Git commit, random config filename, runtime_identity, session_storage, vector/embedding/search/execution settings, and revision-qualified output paths.

  3. The digest includes every non-secret value that can change physical/LSH output: DWH transport and canonical endpoint identity, database/schema and non-secret login identity, examples/introspection, eligibility and LSH policies, plus an explicit artifact-layout policy version.

  4. A content-only Git commit reuses a cache. A changed endpoint, transport, database, schema, or included policy gets another cache and never falls back to the old cache.

  5. Runtime layout is exactly:

    /data/sessions/<workspace-id>/
      sessions/                                  # workspace-global session manifests
      memory/                                    # workspace-global canonical registry
      preprocessing/dwh-cache/<binding-key>/     # reusable immutable generations
      revisions/<40-hex>/readiness/<binding-key>/READY.json
      revisions/<40-hex>/dwh-snapshots/<binding-key>/{artifacts,indexes}/
      revisions/<40-hex>/artifacts/              # annotations and revision-owned artifacts
      revisions/<40-hex>/indexes/                # revision-owned indexes
      revisions/<40-hex>/corpus/                 # corpus generations + ACTIVE
    
  6. paths.memory is authoritative when present. Only a config that lacks it may use the compatibility fallback paths.artifacts/memory.

  7. Schema/Evidence Qdrant reads, hashes, writes, lists, and deletes require a 40-hex revision in point identity and filter. Memory/solved records deliberately omit revision from identity and filters.

  8. P3 is additive until workspace layout enablement. preprocessing/layout-version.json is one workspace-global version marker and never names a revision or binding. Readiness is immutable and keyed by the exact pair (40-hex Git commit, 64-hex effective-DWH cache key) at revisions/<commit>/readiness/<binding-key>/READY.json, but at most one effective binding may ever become READY for a Git revision. Before preparation or activation, securely enumerate that revision's readiness directory: an existing valid READY for the selected key may only byte-match; any valid READY for another key makes activation fail effective_config_mismatch before cache/snapshot/semantic writes. A changed effective binding therefore requires a new content commit/revision. Once the global marker exists, a session probes its harness-owned effective binding, selects only that exact readiness generation and binding-qualified DWH snapshot, and fails migration_required before Pi/session child spawn when it is absent. Prepare maintenance is available for an unready revision only when no different binding is already READY there, through the trusted future resolver and never through an active fallback.

  9. Every mutating migration/activation child runs under P2's exact opaque WorkspaceWriterLockCapability; its sole spawnChild method passes the actual locked writer open file description as FD 3 and the same retained verified root directory open file description as FD 4. P3 extends P2's one closed request union in place. Before content access, the child validates FD 4 as the expected service-owned root, opens preprocessing/writer.lock relative to FD 4, proves that inode is FD 3, and proves FD 3 is the already-held exclusive lock. P3 introduces no root brand, verified-root string, second capability, direct spawn, or ambient/path authorization seam. Source/destination bytes are reverified, publication is atomic/idempotent, and legacy filesystem sources remain for rollback. Conflicting Memory registries or cache destinations fail closed.

  10. The public non-activating semantic-revision command is inventory/rebuild-readiness-only: it persists exact legacy IDs/digests and verifies that current schema/Evidence sources are rebuildable, but performs zero Qdrant replacement upserts and zero deletes. Semantic publish-before-delete occurs only inside one quiesced activation transaction: after durable admission blocking and exclusive acquisition of the no-follow reader gate, reverify the persisted inventory and sources, publish and read back every replacement, publish/verify the global reader-layout marker on initial enablement, delete only unchanged exact legacy IDs, and publish that commit+binding READY last. core may remain running because the durable marker prevents new admission and the exclusive gate proves zero active session readers. No successful non-activation command can create a mixed legacy/replacement reader interval.

  11. The four ordinary operations (migrate_dwh_cache, migrate_memory, migrate_semantic_revision, and activate_revision_layout) keep the selected-workspace writer-first lifecycle: the sole P2 factory acquires the existing root, transfers it into runUnderWorkspaceWriterLock, then the still-live exact capability owns ordinary job/quiescence, exclusive reader-gate acquisition, participants/locked children, publication, reverse release, and root close. The client pre-generates the run ID; process death retains state and owner quiescence for exact same-ID dead-core resume. Only activation may publish Qdrant replacements, switch reader mode/publish the sole binding READY, or delete legacy IDs.

  12. registry_pull is the exact repository-first P2 callback exception and never enters the ordinary writer-first function. It invokes only WorkspaceRegistry.publishAddressed with a P2 registry_pull create/resume literal whose validated installation boundary supplies installationIdentitySha256, repositoryIdentitySha256, and remoteRefIdentitySha256, and whose create branch also supplies expectedBaseCommit. Inspect, lazy list/bootstrap, and status instead invoke only WorkspaceRegistry.ensureBootstrapAddressed. Its continuously repository-locked selector first revalidates active state: a valid snapshot returns P2's exact already_active result without job scanning, state writes, or network; only absence enters bounded exact-identity zero-create/one-resume selection. Corrupt or incompatible active state, corrupt/mismatched/pull/multiple nonterminal jobs, and every identity/base mismatch fail closed before network or state mutation. Under repository.lock, P2 durably advances request_claimed → target_advertised → target_fetched → planned, then acquireOrProvisions every complete changed-ID root through the sole typed WorkspaceFsAtV1 seam and passes all roots once to runUnderOrderedWorkspaceWriterLocks. The exact same callback-scoped set flows through the production lifecycle owner, per-workspace addressed participant leases, every synchronizer, pointer publication, and terminal_durable; reverse invalidation/release occurs before repository release. The same owner and callbacks remain valid for P2's no-active-base registry_bootstrap plan/result variant. Same-ID recovery is phase-exact: before a durable advertisement it may repeat advertisement; at/after target_advertised the recorded OID is permanent, exact-OID fetch/ref reconciliation is the only allowed recovery, and after target_fetched no network or target reselection occurs. migrate_dwh_cache remains pre-READY preparation and writes no global marker, semantic point, or READY.

  13. Maintenance control is profile-independent and host-unpublished: workspaceops.Run invokes only the selected installation's existing profile-gated docker compose run --rm --no-deps --no-TTY workspace-maintenance job with bounded canonical stdin/result. It never calls Fastify, an HTTP/internal route, compose exec core, a frontend proxy, host Python/Node/Pi/tht, or a core stop/start command. Request/result bounds, lock order, no-follow files, deadlines, state transitions, and failure mapping are the exact contract frozen in Task 8.

  14. Every admitted session owns P2's exact WorkspaceSessionReadersLockLease from the final quiescence recheck until all Pi/session children and their stdout/stderr streams have settled. WorkspaceReaderLeaseFactory.acquireForSession delegates only to VerifiedWorkspaceLockRootLease.acquireSessionReadersShared(), and the resulting owner is transferred into PiProcessManager for exactly-once teardown. Maintenance publishes/drains quiescence first, then WorkspaceReaderLeaseFactory.acquireForMaintenance delegates only to WorkspaceWriterLockCapability.runUnderSessionReadersExclusive(...) around the full quiesced action through terminal durability and matching owner clear; its borrowed lease never escapes the callback. P3 never opens/flocks directly, imports WorkspaceFsAtV1, fs-ext, or the raw addon for reader acquisition, exposes a directory handle/path/FD/lock name/flags, casts a handle/name, or adds another lock wrapper. --json remains pristine and all public errors use bounded stable codes, especially effective_config_mismatch, migration_required, preprocessing_conflict, workspace_quiesced, registry_bootstrap_recovery_conflict, and semantic_index_incompatible.

Automated process goal

At implementation start, register this persistent goal if the agent runtime supports goals:

From a clean P3 source tree and clean Docker/fixture namespace, use only the released thothctl host interface to launch dedicated maintenance jobs that atomically create/resume client-addressed runs, durably block admission, drain shared reader leases, and hold the exclusive reader gate plus the exact P2 capability's writer FD 3 and retained-root FD 4 without Fastify or stopping core. Migrate a valid P2 schema-v1 DWH owner and legacy Memory registry; prove dwh-cache materializes the selected binding-qualified revision snapshot without READY activation; prove every non-activating command preserves one unmixed reader mode; then use one activation to publish/verify revision-scoped schema/Evidence replacements, switch the global layout, delete exact verified legacy IDs, publish immutable commit+binding READY, and owner-clear quiescence. Pull content-only revision B, block it until its exact READY, and reuse the cache while isolating revision state. Change binding X to Y at READY B, prove B admission and same-revision activation are refused without mutation, publish/pull C, and prove new v2 Y cache → C/Y snapshot → immutable C/Y READY. Restore installation binding X and admit historical B/X, then restore Y and admit C/Y, proving immutable B/X bytes rather than pinned-revision-only operability; retain global Memory/solved, reject conflicts, survive dead-core resume and SIGKILL, scan fixture secrets, and remove exactly owned resources.

Keep the goal open until ./scripts/p3-acceptance.sh integration --keep succeeds once from clean state with no automatic retry and its report has been verified. Focused tests are progress evidence, not completion of the process goal.

Controlled topology and report contract

The P3 process test extends P2's private fixture topology: a bare local Git remote with revisions A/B/C, installation descriptor and fixture secret files, the real dedicated workspace-maintenance image/job, an optionally running or deliberately unavailable core, real Qdrant, real internal Ollama, and the P2 controlled REST-DWH fixture. No production credential, registry, volume, or network is permitted. The process owns a unique Compose project and .artifacts/p3-effective-config/<run-id>/ownership.json.

report.json and report.md must record the clean source commit/tree, built image digest, fixture Git commits, workspace ID/revisions, safe canonical binding hashes, owner schema/digests, DWH ACTIVE generation and file digests, commit+binding readiness paths and binding-qualified snapshot identities, Qdrant counts by safe kind/revision, Memory registry/projection counts, command event results, secret-scan result, cleanup inventory/result, and overall PASS/FAIL. They must never contain endpoint credentials, fixture secret values, signed URLs, rendered config, raw child stderr, or unbounded logs. --keep retains the owned run for review; cleanup --run <run-id> later removes only resources in its ownership manifest while retaining the sanitized reports and a final cleanup report.

There are no unavoidable human steps inside automation. Manual acceptance is a separate clean environment after automated PASS.


Task 1: Freeze P2 schema-v1 ownership and baseline the dependency

Files:

  • Create: harness/tests/fixtures/dwh-owner-v1/README.md
  • Create: harness/tests/fixtures/dwh-owner-v1/OWNER.json
  • Create: harness/tests/fixtures/dwh-owner-v1/ACTIVE
  • Create: harness/tests/fixtures/dwh-owner-v1/generations/11111111111111111111111111111111/{physical.yaml,analytics_lsh.pkl,analytics_minhashes.pkl,analytics_meta.json,generation-manifest.json}
  • Modify: harness/tests/test_dwh_preprocess_job.py

Step 1: Verify the P2 checkpoint before editing.

Run:

git status --short
./scripts/p2-acceptance.sh integration --keep
(cd backend && npm run build:native && npx vitest run \
  test/workspace-fs-at-native.test.ts test/workspace-lock-root-lease.test.ts \
  test/workspace-session-readers-lock.test.ts \
  test/workspace-registry-addressed-publication.test.ts \
  test/workspace-registry-addressed-process.test.ts test/routes-workspaces.test.ts \
  test/app.test.ts && npx tsc --noEmit -p .)
cd harness && .venv/bin/pytest -q tests/test_dwh_preprocess_job.py

Expected: clean tracked tree before the retained P2 report is created; P2 report says automated integration: PASS; the repo-owned workspace-fs-at addon/root lease and automatic bootstrap recovery route/app/process suites pass with TypeScript green; and the existing DWH suite passes. If P2 has not been manually accepted, stop for the P2 checkpoint.

Step 2: Create the fixture through the unmodified P2 operational writer.

Use the P2 controlled config to run tht preprocess dwh, copy only the bounded owner/ACTIVE/generation files above, replace source-specific values with deterministic fixture values, recompute all declared SHA-256 values, and document the exact generation command in README.md. Do not hand-wave a structurally plausible owner.

Step 3: Write the RED compatibility test.

Add test_schema_v1_fixture_is_a_valid_p2_owner_and_remains_readable and test_p2_writer_still_emits_schema_v1_before_explicit_migration to test_dwh_preprocess_job.py. The first loads the fixture with the current reader and verifies all files/ACTIVE; the second asserts the P2 writer still emits schema_version == 1 at this checkpoint.

Step 4: Run the focused tests.

cd harness && .venv/bin/pytest -q \
  tests/test_dwh_preprocess_job.py::test_schema_v1_fixture_is_a_valid_p2_owner_and_remains_readable \
  tests/test_dwh_preprocess_job.py::test_p2_writer_still_emits_schema_v1_before_explicit_migration

Expected: PASS. This is a characterization task, not production behavior change.

Step 5: Commit the fixture checkpoint.

git add harness/tests/fixtures/dwh-owner-v1 harness/tests/test_dwh_preprocess_job.py
git commit -m "test: preserve P2 DWH owner compatibility fixture"

Task 2: Add the harness-owned versioned effective-DWH canonicalizer

Files:

  • Create: harness/tht/effective_dwh.py
  • Create: harness/tests/test_effective_dwh_binding.py
  • Modify: harness/tht/config.py
  • Modify: harness/tht/cli/config_cmd.py
  • Modify: harness/tht/jobs/dwh_pipeline.py

Step 1: Write RED tests for the exact canonical contract.

Create tests named:

  • test_binding_is_canonical_versioned_secret_free_and_source_stable
  • test_content_revision_temp_path_session_and_semantic_changes_do_not_change_binding
  • test_each_dwh_output_affecting_field_changes_binding
  • test_missing_registry_identity_cannot_claim_a_v2_cache
  • test_effective_dwh_json_is_pristine_and_safe

Use parameterized mutations for transport, direct host/port/user, REST canonical base URL, database, schema, examples, eligibility, every LSH field, language if it affects generated descriptions, and artifact_layout_version. Include obvious fixture passwords/API keys and assert neither secret nor secret-file path appears in canonical JSON, CLI output, exceptions, or repr.

Run:

cd harness && .venv/bin/pytest -q tests/test_effective_dwh_binding.py

Expected: RED because tht.effective_dwh and tht config effective-dwh do not exist.

Step 2: Implement only the canonical contract.

In effective_dwh.py, define immutable models/constants and functions with these public names:

EFFECTIVE_DWH_SCHEMA_VERSION = 2
CANONICALIZER_VERSION = "effective-dwh-v1"
ARTIFACT_LAYOUT_VERSION = "dwh-cache-v1"

class EffectiveDwhBinding(BaseModel):
    schema_version: Literal[2]
    workspace_id: str
    logical_source_identity: str
    canonicalizer_version: Literal["effective-dwh-v1"]
    effective_config_sha256: str
    input_fingerprint: str


def canonical_effective_dwh_config(cfg: Config) -> dict[str, object]: ...
def effective_dwh_binding(cfg: Config) -> EffectiveDwhBinding: ...
def effective_dwh_binding_json(cfg: Config) -> str: ...  # sort_keys, compact, trailing newline
def effective_dwh_cache_key(binding: EffectiveDwhBinding) -> str: ...  # 64 lowercase hex
def effective_dwh_cache_root(cfg: Config) -> Path: ...
def legacy_schema_v1_binding(cfg: Config) -> dict[str, str]: ...

canonical_effective_dwh_config must build an explicit allowlist, normalize URLs/host case/default ports without DNS/network access, and reject query/userinfo/fragments. Never start with cfg.model_dump() and subtract fields. effective_dwh_cache_root appends the binding key to the configured cache base; it must reject missing registry identity and any symlink/path escape.

Add Config._logical_source_identity if needed, but keep _config_source as the compatibility source used by legacy_schema_v1_binding. Registry configs set both to workspace://<id>; legacy file configs retain their resolved filename only for schema-v1 compatibility.

Add tht config effective-dwh --json -c <config> in config_cmd.py. It emits only the safe binding, cache key, and layout version.

Replace dwh_pipeline.config_dwh_binding internals with a compatibility wrapper that delegates to effective_dwh_binding only after the new owner path is enabled in Task 4; until then it must keep schema-v1 behavior so the characterization test remains green.

Step 3: Run RED/GREEN tests.

cd harness && .venv/bin/pytest -q tests/test_effective_dwh_binding.py tests/test_config_resources.py

Expected: PASS; exact stdout from the JSON test parses as one object and contains no secret fixture.

Step 4: Lint the touched Python.

cd harness && .venv/bin/ruff check tht/effective_dwh.py tht/config.py tht/cli/config_cmd.py tht/jobs/dwh_pipeline.py tests/test_effective_dwh_binding.py

Expected: PASS.

Step 5: Commit.

git add harness/tht/effective_dwh.py harness/tht/config.py harness/tht/cli/config_cmd.py \
  harness/tht/jobs/dwh_pipeline.py harness/tests/test_effective_dwh_binding.py
git commit -m "feat: define canonical effective DWH binding"

Task 3: Add global layout and immutable commit+binding readiness models without activation

Files:

  • Modify: backend/src/workspaces/runtime-renderer.ts
  • Create: backend/src/workspaces/revision-layout.ts
  • Modify: backend/test/workspace-runtime-renderer.test.ts
  • Create: backend/test/workspace-revision-layout.test.ts
  • Modify: harness/tht/config.py
  • Modify: harness/tht/paths.py
  • Create: harness/tht/dwh_snapshot.py
  • Create: harness/tests/test_dwh_snapshot.py
  • Modify: harness/tests/test_config_resources.py
  • Modify: harness/tests/test_portable_paths.py

This task is deliberately additive. Session and ordinary maintenance continue to render the exact P2 workspace-global roots. No marker is written and no runtime consumer changes behavior here.

Step 1: Write RED model, secure-reader, and resolver tests.

Add exact future layout support for:

/data/sessions/<workspace-id>/
  preprocessing/layout-version.json             # workspace-global version only
  preprocessing/dwh-cache/<binding-key>/
  revisions/<40-hex>/readiness/<binding-key>/READY.json
  revisions/<40-hex>/dwh-snapshots/<binding-key>/artifacts/ACTIVE
  revisions/<40-hex>/dwh-snapshots/<binding-key>/artifacts/generations/<generation>/physical.yaml
  revisions/<40-hex>/dwh-snapshots/<binding-key>/artifacts/generations/<generation>/snapshot-manifest.json
  revisions/<40-hex>/dwh-snapshots/<binding-key>/indexes/generations/<generation>/analytics_lsh.pkl
  revisions/<40-hex>/dwh-snapshots/<binding-key>/indexes/generations/<generation>/analytics_minhashes.pkl
  revisions/<40-hex>/dwh-snapshots/<binding-key>/indexes/generations/<generation>/analytics_meta.json
  revisions/<40-hex>/corpus/
  memory/
  sessions/

Freeze LayoutVersionMarkerV1 as an exact-key schema containing only schemaVersion: 1, layoutVersion: "revision-layout-v1", workspace ID, and enabled UTC time. It never contains an active revision or binding. Freeze RevisionReadyManifestV1 as an exact-key schema containing workspace ID, 40-hex revision, descriptor blob, complete effective DWH binding plus its binding SHA and 64-hex cache key, the binding-qualified DWH snapshot generation+manifest digest, Memory registry digest/status, semantic inventory/replacement/deletion digests and status, preparing outer run ID, and ready UTC time. Its canonical bytes are published exclusively at revisions/<revision>/readiness/<cache-key>/READY.json; an existing file must byte-match after strict reverification and is never replaced. Thus readiness is an immutable generation for one exact (commit, effective binding) pair, and the sole session admission proof is the manifest selected by the session's harness-reported cache key. The secure readiness-directory reader additionally enforces at most one READY key per revision: a second valid key, an unsafe entry, or ambiguous directory state fails closed. A same-commit installation binding change cannot create a sibling READY or snapshot; activation returns effective_config_mismatch before mutation and the changed binding must be paired with a new content commit/revision.

Freeze these shared synchronous TypeScript exports in backend/src/workspaces/revision-layout.ts:

import type {
  BorrowedVerifiedWorkspaceLockRootLease,
  CanonicalWorkspaceId,
  Revision40,
  Sha256Hex,
} from "./workspace-lock-root-lease.js";
export type WorkspaceLayout = "p2-global" | "revision-layout-v1";
export type RevisionLayoutState =
  | { layout: "p2-global"; ready: null }
  | { layout: "revision-layout-v1"; ready: RevisionReadyManifestV1 | null };
export function futureWorkspaceLayoutPaths(
  rootLease: BorrowedVerifiedWorkspaceLockRootLease,
  workspaceId: CanonicalWorkspaceId,
  workspaceRevision: Revision40,
): RuntimePaths;
export function bindingQualifiedWorkspaceLayoutPaths(
  futurePaths: RuntimePaths, effectiveDwhCacheKey: Sha256Hex,
): RuntimePaths;
export function workspaceRuntimePaths(
  rootLease: BorrowedVerifiedWorkspaceLockRootLease,
  workspaceId: CanonicalWorkspaceId,
  workspaceRevision: Revision40,
  state: RevisionLayoutState,
): RuntimePaths;
export function readRevisionLayoutState(
  rootLease: BorrowedVerifiedWorkspaceLockRootLease,
  expected: {
    workspaceId: CanonicalWorkspaceId; workspaceRevision: Revision40; descriptorBlob: Revision40;
    effectiveDwhCacheKey: Sha256Hex;
  },
): RevisionLayoutState;

readRevisionLayoutState is deliberately synchronous inside an already-active P2 root-lease borrow, so WorkspaceRuntimeConfigLeaseFactory.acquireSession and .acquireMaintenance retain their released signatures and no raw root string crosses the handoff. It is called only after the fixed read-only harness binding probe has returned the exact cache key. The module uses P2's installation-bound root-relative safe reader; it never derives/reopens an ambient path. It opens every component and leaf with O_NOFOLLOW, fstat, owner/mode/nlink/regular-file and post-read identity checks, bounds both JSON files, and closes every relative child FD while leaving the borrowed root owned by its caller. An absent global marker returns p2-global. A present invalid marker fails closed. With a valid global marker, absence of the exact readiness/<expected-cache-key>/READY.json returns {layout: "revision-layout-v1", ready: null} when the exact key is absent; it never selects newest, the sole other-key entry, or a revision-only READY, so session admission maps the absence to migration_required. The activation/prepare guard separately reports effective_config_mismatch when that sole other-key READY exists. Multiple READY keys, an unsafe entry, or a present invalid, descriptor-, revision-, or binding-mismatched selected READY fail closed as corrupted state. No asynchronous wrapper, readFile, or symlink-following convenience API is permitted.

futureWorkspaceLayoutPaths is the trusted migration-destination resolver. It validates the already verified workspace root, workspace ID, and 40-hex revision, then derives binding-independent future roots without reading the global marker, READY, current rendered config, or any P2 compatibility fallback. bindingQualifiedWorkspaceLayoutPaths validates the harness-provided 64-hex cache key and derives only the exact cache, DWH snapshot, and READY generation beneath those trusted roots. Neither accepts a destination path from argv/stdin. workspaceRuntimePaths may return P2 paths only when the global marker is absent; when layout v1 is enabled it uses the selected binding-qualified resolver and never falls back because READY is absent.

In harness/tht/dwh_snapshot.py, freeze:

@dataclass(frozen=True)
class DwhArtifactSnapshot:
    generation_id: str
    physical: Path
    analytics_lsh: Path
    analytics_minhashes: Path
    analytics_meta: Path
    manifest_sha256: str


def resolve_revision_dwh_snapshot(cfg: Config) -> DwhArtifactSnapshot: ...
def resolve_effective_dwh_inputs(cfg: Config) -> DwhArtifactSnapshot | None: ...

resolve_revision_dwh_snapshot securely validates the exact ACTIVE, generation directory, manifest, modes/link counts, revision identity, and every declared digest. resolve_effective_dwh_inputs is the only compatibility branch: when paths.dwh_snapshot is absent it returns None, telling existing consumers to use unchanged P2 paths. No caller constructs a snapshot path itself.

Run:

(cd backend && npx vitest run \
  test/workspace-runtime-renderer.test.ts \
  test/workspace-revision-layout.test.ts)
(cd harness && .venv/bin/pytest -q \
  tests/test_config_resources.py tests/test_portable_paths.py tests/test_dwh_snapshot.py)

Expected: RED on missing models/resolver, while every existing P2 rendering assertion stays green.

Step 2: Implement additive models only.

Extend RuntimePaths/PathsConfig with optional memory, corpus, dwh_cache, and dwh_snapshot. Keep P2 fallback semantics exactly for ordinary active rendering. Implement the pure trusted future resolver and synchronous secure state reader, but do not call either from ThtRunner or WorkspaceRuntimeConfigLeaseFactory yet.

Step 3: Run GREEN tests and the P2 regression boundary.

(cd backend && npx vitest run \
  test/workspace-runtime-renderer.test.ts \
  test/workspace-revision-layout.test.ts \
  test/workspace-runtime-config-lease.test.ts \
  test/workspace-preprocessing-service.test.ts \
  test/workspace-maintenance.test.ts && npx tsc --noEmit -p .)
(cd harness && .venv/bin/pytest -q \
  tests/test_config_resources.py tests/test_portable_paths.py tests/test_dwh_snapshot.py \
  tests/test_dwh_preprocess_job.py tests/test_lsh_job_resume.py)

Expected: PASS; session and maintenance still render byte-identical P2 YAML and equal schema-v1 config_dwh_binding().

Step 4: Commit.

git add backend/src/workspaces/runtime-renderer.ts backend/src/workspaces/revision-layout.ts \
  backend/test/workspace-runtime-renderer.test.ts backend/test/workspace-revision-layout.test.ts \
  harness/tht/config.py harness/tht/paths.py harness/tht/dwh_snapshot.py \
  harness/tests/test_config_resources.py harness/tests/test_portable_paths.py \
  harness/tests/test_dwh_snapshot.py
git commit -m "feat: add inactive revision layout models"

Task 4: Add schema-v2 OWNER reading/writing without weakening schema-v1 validation

Files:

  • Create: harness/tht/dwh_owner.py
  • Create: harness/tests/test_dwh_owner_v2.py
  • Modify: harness/tht/jobs/dwh_pipeline.py
  • Modify: harness/tests/test_dwh_preprocess_job.py

Step 1: Write RED owner tests.

Cover new empty binding-key cache schema 2; complete EffectiveDwhBinding; canonical binding_sha256; strict exact keys/modes/UID/nlink; malformed ACTIVE/manifest; v1 compatibility read-only; content-only revision reuse; and mismatch refusal without sibling-key fallback.

(cd harness && .venv/bin/pytest -q tests/test_dwh_owner_v2.py tests/test_dwh_preprocess_job.py)

Expected: RED because v2 owner code does not exist.

Step 2: Implement strict dual readers and a v2-only writer.

class OwnerSchemaV1(BaseModel): ...
class OwnerSchemaV2(BaseModel): ...


def read_owner_at(root_fd: int) -> OwnerSchemaV1 | OwnerSchemaV2: ...
def validate_owner_at(root_fd: int, expected: EffectiveDwhBinding) -> OwnerSchemaV2: ...
def write_owner_v2_at(root_fd: int, binding: EffectiveDwhBinding) -> None: ...

Select only by literal schema_version; never reinterpret v1 as v2. New v2 cache generations bind the complete v2 owner digest. Existing P2 runtime roots remain selected until Task 9 activation.

Step 3: Run focused/regression suites and lint.

(cd harness && .venv/bin/pytest -q \
  tests/test_dwh_owner_v2.py tests/test_dwh_preprocess_job.py tests/test_search_pack.py && \
  .venv/bin/ruff check tht/dwh_owner.py tht/jobs/dwh_pipeline.py \
  tests/test_dwh_owner_v2.py tests/test_dwh_preprocess_job.py)

Expected: PASS, including the untouched valid schema-v1 fixture.

Step 4: Commit.

git add harness/tht/dwh_owner.py harness/tht/jobs/dwh_pipeline.py \
  harness/tests/test_dwh_owner_v2.py harness/tests/test_dwh_preprocess_job.py
git commit -m "feat: version DWH cache ownership"

Task 5: Implement verified schema-v1 migration, prepare-mode DWH builds, and immutable binding snapshots

Files:

  • Create: harness/tht/dwh_migration.py
  • Create: harness/tests/test_dwh_owner_migration.py
  • Modify: harness/tht/dwh_snapshot.py
  • Modify: harness/tht/effective_dwh.py
  • Modify: harness/tht/jobs/dwh_pipeline.py
  • Modify: harness/tht/cli/config_cmd.py
  • Modify: harness/tht/cli/preprocess_cmd.py

Step 1: Write RED migration/lock tests.

Cover verified v1 copy with source preservation; prepare-mode introspection/LSH build when the exact binding-key cache is absent or the v1 binding does not match; v2-only owner/manifests; idempotent full reverification; mismatch/corruption/collision/partial destination refusal; copy/build/fsync/rename fault injection; and exact binding-qualified snapshot layout/digests. Assert the migration source is exactly the legacy <verified-workspace-root>/.tht-dwh and destinations are exactly the cache/snapshot roots returned by futureWorkspaceLayoutPaths, even while active P2 rendering still points at compatibility roots. Reject a config-derived fallback destination and every caller-supplied destination. Require P2's actual inherited writer FD 3 and retained-root FD 4: missing/closed descriptors, another writer/root inode, a cross-root pair, or a forged environment marker without both descriptors, and ordinary direct internal CLI all fail preprocessing_conflict; SIGKILL releases the parent Node lock and leaves no readable partial. Add the complete changed-binding boundary test: READY for revision B/binding X exists; installation binding changes to Y; session admission refuses B/X and activation fails effective_config_mismatch before DWH access, cache/snapshot creation, semantic calls, or READY publication. After a content-only commit C is published and pulled, prepare mode builds and verifies Y's new v2 owner/cache and C/Y binding-qualified snapshot; immutable B/X READY and snapshot remain byte-identical; C/Y READY is later published. While installation binding Y is selected, B admission remains intentionally refused although B/X bytes are immutable. The test then explicitly restores installation binding X and admits B/X, restores Y, and admits C/Y; revision pinning alone is never claimed to restore a historical installation binding.

(cd harness && .venv/bin/pytest -q tests/test_dwh_owner_migration.py)

Expected: RED.

Step 2: Implement migration with the fixed kernel-lock capability.

@dataclass(frozen=True)
class DwhMigrationReport: ...


def migrate_schema_v1_cache(cfg: Config) -> DwhMigrationReport: ...
def prepare_effective_dwh_cache(cfg: Config) -> DwhMigrationReport: ...
def materialize_revision_dwh_snapshot(cfg: Config) -> DwhArtifactSnapshot: ...

Both functions begin with P2's require_workspace_writer_lock(cfg), which validates the retained root directory, opens the canonical lock relative to FD 4, proves that inode is FD 3, and calls fcntl.flock(3, LOCK_EX|LOCK_NB). There is no callback, boolean, path, independently reopened root, or environment-marker seam.

Derive the sole P2 migration source as <verified-workspace-root>/.tht-dwh; never accept a caller path. The backend first performs the fixed read-only effective-binding probe, then acquires maintenance with layoutIntent: "prepare-revision-layout-v1" and binds the returned key through bindingQualifiedWorkspaceLayoutPaths. Python re-derives and asserts the configured cache and snapshot roots equal /preprocessing/dwh-cache/<binding-key> and /revisions/<revision>/dwh-snapshots/<binding-key>/..., never effective_dwh_cache_root under an active P2 fallback. If the exact legacy schema-v1 binding matches, verify every generation/digest and ACTIVE, copy to a temporary sibling of that trusted binding-key cache, write schema-v2 owner/manifests, re-read through strict v2 code, rename, and fsync. Preserve source bytes.

If no exact v2 cache exists and the legacy binding does not match, activation must call prepare_effective_dwh_cache: run the existing DWH introspection and LSH pipeline in explicit prepare-revision-layout-v1 mode, writing only a temporary sibling of that binding-key cache; publish OWNER.json schema v2, generation manifest and ACTIVE in the existing safe order; strictly re-read the complete owner/binding/digests; then rename/fsync the cache. Prepare mode is allowed for an unready commit+binding pair, requires the exact FD 3/FD 4 pair, cannot read or fall back to any old cache, and on failure removes or quarantines only its unpublished temporary sibling while leaving legacy and other binding caches unchanged. Existing exact destinations must fully reverify and return already_prepared; different bytes at the same binding key fail closed.

materialize_revision_dwh_snapshot writes the exact Task 3 binding-qualified generation layout. It copies only the validated cache ACTIVE generation, writes and revalidates snapshot-manifest.json, fsyncs, renames, then publishes that binding snapshot's ACTIVE last. It never symlinks the revision to the cache and never overwrites another binding snapshot.

Add fixed internal tht config migrate-dwh-cache --json -c <config>, tht preprocess dwh --prepare-revision-layout-v1 --json -c <config>, and tht config materialize-dwh-snapshot --json -c <config>. Direct invocation of any mutating operation without the actual inherited writer FD 3 and retained-root FD 4 fails before source inspection or DWH access.

Step 3: Run focused tests and the P2 regression boundary.

(cd harness && .venv/bin/pytest -q \
  tests/test_dwh_owner_migration.py tests/test_dwh_owner_v2.py \
  tests/test_dwh_preprocess_job.py tests/test_search_pack.py tests/test_dwh_snapshot.py && \
  .venv/bin/ruff check tht/dwh_migration.py tht/dwh_snapshot.py tht/effective_dwh.py \
  tht/jobs/dwh_pipeline.py tht/cli/config_cmd.py tht/cli/preprocess_cmd.py \
  tests/test_dwh_owner_migration.py)

Expected: PASS; v1 source and P2 active paths remain byte-identical.

Step 4: Commit.

git add harness/tht/dwh_migration.py harness/tht/dwh_snapshot.py harness/tht/effective_dwh.py \
  harness/tht/jobs/dwh_pipeline.py harness/tht/cli/config_cmd.py \
  harness/tht/cli/preprocess_cmd.py harness/tests/test_dwh_owner_migration.py
git commit -m "feat: migrate legacy DWH caches explicitly"

Task 6: Add revision semantic identities, inventory/readiness preparation, and activation-only publish/delete

Files:

  • Modify: harness/tht/vectorstore/records.py
  • Modify: harness/tht/adapters/vector/qdrant.py
  • Modify: harness/tht/ports/vector.py
  • Modify: harness/tht/adapters/factory.py
  • Modify: harness/tht/search/evidence.py
  • Create: harness/tht/semantic_migration.py
  • Modify: harness/tht/cli/vector_cmd.py
  • Create: harness/tests/test_semantic_revision_migration.py
  • Modify: harness/tests/test_qdrant_vector_store.py
  • Modify: harness/tests/test_semantic_kind_isolation.py
  • Modify: harness/tests/test_qdrant_cli_commands.py
  • Modify: harness/tests/test_corpus_pipeline.py
  • Modify: harness/tests/test_memory_save_one.py
  • Modify: harness/tests/test_solved_question.py

Step 1: Write RED scoped identity/filter tests.

Test every operation: schema/Evidence IDs and filters include exact workspace+40-hex revision; Memory/solved remain workspace-only; mixed searches partition and merge deterministically; caller namespace conflicts fail before network; and revisionless points report migration_required. Runtime selection stays in P2 legacy mode until Task 9 publishes the global layout version and the exact A+binding READY. The public non-activating semantic command may only inventory legacy points and prove canonical schema/Evidence inputs are rebuildable; it must not call Qdrant upsert or delete. Only Task 9 activation may write/verify revision-scoped targets. After global enablement, an unready revision/binding never falls back to P2.

Step 2: Write RED publish-before-delete/resume tests.

Freeze semantic state phases in the P2 PreprocessingStateStore record: legacy_inventory_persisted, semantic_sources_ready, replacement_published, replacement_verified, legacy_delete_complete. The non-activating command can reach only the first two; the last three are activation-only. The inventory contains each exact legacy schema/Evidence point ID and the SHA-256 of canonical bounded payload bytes, plus the inventory digest and source artifact/corpus digests.

Tests must prove:

  • after every successful non-activation command, a newly admitted legacy-mode test session observes no revision-scoped replacement points and Qdrant's upsert/delete call counts remain exactly zero;
  • only activation can invoke schema/Evidence replacement upserts; readback verification completes before any delete call;
  • embed/upsert/verification failure deletes zero legacy IDs;
  • unavailable Evidence canonical source returns migration_required and deletes zero IDs;
  • resume after replacement verification does not re-embed verified points;
  • final deletion addresses only inventory IDs and first re-reads each legacy payload digest;
  • changed/missing legacy payload stops with conflict and does not broaden deletion;
  • crash during deletion resumes the exact remaining set;
  • Memory/solved and other workspaces/revisions are never listed or deleted.
(cd harness && .venv/bin/pytest -q \
  tests/test_qdrant_vector_store.py tests/test_semantic_kind_isolation.py \
  tests/test_qdrant_cli_commands.py tests/test_corpus_pipeline.py \
  tests/test_memory_save_one.py tests/test_solved_question.py \
  tests/test_semantic_revision_migration.py)

Expected: RED.

Step 3: Implement the exact contracts.

def point_id(
    workspace_id: str,
    kind: str,
    record_key: str,
    workspace_revision: str | None = None,
) -> str: ...

@dataclass(frozen=True)
class LegacySemanticPoint:
    point_id: str
    payload_sha256: str
    kind: Literal["schema", "evidence"]


def inventory_legacy_semantic_points(cfg: Config) -> SemanticLegacyInventory: ...
def verify_semantic_rebuild_readiness(cfg: Config, inventory: SemanticLegacyInventory) -> SemanticReadinessReport: ...
def publish_semantic_replacements(cfg: Config, inventory: SemanticLegacyInventory) -> SemanticMigrationReport: ...
def verify_semantic_replacements(cfg: Config, inventory: SemanticLegacyInventory) -> SemanticMigrationReport: ...
def delete_confirmed_legacy_semantic_points(cfg: Config, inventory: SemanticLegacyInventory) -> SemanticMigrationReport: ...

Use UUIDv5 input thothii:<workspace>:<revision>:<semantic-kind>:<record-key> for schema/Evidence and the existing workspace-global form for Memory. Apply identity filters to search, hashes, scroll, list, and delete. Preparation begins by verifying the inherited writer FD 3 and retained-root FD 4 pair. The fixed Python inventory phase returns a bounded exact legacy inventory; WorkspacePreprocessingService.execute persists those exact bytes and digest through PreprocessingStateStore. The fixed readiness phase opens and hashes the verified DWH snapshot and canonical Evidence corpus/source, proves they are complete and rebuildable, and returns only bounded counts/digests. It has no vector-store writer dependency and must be proven incapable of Qdrant upsert/delete. A successful released semantic-revision command stops after persisting semantic_sources_ready, durably completes and owner-clears quiescence while still holding the exclusive reader gate, then admits a test legacy reader that sees exactly the pre-command legacy set.

Only activation may invoke fixed publish and verify phases. With durable quiescence installed and the exclusive reader gate held, they reverify the persisted inventory/readiness bytes, rebuild current-revision schema from the binding-qualified DWH snapshot and Evidence from the canonical corpus/source, then upsert/read back every replacement identity/hash. Only after the backend durably records replacement_verified and publishes/verifies the initial global reader marker may fixed delete-confirmed receive the exact persisted inventory over bounded child stdin, re-read each legacy digest, and delete those IDs. Python never imports or impersonates the TypeScript store, and no phase accepts an arbitrary state path. No phase creates or deletes the collection.

Step 4: Run tests and lint.

(cd harness && .venv/bin/pytest -q \
  tests/test_qdrant_vector_store.py tests/test_semantic_kind_isolation.py \
  tests/test_qdrant_cli_commands.py tests/test_corpus_pipeline.py \
  tests/test_memory_save_one.py tests/test_solved_question.py \
  tests/test_semantic_revision_migration.py && \
  .venv/bin/ruff check tht/vectorstore/records.py tht/adapters/vector/qdrant.py \
  tht/ports/vector.py tht/adapters/factory.py tht/search/evidence.py \
  tht/semantic_migration.py tht/cli/vector_cmd.py \
  tests/test_semantic_revision_migration.py)

Expected: PASS; failure-before-delete assertions observe zero deletion calls.

Step 5: Commit.

git add harness/tht/vectorstore/records.py harness/tht/adapters/vector/qdrant.py \
  harness/tht/ports/vector.py harness/tht/adapters/factory.py harness/tht/search/evidence.py \
  harness/tht/semantic_migration.py harness/tht/cli/vector_cmd.py \
  harness/tests/test_semantic_revision_migration.py \
  harness/tests/test_qdrant_vector_store.py harness/tests/test_semantic_kind_isolation.py \
  harness/tests/test_qdrant_cli_commands.py harness/tests/test_corpus_pipeline.py \
  harness/tests/test_memory_save_one.py harness/tests/test_solved_question.py
git commit -m "feat: migrate semantic state publish before delete"

Task 7: Add the explicit Memory root and migrate one canonical registry without activating it

Files:

  • Create: harness/tht/memory_migration.py
  • Create: harness/tests/test_memory_migration.py
  • Modify: harness/tht/cli/memory_cmd.py
  • Modify: harness/tht/memory.py
  • Modify: harness/tht/solved.py
  • Modify: harness/tests/test_memory_promotion.py
  • Modify: harness/tests/test_memory_save_one.py
  • Modify: harness/tests/test_solved_search_cli.py
  • Modify: harness/tests/test_repository_memory_sql_paths.py

Step 1: Write RED path/migration tests.

Cover all Memory commands; solved global identity; compatibility fallback only when paths.memory is absent; exact known P2/revision candidates; zero/one/equivalent/conflicting registries; unsafe file cases; fault injection; idempotence; source preservation; and rebuild of only Memory/solved projections. Assert every known legacy source separately from the exact future destination <verified-workspace-root>/memory returned by futureWorkspaceLayoutPaths; a P2 paths.artifacts/memory fallback must never become the destination. The P2-rendered active config still lacks explicit paths.memory, so ordinary runtime behavior remains on its legacy root here.

(cd harness && .venv/bin/pytest -q \
  tests/test_memory_migration.py tests/test_memory_promotion.py \
  tests/test_memory_save_one.py tests/test_solved_search_cli.py)

Expected: RED.

Step 2: Centralize future and fallback paths.

def memory_root(cfg: Config) -> Path: ...
def registry_path(cfg: Config) -> Path: ...
class MemoryRegistryLock: ...

Every command uses these helpers. Explicit paths.memory selects the future global root; absence selects exactly the P2 fallback. Do not change Memory IDs or merge semantics.

Step 3: Implement migration with the actual writer FD 3 and retained-root FD 4.

@dataclass(frozen=True)
class MemoryMigrationReport: ...


def migrate_memory_root(cfg: Config) -> MemoryMigrationReport: ...
def rebuild_global_memory_projection(cfg: Config) -> dict[str, int]: ...

Derive source candidates from the verified workspace root, never caller paths. The backend passes a layoutIntent: "prepare-revision-layout-v1" maintenance config derived only from futureWorkspaceLayoutPaths; Python re-derives and asserts that paths.memory is exactly the future workspace-global /memory destination before writing or rebuilding projections. Begin with require_workspace_writer_lock(cfg). Validate/canonicalize every MemoryRecord; conflicting semantic content fails without merge. Publish the future canonical JSONL atomically and reverify, then rebuild only global Memory/solved projections from that exact registry. Add fixed internal tht memory migrate-root --json -c <config>; missing/closed/substituted FD fails.

Step 4: Run tests and lint.

(cd harness && .venv/bin/pytest -q \
  tests/test_memory_migration.py tests/test_memory_promotion.py tests/test_memory_save_one.py \
  tests/test_solved_search_cli.py tests/test_solved_question.py \
  tests/test_repository_memory_sql_paths.py && \
  .venv/bin/ruff check tht/memory_migration.py tht/memory.py tht/solved.py \
  tht/cli/memory_cmd.py tests/test_memory_migration.py)

Expected: PASS; P2 fallback paths remain green.

Step 5: Commit.

git add harness/tht/memory_migration.py harness/tht/memory.py harness/tht/solved.py \
  harness/tht/cli/memory_cmd.py harness/tests/test_memory_migration.py \
  harness/tests/test_memory_promotion.py harness/tests/test_memory_save_one.py \
  harness/tests/test_solved_search_cli.py harness/tests/test_repository_memory_sql_paths.py
git commit -m "feat: migrate workspace-global memory state"

Task 8: Expose registry pull and durable quiesced migrations through the exact P2 one-shot API

Files:

  • Reuse unchanged from accepted P2: backend/src/workspaces/workspace-fs-at.ts
  • Reuse unchanged from accepted P2: backend/src/native/workspace-fs-at-binding.d.ts
  • Re-run unchanged P2 ownership test: backend/test/workspace-fs-at-native.test.ts
  • Reuse unchanged from accepted P2: backend/src/workspaces/workspace-lock-root-lease.ts
  • Re-run unchanged P2 ownership tests: backend/test/workspace-lock-root-lease.test.ts, backend/test/workspace-session-readers-lock.test.ts
  • Modify only for the P3 child-request union (not reader locking): backend/src/workspaces/preprocessing-state.ts
  • Modify: backend/src/workspaces/preprocessing-service.ts
  • Modify: backend/src/workspace-maintenance.ts
  • Modify: backend/src/workspaces/registry.ts
  • Modify: backend/src/workspaces/registry-publication.ts
  • Create: backend/src/workspaces/registry-pull-job.ts
  • Create: backend/test/registry-pull-job-imports.compile.ts
  • Modify: backend/test/workspace-registry.test.ts
  • Modify: backend/test/workspace-registry-addressed-publication.test.ts
  • Modify: backend/test/workspace-registry-addressed-process.test.ts
  • Modify: backend/test/fixtures/workspace-lock-root-worker.mjs
  • Modify: backend/test/fixtures/workspace-registry-addressed-worker.mjs
  • Create: backend/src/workspaces/workspace-reader-lease.ts
  • Modify: backend/src/routes/workspaces.ts
  • Modify: backend/src/routes/sessions.ts
  • Modify: backend/src/app.ts
  • Modify: backend/test/app.test.ts
  • Modify: backend/src/pi/pi-process-manager.ts
  • Modify: backend/test/workspace-preprocessing-state.test.ts
  • Modify: backend/test/workspace-preprocessing-service.test.ts
  • Modify: backend/test/workspace-maintenance.test.ts
  • Create: backend/test/workspace-reader-lease.test.ts
  • Modify: backend/test/routes-workspaces.test.ts
  • Modify: backend/test/routes-sessions.test.ts
  • Modify: backend/test/pi-process-manager.test.ts
  • Create: harness/tht/locked_child_stdin.py
  • Create: harness/tht/layout_markers.py
  • Modify: harness/tht/cli/config_cmd.py
  • Modify: harness/tht/cli/vector_cmd.py
  • Modify: harness/tht/semantic_migration.py
  • Create: harness/tests/test_locked_child_stdin.py
  • Create: harness/tests/test_layout_marker_commands.py
  • Create: harness/tests/test_p3_internal_cli.py
  • Modify: harness/tests/test_semantic_revision_migration.py
  • Modify: harness/tests/test_qdrant_cli_commands.py
  • Modify: tools/thothctl/internal/workspaceops/operations.go
  • Modify: tools/thothctl/internal/workspaceops/operations_test.go
  • Modify: tools/thothctl/cmd/thothctl/main.go
  • Modify: tools/thothctl/cmd/thothctl/main_test.go
  • Modify: deploy/compose.git-https.yaml
  • Modify: deploy/compose.git-ssh.yaml
  • Modify: scripts/generate-connector-secrets-override.sh
  • Modify: scripts/test-preprocess-compose-config.sh

Step 1: Write RED public-command, exact-P2 registry, route, provisioning, and compile-import tests.

Add these released public commands, with no aliases:

thothctl --installation <abs> workspace registry pull
  --workspace <id> [--resume <32-hex-outer-run-id>] [--json]
thothctl --installation <abs> workspace migrate dwh-cache
  --workspace <id> [--resume <32-hex-outer-run-id>] [--json]
thothctl --installation <abs> workspace migrate memory
  --workspace <id> [--resume <32-hex-outer-run-id>] [--json]
thothctl --installation <abs> workspace migrate semantic-revision
  --workspace <id> --yes [--resume <32-hex-outer-run-id>] [--json]

Before a fresh call launches Compose, thothctl obtains exactly 16 bytes from crypto/rand, formats one 32-lowercase-hex outer run ID, builds the canonical request/digest below, and retains that ID in every result or error. For these four released pull/migration commands, --resume uses exactly the caller-supplied ID; no newest-run, digest, marker, or automatic selection exists. P2's separate empty-registry inspect/lazy-list/status bootstrap path remains the sole automatic-recovery exception via ensureBootstrapAddressed. The four ordinary operations reject wrong workspace, operation, active revision/descriptor, selected binding, or request digest before mutation. registry_pull is different: the outer request addresses the P2 job, and all base/target authority comes from the exact durable P2 registry_pull state. An ambiguous Compose exit/timeout/truncated result returns the known ID and exact --resume instruction; it never allocates a replacement ID.

Freeze RegistryPullCommand and operation literal registry_pull. It alone receives writable registry storage plus the selected HTTPS/SSH Git transport capability and calls only WorkspaceRegistry.publishAddressed. It never acquires the selected workspace writer first. Every ordinary migration gets a read-only registry and no Git credential. Rendered Compose tests inspect mounts, environment, profile, image ID, and command; inspect cannot pull an existing registry.

The RED registry matrix must consume P2's exact request/plan/state/callback surface rather than shadow it. Test both registry_bootstrap (no base; changed set is all target IDs) and registry_pull (exact base; changed set is the lexical symmetric base/target identity difference). Test every phase in order: request_claimed, target_advertised, target_fetched, planned, participants_prepared, publication_intent_durable, target_published, terminal_durable. Test the exact addressed-publication-jobs/<run-id>.json path and reject the removed pull-only job-directory spelling. Require repository lock before the claim/network/pin sequence; acquireOrProvision for every complete changed ID while the repository lock is held; one call to runUnderOrderedWorkspaceWriterLocks; the same callback-scoped capability set in the production lifecycle owner and every synchronizer; the same addressed lease objects in participants; no reacquisition; and all-or-nothing pointer publication.

Root tests include newly added and never-used removed IDs with absent leaves, concurrent creators, symlink substitution, wrong owner/mode, parent replacement, failure before publication, retained unused leaf, and successful same-ID retry. Re-run P2's unchanged native seam and workspace-session-readers-lock.test.ts production ownership tests: exact LockFileName = "writer.lock" | "session-readers.lock", wrapper-only typed open/flock ownership, VerifiedWorkspaceLockRootLease.acquireSessionReadersShared(), exact WorkspaceSessionReadersLockLease transfer/close, and WorkspaceWriterLockCapability.runUnderSessionReadersExclusive(...). P3 RED tests compile the adapter against those exact types, spy that each adapter method calls only its matching P2 method, and exercise real contention through the production P2 methods rather than a shadow opener. Compile/source fences reject any P3 WorkspaceFsAtV1 dependency for reader acquisition, raw-addon/fs-ext import, direct open/flock, directory handle, path, numeric FD, lock name/flags, cast, wrapper lease, public constructor, or second lock factory.

Route/app tests prove inspect, lazy list bootstrap, and status call only ensureBootstrapAddressed; explicit pull and author-publication activation call only publishAddressed. Across every bootstrap preterminal kill point, each actual product caller automatically resumes the sole exact-identity nonterminal run under continuously held repository.lock; zero nonterminal jobs creates one fresh run. Add a two-/three-process barrier test in which inspect, lazy list, and status all observe initial absence: the first locked caller publishes exactly one bootstrap, while queued callers revalidate and receive the same exact P2 already_active snapshot result without scanning zero into another create, state writes, or network. Ordinary calls made after active state exists take the same branch. Corrupt/incompatible active state and corrupt/mismatched/pull/multiple nonterminals return the exact fail-closed code before network/state and never choose newest/mtime/lexical/OID. Each route/service adapter consumes P2's exact RegistryEnsureBootstrapAddressedResultV1 rather than assuming a terminal bootstrap result, and compile imports pin its RegistryActiveSnapshotV1 snapshot. The removed public/internal bootstrap, pull, activate, and direct active-pointer writer are absent. Session routes use canonicalInput(id) plus existing-leaf acquire, never provisioning. Application construction injects the sole P2 WorkspaceFsAtV1 into the installation-bound root factory and production lifecycle owner once; WorkspaceReaderLeaseFactory receives neither that wrapper nor any filesystem operand.

For the actual production pull call, add compile assertions on both literal branches and runtime spies through the released create and resume commands. Create must pass the validated boundary's installationIdentitySha256, repositoryIdentitySha256, remoteRefIdentitySha256, and expectedBaseCommit; resume must pass all three identity digests and omit only expectedBaseCommit. Omission, substitution, cross-installation/repository/ref reuse, and create-base mismatch must fail before the first advertisement/network call and before addressed state creation or transition. Assert zero calls to the network and state-mutation spies, and prove the host request cannot supply or override any of these four values.

Create backend/test/registry-pull-job-imports.compile.ts with one import type declaration from the single exact pull-job module below and references to all six released names. In that same compile gate, import P2's exact LockFileName from workspace-fs-at.js and exact RegistryActiveSnapshotV1/RegistryEnsureBootstrapAddressedResultV1 from registry-publication.js; no alias or alternate module path is permitted:

import type {
  RegistryPullPublicJobRequestV1,
  RegistryPullPhaseV1,
  RegistryPullAddressedJobRequestV1,
  RegistryPullParticipantStateV1,
  RegistryPullSynchronizerStateV1,
  RegistryPullJobStateV1,
} from "../src/workspaces/registry-pull-job.js";
import type { LockFileName } from "../src/workspaces/workspace-fs-at.js";
import type {
  Revision40,
  Sha256Hex,
} from "../src/workspaces/workspace-lock-root-lease.js";
import type {
  RegistryActiveSnapshotV1,
  RegistryAddressedPublicationPhaseV1,
  RegistryAddressedRequestV1,
  RegistryEnsureBootstrapAddressedResultV1,
  RegistryPullAddressedPublicationStateV1,
} from "../src/workspaces/registry-publication.js";

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
    ? (<T>() => T extends B ? 1 : 2) extends (<T>() => T extends A ? 1 : 2)
      ? true
      : false
    : false;
type Assert<T extends true> = T;
type AllRegistryPullExports = readonly [
  RegistryPullPublicJobRequestV1,
  RegistryPullPhaseV1,
  RegistryPullAddressedJobRequestV1,
  RegistryPullParticipantStateV1,
  RegistryPullSynchronizerStateV1,
  RegistryPullJobStateV1,
];
type RegistryPullCreateRequestV1 = Extract<
  RegistryPullAddressedJobRequestV1,
  { readonly mode: "create" }
>;
type RegistryPullResumeRequestV1 = Extract<
  RegistryPullAddressedJobRequestV1,
  { readonly mode: "resume" }
>;
type ExactP2HandoffSymbols = readonly [
  Assert<Equal<LockFileName, "writer.lock" | "session-readers.lock">>,
  RegistryActiveSnapshotV1,
  RegistryEnsureBootstrapAddressedResultV1,
  Assert<Equal<
    Extract<RegistryEnsureBootstrapAddressedResultV1, { readonly kind: "already_active" }>["snapshot"],
    RegistryActiveSnapshotV1
  >>,
];
type ExactP2Parity = readonly [
  Assert<Equal<RegistryPullPhaseV1, RegistryAddressedPublicationPhaseV1>>,
  Assert<Equal<
    RegistryPullAddressedJobRequestV1,
    Extract<RegistryAddressedRequestV1, { readonly operation: "registry_pull" }>
  >>,
  Assert<Equal<
    Pick<
      RegistryPullCreateRequestV1,
      | "installationIdentitySha256"
      | "repositoryIdentitySha256"
      | "remoteRefIdentitySha256"
      | "expectedBaseCommit"
    >,
    {
      readonly installationIdentitySha256: Sha256Hex;
      readonly repositoryIdentitySha256: Sha256Hex;
      readonly remoteRefIdentitySha256: Sha256Hex;
      readonly expectedBaseCommit: Revision40;
    }
  >>,
  Assert<Equal<
    Pick<
      RegistryPullResumeRequestV1,
      | "installationIdentitySha256"
      | "repositoryIdentitySha256"
      | "remoteRefIdentitySha256"
    >,
    {
      readonly installationIdentitySha256: Sha256Hex;
      readonly repositoryIdentitySha256: Sha256Hex;
      readonly remoteRefIdentitySha256: Sha256Hex;
    }
  >>,
  Assert<Equal<Extract<keyof RegistryPullResumeRequestV1, "expectedBaseCommit">, never>>,
  Assert<Equal<RegistryPullJobStateV1, RegistryPullAddressedPublicationStateV1>>,
];
export type { AllRegistryPullExports, ExactP2HandoffSymbols, ExactP2Parity };

Run:

(cd tools/thothctl && test "$(go env GOVERSION)" = "go1.26.5" && \
  go test ./internal/workspaceops ./cmd/thothctl \
    -run 'Workspace|RegistryPull|Migrate|Resume|Capability|DedicatedJob' -v)
(cd backend && npx vitest run \
  test/workspace-lock-root-lease.test.ts test/workspace-session-readers-lock.test.ts \
  test/workspace-preprocessing-state.test.ts \
  test/workspace-preprocessing-service.test.ts test/workspace-maintenance.test.ts \
  test/workspace-registry.test.ts test/workspace-registry-addressed-publication.test.ts \
  test/workspace-registry-addressed-process.test.ts test/workspace-reader-lease.test.ts \
  test/routes-workspaces.test.ts test/routes-sessions.test.ts test/pi-process-manager.test.ts)
(cd backend && npx tsc --noEmit --target ES2022 --module ES2022 \
  --moduleResolution Bundler --strict --skipLibCheck \
  test/registry-pull-job-imports.compile.ts)

Expected: RED on new command members, reader lifecycle, P3 callback participants, root-provisioning coverage, route regression, and the not-yet-created exact export module; existing P2 names themselves compile unchanged.

Step 2: Export one exact RegistryPull type surface without forking P2 state.

Create exactly backend/src/workspaces/registry-pull-job.ts. It is the sole module that exports the six RegistryPull* job names consumed by P4-P6. It imports P2's brands and addressed types; it declares no second workspace/revision/SHA/run-ID brand and no second durable job shape:

// backend/src/workspaces/registry-pull-job.ts
import type {
  CanonicalWorkspaceId,
  Sha256Hex,
} from "./workspace-lock-root-lease.js";
import type {
  RegistryAddressedPublicationPhaseV1,
  RegistryAddressedRequestV1,
  RegistryPullAddressedPublicationStateV1,
  RegistryRunId32,
} from "./registry-publication.js";

export interface RegistryPullPublicJobRequestV1 {
  readonly mode: "create" | "resume";
  readonly operation: "registry_pull";
  readonly requestSha256: Sha256Hex;
  readonly runId: RegistryRunId32;
  readonly schemaVersion: 1;
  readonly workspaceId: CanonicalWorkspaceId; // result selector, never the changed set
}
export type RegistryPullPhaseV1 = RegistryAddressedPublicationPhaseV1;
export type RegistryPullAddressedJobRequestV1 = Extract<
  RegistryAddressedRequestV1,
  { readonly operation: "registry_pull" }
>;
export interface RegistryPullParticipantStateV1 {
  readonly workspaceId: CanonicalWorkspaceId;
  readonly baseWorkspaceIdentitySha256: Sha256Hex | null;
  readonly targetWorkspaceIdentitySha256: Sha256Hex | null;
  readonly preparedSha256: Sha256Hex | null;
}
export interface RegistryPullSynchronizerStateV1 {
  readonly synchronizerId: string;
  readonly preparedSha256: Sha256Hex | null;
}
export type RegistryPullJobStateV1 = RegistryPullAddressedPublicationStateV1;

RegistryPullPhaseV1, RegistryPullAddressedJobRequestV1, and RegistryPullJobStateV1 are deliberate exact aliases to P2's released types, not parallel brands or persisted wrappers. Thus the pull state is field-for-field P2: jobArtifactPath; operation/base fields; installationIdentitySha256, repositoryIdentitySha256, and remoteRefIdentitySha256; nullable advertisedTargetCommit, immutableTargetRef, fetchedTargetCommit, target/changed-plan fields and digests before their phases; and all publication, terminal, and prior-state digests. RegistryPullParticipantStateV1 and RegistryPullSynchronizerStateV1 are bounded callback receipts used to compute P2's aggregate digests; they never create another job artifact or add fields to RegistryPullJobStateV1.

The compile-import test is a release gate and its bidirectional Equal assertions are the exact P2 symbol/field parity fence. Add an AST/source-boundary test that all six names have one exporting module, that consumers import that exact .js path, and that none is redeclared in preprocessing-state.ts, registry-publication.ts, or P4-P6. No barrel, compatibility export, or P5/P6 alias is permitted.

The public process request remains canonical compact JSON plus LF. requestSha256 hashes exactly {"operation":"...","runId":"...","schemaVersion":1,"workspaceId":"..."}\n; the stdin object has exactly mode,operation,requestSha256,runId,schemaVersion,workspaceId in lexicographic order and is at most 4,096 bytes. Go and TypeScript share golden vectors. Reject duplicate/reordered/unknown keys, BOM, noncanonical whitespace/escaping, trailing bytes, invalid UTF-8, bad identities, and oversize input before state access.

The state locations are exactly:

/data/sessions/<workspace-id>/preprocessing/
  writer.lock
  session-readers.lock
  maintenance-quiescence.json
  jobs/<32-hex-run-id>.json                    # four ordinary operations only
<registry-root>/
  repository.lock
  addressed-publication-jobs/<32-hex-run-id>.json  # exact P2 addressed state
/data/sessions/<changed-id>/preprocessing/
  maintenance-quiescence.json                 # addressed callback owner
  session-readers.lock

Do not create a pull-only job directory, a P3 registry state directory, or another active-pointer writer.

Step 3: Implement writer-first ordinary ownership and consume P2's repository-first callback.

thothctl invokes no backend transport. Fresh and resume commands launch only P2's selected, immutable-image-pinned, profile-gated dedicated job:

docker compose ... run --rm --no-deps --no-TTY --name thoth-workspace-maintenance-<run-id> workspace-maintenance --request-json-stdin

The job is runnable with core absent. No path uses compose exec core, curl/HTTP/internal routes, Fastify, frontend, host Python/Node/Pi/tht, or a core stop/start command.

The four ordinary operations retain this exact order:

VerifiedWorkspaceLockRootLeaseFactory.acquire(existing canonical selected ID)
-> transfer into runUnderWorkspaceWriterLock
-> exact WorkspaceWriterLockCapability callback (writer FD 3 + retained root FD 4)
-> ordinary job create/replay, owner quiescence publication, and reader drain
-> WorkspaceReaderLeaseFactory.acquireForMaintenance(writerCapability, full quiesced callback)
-> P2 runUnderSessionReadersExclusive holds LOCK_EX across final recheck, children, and publication
-> terminal durability and owner clear inside that same callback
-> callback settlement invalidates/closes reader borrow, then writer/root close

They must neither call acquireOrProvision nor enter publishAddressed. Implement WorkspaceReaderLeaseFactory as a zero-filesystem adapter over P2's exact owner methods—without a P3 verified-root alias or wrapper lease:

import type {
  VerifiedWorkspaceLockRootLease,
  WorkspaceSessionReadersLockLease,
} from "./workspace-lock-root-lease.js";
import type {
  BorrowedWorkspaceSessionReadersExclusiveLockLease,
  WorkspaceWriterLockCapability,
} from "./preprocessing-state.js";

export class WorkspaceReaderLeaseFactory {
  acquireForSession(
    rootLease: VerifiedWorkspaceLockRootLease,
  ): Promise<WorkspaceSessionReadersLockLease> {
    return rootLease.acquireSessionReadersShared();
  }

  acquireForMaintenance<T>(
    writerCapability: WorkspaceWriterLockCapability,
    action: (lease: BorrowedWorkspaceSessionReadersExclusiveLockLease) => Promise<T>,
  ): Promise<T> {
    return writerCapability.runUnderSessionReadersExclusive(action);
  }
}

There is no P3 WorkspaceReaderLease, release() wrapper, constructor, lock opener, or copied lifetime logic. acquireForSession calls only P2's consuming acquireSessionReadersShared() and returns its exact WorkspaceSessionReadersLockLease. acquireForMaintenance returns the exact result of writerCapability.runUnderSessionReadersExclusive(action); it never returns an owned maintenance lease, and the exact BorrowedWorkspaceSessionReadersExclusiveLockLease may exist only inside action. WorkspaceReaderLeaseFactory has no constructor dependency and never imports or receives WorkspaceFsAtV1, a directory/regular-file handle, path, FD, lock name, flags, or mode. P2 alone opens, flocks, invalidates, and closes the lock.

The session route calls only canonicalInput(id) then existing-leaf acquire(input), checks quiescence, calls acquireForSession(rootLease), and rechecks quiescence while holding that exact P2 shared owner. RuntimeOptions.sessionReadersLease is typed as WorkspaceSessionReadersLockLease. PiProcessManager synchronously calls transfer() at the ownership handoff and from then on closes its owned lease exactly once only after every Pi/session child and every stdout/stderr/read stream has settled. The route closes an untransferred owner on every pre-handoff failure; after transfer its source close is the P2 idempotent no-op and the manager owns cleanup across configure/start failure, exit, close, replacement, explicit teardown, shutdown, cancellation, and stream error.

For ordinary maintenance, publish the owner-qualified quiescence marker and drain existing readers first, then call acquireForMaintenance(writerCapability, async (readerBorrow) => { ... }) exactly once. That callback contains the complete quiesced action: final drain/admission-state recheck, every locked child/participant, publication, terminal durability, and matching owner clear. It calls readerBorrow.assertLive() at its protected boundaries and settles only after child stdout/stderr collection settles. The P2 exclusive owner remains held for the callback's resolve/reject lifetime; the borrow invalidates before return, and no existing reader is killed.

Adapter tests use compile-time exact P2 imports and runtime spies to prove one call to each matching P2 method, exact returned/result identity, no other call, and no maintenance-borrow escape. Production tests reuse workspace-session-readers-lock.test.ts: acquire a real shared owner, transfer it into a fixture PiProcessManager, keep a child plus stdout/stderr drains open, and prove the production exclusive callback contends until all child/stream teardown and exactly-once close. Conversely, hold the production exclusive callback around the full quiesced action and prove new shared admission contends, captured borrows fail after settlement, callback failure closes once, and release permits admission. Source/type fences reject a direct wrapper/raw-addon/fs-ext import, directory or file handle, path, numeric FD, lock name/flags/mode, cast, direct open/flock, path fallback, wrapper lease, or alternate factory.

registry_pull has no selected-workspace wrapper. Its production path first enters the validated installation/registry boundary and obtains one immutable identity tuple. The boundary derives and revalidates all four values against the retained installation descriptor, owned registry volume, canonical repository, configured remote/ref, and current active snapshot; the host request cannot supply or override any of them:

const {
  installationIdentitySha256,
  repositoryIdentitySha256,
  remoteRefIdentitySha256,
  expectedBaseCommit,
} = await validatedInstallationRegistryBoundary.deriveRegistryPullIdentity();

const result = await registry.publishAddressed(
  request.mode === "create"
    ? ({
        mode: "create",
        operation: "registry_pull",
        runId: request.runId,
        requestSha256: request.requestSha256,
        installationIdentitySha256,
        repositoryIdentitySha256,
        remoteRefIdentitySha256,
        expectedBaseCommit,
      } satisfies Extract<
        RegistryPullAddressedJobRequestV1,
        { readonly mode: "create" }
      >)
    : ({
        mode: "resume",
        operation: "registry_pull",
        runId: request.runId,
        requestSha256: request.requestSha256,
        installationIdentitySha256,
        repositoryIdentitySha256,
        remoteRefIdentitySha256,
      } satisfies Extract<
        RegistryPullAddressedJobRequestV1,
        { readonly mode: "resume" }
      >),
);

These are the actual production call literals, not test-only examples: their two satisfies clauses are compile gates against P2's exact request union, while runtime command/service tests spy on this call for both modes. A create base mismatch, or any create/resume installation/repository/remote identity mismatch with validated active state or a durable addressed record, fails before creating/transitioning addressed state and before advertisement or other network. Do not redeclare P2's callback. Consume it exactly: CapabilityAwareRegistryPublicationLifecycleOwner.run({ plan, capabilities, participants, synchronizers, action }). Its plan is the P2 RegistryAddressedPlanV1 union, so P3 participants and synchronizers handle both RegistryBootstrapAddressedPlanV1 and RegistryPullAddressedPlanV1 by discriminating plan.operation. Each participant receives exactly AddressedWorkspacePublicationLeaseV1, including P2's borrowed retained root, writer capability, quiescence, and reader lease. Each synchronizer receives the exact same OrderedWorkspaceWriterCapabilitySet. No callback calls root acquisition, writer-lock acquisition, reader acquisition, registry publication, or a nested lifecycle owner.

For both addressed variants, P2 holds repository.lock, computes the exact complete changed IDs, calls acquireOrProvision(canonicalInput(id)) for each ID, and passes the full root array once to runUnderOrderedWorkspaceWriterLocks. Missing added and never-used removed leaves are securely provisioned under the retained parent FD with P2's fixed UID/mode/fsync/identity rules and retained on failure. The factory consumes only P2's WorkspaceFsAtV1 wrapper over backend/native/workspace-fs-at/workspace_fs_at.cc; literal openat/mkdirat/no-follow fstatat, directory fsync, owned close, the closed "writer.lock" | "session-readers.lock" open surface, typed flock modes, flags, errors, Node 22 build, and Darwin/Linux behavior remain the exact P2 contract. The wrapper's module-private synchronous numeric borrow is the only bridge to exact fs-ext@2.1.1 and is used internally for typed flock; P3 cannot import either underlying module, accept/return/borrow/cast an FD, cast an owned handle/name, accept flags, implement another mkdir/open/flock factory, or introduce a path fallback. Bootstrap requires no active base, null base fields, [] base workspaces, and all target IDs. Pull requires the exact active base and lexical symmetric base/target workspace-identity difference. Empty changed sets still use the same lifecycle and publish/terminal protocol without inventing a selected lock.

Product bootstrap recovery is automatic rather than a new public selector. Inspect, lazy list, and status call only ensureBootstrapAddressed(identity) and consume P2's exact discriminated result union. Under one continuously held repository.lock, the selector first reads and validates active state. A valid compatible snapshot returns the exact already_active branch with that snapshot and performs no job scan, state create/transition, or network; corrupt/incompatible active state fails closed. Only validated absence no-follow scans one lexically sorted addressed-publication-jobs/ snapshot with P2's exact 4,096-entry, 1,048,576-byte-per-artifact, and 67,108,864-byte-total bounds and validates every record before selection. Zero nonterminals creates one fresh ID; exactly one matching registry_bootstrap nonterminal resumes that exact ID before any network. Unknown/corrupt/churning entries, identity mismatch, nonterminal pull, or multiple nonterminals fail registry_bootstrap_recovery_conflict; terminal jobs are ignored for automatic selection, and no mtime/newest/lexical-last/OID/remote-head heuristic exists. A queued inspect/list/status caller never acts on its pre-lock observation: after acquiring the lock it returns the new already_active snapshot published by the winning caller. The bootstrap request digest excludes run ID/mode and binds schema, operation, installation, repository, and remote-ref identity exactly.

Fresh addressed create durably writes request_claimed before ls-remote, fetch, or any network call. Then it advertises once and records the OID at target_advertised; exact-OID fetch creates only refs/thoth/addressed-runs/<run-id>/target, verifies it equals the advertisement, and records target_fetched; only then may manifest reading and the full plan produce planned. All writes use the exact P2 sibling-write/file-fsync/rename/parent-fsync state transition and priorStateSha256 chain.

Same-ID recovery is split at the durable pin and tested literally:

  1. Before request_claimed rename is durable, no network was allowed; the same request may recreate that claim.
  2. From durable request_claimed until target_advertised is durable, no target was promised; resume repeats advertisement and may observe a newer OID.
  3. At/after durable target_advertised, that OID is permanent. If the immutable run ref is absent, resume retries fetch of only that exact OID; if it already resolves to that OID, resume performs no network and advances; a different OID is corruption.
  4. At/after target_fetched, resume never advertises, fetches, consults remote target selection, or accepts another OID. A terminal job replays its stored result even if a later independent pull moved remote-tracking refs. Drift checks apply only while reconciling a nonterminal run.

After planned, participant preparation and aggregate digests precede publication_intent_durable. Publication stages immutable target records, atomically renames and parent-fsyncs the installation-wide active pointer, rereads exact bytes, advances target_published, and persists terminal_durable before callback settlement. Before pointer rename, active is exact base (or absent for bootstrap). At/after it, same-ID resume accepts only exact recorded base/target projections, converges all-base or mixed to target, recognizes all-target lost acknowledgement, and refuses every third identity. It reuses the same full callback capability set. Owner markers clear only after terminal durability while readers remain exclusive; then callback borrows are invalidated and readers/quiescence/writers/roots release in reverse lexical order before repository release.

Kill/process tests cover before claim rename, after claim fsync, during/after advertisement before its durable record, after advertisement fsync, during/after exact fetch, after immutable-ref creation before target_fetched, after its fsync, after planned, before/after pointer file fsync/rename/parent fsync, after target reread before phase persistence, and after terminal before clear. Assert exact same-ID behavior at every boundary, no post-pin target reselection, no post-target_fetched network, terminal replay despite a later tracking-ref move, and deterministic base/target/mixed recovery. Repeat every bootstrap preterminal boundary through actual inspect, lazy-list, and status callers and prove automatic same-ID selection occurs before network; exercise zero/one/multiple/pull/mismatched/corrupt/churning job sets and all three P2 scan bounds. Repository/ref/installation identity change, a missing/changed pinned object, third active identity, wrong owner/digest, unsafe state, or nonterminal clear fails closed without publication, and automatic conflicts preserve exact registry_bootstrap_recovery_conflict.

Step 4: Extend P2's closed locked-child union in place and implement the one-shot lifecycle.

In backend/src/workspaces/preprocessing-state.ts, preserve P2's three variants and extend the same exported alias—never create a parallel spawner—with these exact P3 contracts:

type P3LockedOperation =
  | "p3_migrate_dwh_cache" | "p3_prepare_dwh_cache" | "p3_materialize_dwh_snapshot"
  | "p3_migrate_memory_root" | "p3_rebuild_memory_projection"
  | "p3_inventory_semantic_legacy" | "p3_check_semantic_readiness"
  | "p3_publish_semantic_replacements" | "p3_verify_semantic_replacements"
  | "p3_delete_confirmed_semantic_legacy" | "p3_prepare_layout_markers"
  | "p3_publish_layout_version" | "p3_publish_revision_ready"
  | "p3_verify_revision_readiness";
import type {
  CanonicalWorkspaceId,
  Revision40,
  Sha256Hex,
  WorkspaceLockRootIdentityV1,
} from "./workspace-lock-root-lease.js";
interface P3LockedChildContextV1 {
  schemaVersion: 1;
  workspaceId: CanonicalWorkspaceId;
  rootIdentity: WorkspaceLockRootIdentityV1; // from the consumed P2 lease; never argv/stdin
  workspaceRevision: Revision40;
  descriptorBlob: Revision40;
  effectiveDwhBindingSha256: Sha256Hex;
  effectiveDwhCacheKey: Sha256Hex;
  outerRunId: string;                    // exactly 32 lowercase hex
  configLease: MaintenanceRuntimeConfigLease; // opaque owned lease, not a caller path
}
interface P3LockedChildStdinV1 {
  artifactKind: "semantic-legacy-inventory-v1" | "revision-ready-input-v1";
  contentBase64: string;
  contentByteLength: number;
  contentSha256: Sha256Hex;
  descriptorBlob: Revision40;
  effectiveDwhBindingSha256: Sha256Hex;
  effectiveDwhCacheKey: Sha256Hex;
  outerRunId: string; // exactly 32 lowercase hex
  schemaVersion: 1;
  stateArtifact: "semantic-legacy-inventory.json" | "revision-ready-input.json";
  workspaceId: CanonicalWorkspaceId;
  workspaceRevision: Revision40;
}
type P3LockedChildRequest =
  | { kind: "p3_migrate_dwh_cache"; context: P3LockedChildContextV1 }
  | { kind: "p3_prepare_dwh_cache"; context: P3LockedChildContextV1 }
  | { kind: "p3_materialize_dwh_snapshot"; context: P3LockedChildContextV1 }
  | { kind: "p3_migrate_memory_root"; context: P3LockedChildContextV1 }
  | { kind: "p3_rebuild_memory_projection"; context: P3LockedChildContextV1 }
  | { kind: "p3_inventory_semantic_legacy"; context: P3LockedChildContextV1 }
  | { kind: "p3_check_semantic_readiness"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_publish_semantic_replacements"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_verify_semantic_replacements"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_delete_confirmed_semantic_legacy"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_prepare_layout_markers"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_publish_layout_version"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_publish_revision_ready"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 }
  | { kind: "p3_verify_revision_readiness"; context: P3LockedChildContextV1; stdin: P3LockedChildStdinV1 };
export type WorkspaceLockedChildRequest =
  | DwhLockedChildRequest | SchemaLockedChildRequest | EvidenceLockedChildRequest
  | P3LockedChildRequest;
interface P3ResultBaseV1 {
  schemaVersion: 1; workspaceId: CanonicalWorkspaceId; workspaceRevision: Revision40;
  effectiveDwhCacheKey: Sha256Hex; status: "succeeded" | "unchanged" | "not_ready";
}
type P3LockedChildParsedResult =
  | (P3ResultBaseV1 & { operation: "p3_migrate_dwh_cache" | "p3_prepare_dwh_cache";
      ownerSha256: Sha256Hex; generationId: string; generationManifestSha256: Sha256Hex })
  | (P3ResultBaseV1 & { operation: "p3_materialize_dwh_snapshot";
      snapshotGenerationId: string; snapshotManifestSha256: Sha256Hex })
  | (P3ResultBaseV1 & { operation: "p3_migrate_memory_root";
      registrySha256: Sha256Hex; recordCount: number })
  | (P3ResultBaseV1 & { operation: "p3_rebuild_memory_projection";
      registrySha256: Sha256Hex; memoryPointCount: number; solvedPointCount: number })
  | (P3ResultBaseV1 & { operation: "p3_inventory_semantic_legacy";
      inventorySha256: Sha256Hex; inventoryByteLength: number; pointCount: number;
      artifactKind: "semantic-legacy-inventory-v1"; contentBase64: string })
  | (P3ResultBaseV1 & { operation: "p3_check_semantic_readiness";
      inventorySha256: Sha256Hex; schemaSourceSha256: Sha256Hex;
      evidenceSourceSha256: Sha256Hex; schemaCount: number; evidenceCount: number })
  | (P3ResultBaseV1 & { operation: "p3_publish_semantic_replacements" |
      "p3_verify_semantic_replacements" | "p3_delete_confirmed_semantic_legacy";
      inventorySha256: Sha256Hex; replacementSetSha256: Sha256Hex;
      schemaCount: number; evidenceCount: number; deletedLegacyCount: number })
  | (P3ResultBaseV1 & { operation: "p3_prepare_layout_markers";
      layoutMarkerInputSha256: Sha256Hex; readyInputSha256: Sha256Hex })
  | (P3ResultBaseV1 & { operation: "p3_publish_layout_version";
      layoutMarkerInputSha256: Sha256Hex; layoutMarkerSha256: Sha256Hex })
  | (P3ResultBaseV1 & { operation: "p3_publish_revision_ready";
      readyInputSha256: Sha256Hex; readyManifestSha256: Sha256Hex })
  | (P3ResultBaseV1 & { operation: "p3_verify_revision_readiness";
      readyInputSha256: Sha256Hex; readyManifestSha256: Sha256Hex });

Only semantic operations accept semantic-legacy-inventory-v1; all four marker/readiness variants accept only revision-ready-input-v1. Marker preparation returns canonical bounded layout/READY input digests but writes neither marker. The coordinator then calls the two exact capability variants in transaction order: p3_publish_layout_version before confirmed legacy delete and p3_publish_revision_ready after deletion. Both publishers mutate only through inherited FD 4 while FD 3 remains verified/held; readiness verification rereads the exact published bytes. Node never publishes either marker by pathname.

The child stdin wire is frozen byte-for-byte. It is one UTF-8 compact JSON object with exactly the 12 keys in lexicographic order artifactKind,contentBase64,contentByteLength,contentSha256,descriptorBlob,effectiveDwhBindingSha256,effectiveDwhCacheKey,outerRunId,schemaVersion,stateArtifact,workspaceId,workspaceRevision, no BOM, duplicate/reordered/unknown key, optional whitespace, alternate escaping, or trailing byte, followed by exactly one LF. contentBase64 is strict padded RFC 4648 base64 of the canonical persisted artifact bytes; contentByteLength and contentSha256 describe the decoded bytes, never the JSON/base64 text. The remaining identity fields must equal the locked request context, rendered config, and exact identity fields inside the decoded artifact.

Inventory content is at most 716,800 decoded bytes and readiness content at most 65,536. With the inherited canonical ASCII workspace-ID bound of 128 bytes and all fixed-width identities above, the exact largest inventory stdin is 4*ceil(716800/3) + 745 = 956,481 bytes; the largest readiness stdin is 88,118 bytes. Both are below the fixed 1,048,576-byte stdin cap. Node computes the envelope once from state-store bytes, validates it before spawn, and writes those exact bytes without JSON.stringify(Uint8Array) or a second serialization. No variant can represent raw argv, executable, env, cwd, stdio, FD, config path, workspace destination, or arbitrary state path.

The capability's exhaustive switch is the sole argv builder and emits exactly these internal argv after the tht executable (where CONFIG comes only from the opaque lease):

p3_migrate_dwh_cache                 config migrate-dwh-cache --json -c CONFIG
p3_prepare_dwh_cache                 preprocess dwh --prepare-revision-layout-v1 --json -c CONFIG
p3_materialize_dwh_snapshot          config materialize-dwh-snapshot --json -c CONFIG
p3_migrate_memory_root               memory migrate-root --json -c CONFIG
p3_rebuild_memory_projection         memory rebuild-projection --json -c CONFIG
p3_inventory_semantic_legacy         vector semantic-inventory --json -c CONFIG
p3_check_semantic_readiness          vector semantic-readiness --json -c CONFIG
p3_publish_semantic_replacements     vector semantic-publish --json -c CONFIG
p3_verify_semantic_replacements      vector semantic-verify --json -c CONFIG
p3_delete_confirmed_semantic_legacy  vector semantic-delete-confirmed --json -c CONFIG
p3_prepare_layout_markers            config prepare-layout-markers --json -c CONFIG
p3_publish_layout_version             config publish-layout-version --json -c CONFIG
p3_publish_revision_ready             config publish-revision-ready --json -c CONFIG
p3_verify_revision_readiness          config verify-revision-readiness --json -c CONFIG

Implement harness/tht/locked_child_stdin.py now, not in Task 9, with one bounded binary-reader API whose caller supplies only a compile-time literal expected artifact kind/state-artifact and decoded limit. It requires actual writer FD 3 and retained-root FD 4 first; streams at most 1,048,576 stdin bytes; enforces the canonical wire above; strict-base64 decodes; checks decoded length/digest; derives workspace, revision, descriptor, and both binding identities from Config; and verifies outer-run/artifact identities inside the decoded exact-key artifact. All four stdin-consuming semantic wrappers and all four marker/readiness wrappers must call it; inventory has no stdin. Python negatives cover zero/two LF, BOM, invalid UTF-8, empty/truncated/oversize JSON, duplicate/missing/unknown/reordered keys, whitespace/noncanonical escaping, bool-for-integer, invalid base64/padding, decoded oversize, length/digest mismatch, wrong artifact/state/run/workspace/revision/ descriptor/binding, content-identity mismatch, and missing/substituted/cross-root FD 3 or FD 4 before content use.

Implement harness/tht/layout_markers.py and register all four exact Typer targets in harness/tht/cli/config_cmd.py in this task. config prepare-layout-markers accepts only the frozen revision-ready-input-v1 envelope, validates the single-READY-per-revision rule, and returns the canonical bounded layout/READY input digests without publication. config verify-revision-readiness accepts the same contract and securely reopens the coordinator-published marker and selected READY, checks exact bytes/digests/identities and rejects another READY key. Unit tests cover help/registration, happy paths, strict stdin negatives, symlink/hardlink/replacement, other-key READY, and no-write preparation. A table-driven Python test proves all fourteen argv targets above resolve to the intended Typer command. A built-image test runs each target's fixed prefix with --help in the selected workspace-maintenance image and compares the fourteen-target set exactly, so coordinator tests cannot go GREEN against an unimplemented image command.

All mutating commands inherit only the actual writer FD 3 and retained-root FD 4 from the exact P2 capability. Inventory content remains capped at 716,800 decoded bytes, its RFC 4648 base64 is exactly at most 955,736 bytes, pointCount is bounded 0..716800, and canonical workspace ID is ASCII at most 128 bytes. With the exact inventory-result keys/types above, compact lexicographically keyed JSON, maximum-width values, and exactly one LF, the proved worst-case child stdout is 955736 + 581 = 956,317 bytes. Therefore inventory child stdout is bounded to 1,048,576 bytes (matching P2's 1 MiB boundary), not 921,600; every other variant remains 262,144 and stderr 65,536. A real bounded-collector round-trip test emits a 716,800-byte inventory, observes exactly 956,317 serialized bytes at maximum-width fields, strictly parses/decodes it, and compares all original bytes/digests; 1,048,576 total bytes is accepted and byte 1,048,577 terminates the process group without retaining overflow. Timeouts are 60 seconds for inventory/readiness verification, 300 seconds for migrations/snapshot/semantic verify/delete, 900 seconds for Memory projection, and 1,800 seconds for DWH prepare and semantic publish. Overflow/timeout terminates the child process group and returns a stable failed result.

Parse exactly P3LockedChildParsedResult above; validate base identity, literal operation, SHA-256, generation ID, integer counts, and status, and reject unknown keys, impossible per-operation fields, identity mismatch, output other than the one canonical JSON object plus one LF, invalid base64/decoded length, and public unsafe fields. Inventory contentBase64 is decoded, hash/length checked, persisted by PreprocessingStateStore, then removed before any public result encoding.

Add TypeScript compile-time satisfies Record<P3LockedOperation,...> and runtime assertNever tests, plus AST/source-boundary tests that fail on spawn, exec, fork, or raw argv outside the capability. Exercise every variant's exact argv/stdin/result/timeout; missing/substituted/cross-root FD 3 or FD 4, cross-workspace/revision/binding context, wrong artifact, post-settlement use, and arbitrary path/argv constructions fail before spawn. Coordinator tests monkeypatch all general process spawn APIs to throw and prove every migration still succeeds only through capability.spawnChild.

The exhaustive WorkspaceWriterLockCapability.spawnChild switch remains P2's sole process producer. Every P3 variant passes the locked writer open description as child FD 3 and the same capability-owned retained root directory open description as child FD 4. Before reading config/stdin or touching a source/destination, the Python shared child guard verifies FD 4's expected root identity and ownership, opens preprocessing/writer.lock relative to FD 4 with no-follow semantics, proves it is FD 3's inode, and proves FD 3 is already exclusively held. Missing, closed, renumbered, independently locked, substituted, cross-root, cross-workspace, or post-callback FD use fails preprocessing_conflict. No P3 raw root path, verified-root alias, capability wrapper/brand, environment marker, or direct helper spawn exists.

On an ordinary failure/signal, retain state and owner quiescence, return the exact run ID, and require same-ID resume; never wait for or restart core. Resume skips only phases whose exact persisted inputs and outputs reverify. After every successful non-activation operation, owner-clear while the exclusive reader lease remains held, then test a fresh admission. Before initial activation no revision-scoped semantic replacement exists. After enablement a session selects only its currently probed exact commit+binding READY; a second run cannot steal or clear the workspace.

Step 5: Run type/symbol/process gates and commit the complete release surface.

Run:

(cd tools/thothctl && test "$(go env GOVERSION)" = "go1.26.5" && go test ./...)
(cd backend && npm run build:native && npx vitest run \
  test/workspace-fs-at-native.test.ts test/workspace-lock-root-lease.test.ts \
  test/workspace-session-readers-lock.test.ts \
  test/workspace-preprocessing-state.test.ts test/workspace-preprocessing-service.test.ts \
  test/workspace-maintenance.test.ts test/workspace-registry.test.ts \
  test/workspace-registry-addressed-publication.test.ts \
  test/workspace-registry-addressed-process.test.ts test/workspace-reader-lease.test.ts \
  test/routes-workspaces.test.ts test/routes-sessions.test.ts test/app.test.ts \
  test/pi-process-manager.test.ts && npx tsc --noEmit -p . && npm run build)
(cd backend && npx tsc --noEmit --target ES2022 --module ES2022 \
  --moduleResolution Bundler --strict --skipLibCheck \
  test/registry-pull-job-imports.compile.ts)
(cd harness && .venv/bin/pytest -q \
  tests/test_locked_child_stdin.py tests/test_layout_marker_commands.py \
  tests/test_p3_internal_cli.py tests/test_semantic_revision_migration.py \
  tests/test_qdrant_cli_commands.py)
bash scripts/test-preprocess-compose-config.sh
bash scripts/test-compose-secret-policy.sh

Expected: PASS with no Go toolchain download. The four ordinary operations retain writer-first order, request bytes, phase transitions, fixed child argv/results, admission semantics, and existing-root acquire. The P2 native addon and typed wrapper pass their Linux/Darwin contract and remain the sole FD-relative provisioning/open/flock seam. P3's zero-filesystem adapter calls only acquireSessionReadersShared() and runUnderSessionReadersExclusive(...); the exact shared owner is transferred into PiProcessManager through child/stream teardown, and the exclusive callback encloses the full quiesced action. No P3 wrapper/handle/path/FD/name/flags/direct-open factory exists. The pull path calls only publishAddressed; its actual create literal passes all three validated identity digests plus expectedBaseCommit, its resume literal passes all three digests, and compile/runtime mismatch tests fail before network/state. It proves all eight exact P2 phases, pre-advertisement and post-advertisement same-ID recovery, exact-OID immutable-ref recovery, no network at/after target_fetched, bootstrap/pull callback parity, complete lexical ownership, missing-root provisioning, same participant/capability objects, no reentry, exact state path, all-or-nothing publication, base/target/mixed reconciliation, terminal-before-clear recovery, and reverse release. Inspect, lazy list, and status call only ensureBootstrapAddressed and consume its full result union: bounded exact-identity automatic same-ID bootstrap recovery works at every preterminal kill point, and queued or ordinary active callers converge through the exact already_active snapshot with one publication and no second network/state work; ambiguous/corrupt/incompatible cases fail before network/state. Routes retain the sole bootstrap/pull/activation mutation lifecycle. Every mutating child validates both writer FD 3 and retained-root FD 4. The standalone compile command imports all six released job types from ../src/workspaces/registry-pull-job.js, exact LockFileName from workspace-fs-at.js, and exact RegistryActiveSnapshotV1/RegistryEnsureBootstrapAddressedResultV1 from registry-publication.js.

Run a final symbol fence before commit:

python3 - <<'PY'
from pathlib import Path
roots = [Path("backend/src"), Path("backend/test")]
text = "\n".join(p.read_text() for root in roots for p in root.rglob("*.ts"))
for stale in (
    "RegistryPullBa" + "seTargetPlanV1",
    "RegistryAddressedW" + "orkspaceLeaseOwner",
    "acquireCo" + "mpleteSet",
    "releaseCo" + "mpleteSet",
    "pullAndPubl" + "ishAddressed",
    "registry-" + "pull-jobs",
    "VerifiedWo" + "rkspaceRoot",
    "WorkspaceLockFi" + "leNameV1",
    "RegistryValidatedActive" + "SnapshotV1",
    "RegistryBootstrapEnsure" + "AddressedResultV1",
    "interface Workspace" + "ReaderLease",
):
    assert stale not in text, stale
module = Path("backend/src/workspaces/registry-pull-job.ts").read_text()
for name in (
    "RegistryPullPublicJobRequestV1",
    "RegistryPullPhaseV1",
    "RegistryPullAddressedJobRequestV1",
    "RegistryPullParticipantStateV1",
    "RegistryPullSynchronizerStateV1",
    "RegistryPullJobStateV1",
):
    assert f"export " in module and name in module, name
print("P3 registry symbol fence: PASS")
PY

Expected: P3 registry symbol fence: PASS.

Commit all owning files, including the exact export and compile-import test:

git add backend/src/workspaces/preprocessing-state.ts \
  backend/src/workspaces/preprocessing-service.ts backend/src/workspace-maintenance.ts \
  backend/src/workspaces/registry.ts backend/src/workspaces/registry-publication.ts \
  backend/src/workspaces/registry-pull-job.ts \
  backend/test/registry-pull-job-imports.compile.ts \
  backend/test/workspace-registry.test.ts \
  backend/test/workspace-registry-addressed-publication.test.ts \
  backend/test/workspace-registry-addressed-process.test.ts \
  backend/test/fixtures/workspace-lock-root-worker.mjs \
  backend/test/fixtures/workspace-registry-addressed-worker.mjs \
  backend/src/workspaces/workspace-reader-lease.ts \
  backend/src/routes/workspaces.ts backend/src/routes/sessions.ts backend/src/app.ts \
  backend/test/app.test.ts backend/src/pi/pi-process-manager.ts \
  backend/test/workspace-preprocessing-state.test.ts \
  backend/test/workspace-preprocessing-service.test.ts backend/test/workspace-maintenance.test.ts \
  backend/test/workspace-reader-lease.test.ts backend/test/routes-workspaces.test.ts \
  backend/test/routes-sessions.test.ts backend/test/pi-process-manager.test.ts \
  harness/tht/locked_child_stdin.py harness/tht/layout_markers.py \
  harness/tht/cli/config_cmd.py harness/tht/cli/vector_cmd.py harness/tht/semantic_migration.py \
  harness/tests/test_locked_child_stdin.py harness/tests/test_layout_marker_commands.py \
  harness/tests/test_p3_internal_cli.py harness/tests/test_semantic_revision_migration.py \
  harness/tests/test_qdrant_cli_commands.py \
  tools/thothctl/internal/workspaceops/operations.go \
  tools/thothctl/internal/workspaceops/operations_test.go \
  tools/thothctl/cmd/thothctl/main.go tools/thothctl/cmd/thothctl/main_test.go \
  deploy/compose.git-https.yaml deploy/compose.git-ssh.yaml \
  scripts/generate-connector-secrets-override.sh scripts/test-preprocess-compose-config.sh
git commit -m "feat: add exact addressed registry pull and resumable maintenance"

Task 9: Prepare one exact binding generation per commit and activate the revision layout

Files:

  • Modify: backend/src/workspaces/revision-layout.ts
  • Modify: backend/src/workspaces/runtime-config-lease.ts
  • Modify: backend/src/workspaces/runtime-renderer.ts
  • Modify: backend/src/tht/tht-runner.ts
  • Modify: backend/src/workspaces/preprocessing-state.ts
  • Modify: backend/src/workspaces/preprocessing-service.ts
  • Modify: backend/src/workspace-maintenance.ts
  • Reuse unchanged from Task 8: backend/src/workspaces/workspace-reader-lease.ts
  • Modify: backend/src/pi/pi-process-manager.ts
  • Modify: backend/test/workspace-revision-layout.test.ts
  • Modify: backend/test/workspace-runtime-renderer.test.ts
  • Modify: backend/test/workspace-runtime-config-lease.test.ts
  • Modify: backend/test/workspace-runtime-handoff.test.ts
  • Create: backend/test/workspace-effective-config-equivalence.test.ts
  • Modify: backend/test/workspace-preprocessing-service.test.ts
  • Modify: backend/test/workspace-maintenance.test.ts
  • Modify: backend/test/workspace-reader-lease.test.ts
  • Modify: backend/test/pi-process-manager.test.ts
  • Modify: harness/tht/layout_markers.py
  • Modify: harness/tht/cli/config_cmd.py
  • Modify: harness/tests/test_layout_marker_commands.py
  • Modify: harness/tests/test_p3_internal_cli.py
  • Modify: harness/tht/cli/schema_cmd.py
  • Modify: harness/tht/cli/lsh_cmd.py
  • Modify: harness/tht/cli/preprocess_cmd.py
  • Modify: harness/tht/jobs/dwh_pipeline.py
  • Modify: harness/tht/taskdoc.py
  • Modify: harness/tht/search/evidence.py
  • Modify: harness/tht/cli/memory_cmd.py
  • Modify: harness/tht/memory.py
  • Modify: harness/tht/solved.py
  • Modify: harness/tests/test_dwh_snapshot.py
  • Modify: harness/tests/test_dwh_owner_migration.py
  • Modify: harness/tests/test_dwh_preprocess_job.py
  • Modify: harness/tests/test_lsh_job_resume.py
  • Modify: harness/tests/test_search_pack.py
  • Modify: harness/tests/test_schema_fk_annotations.py
  • Modify: harness/tests/test_qdrant_cli_commands.py
  • Modify: harness/tests/test_corpus_pipeline.py
  • Modify: harness/tests/test_memory_migration.py
  • Modify: harness/tests/test_memory_promotion.py
  • Modify: harness/tests/test_memory_save_one.py
  • Modify: harness/tests/test_solved_search_cli.py
  • Modify: tools/thothctl/internal/workspaceops/operations.go, operations_test.go
  • Modify: tools/thothctl/cmd/thothctl/main.go, main_test.go

Step 1: Write the RED complete-consumer, admission, and transaction tests.

Add the exact released host command:

thothctl --installation <abs> workspace migrate activate-revision-layout
  --workspace <id> --yes [--resume <32-hex-outer-run-id>] [--json]

Fresh pre-generates one outer run ID and launches Task 8's single dedicated job with the canonical create request. The one-shot atomically creates or exactly replays that ID/digest and can produce no second run. Every retry after a confirmed nonterminal result uses that exact --resume; an ambiguous host result exposes the generated ID with run_durability_unconfirmed. Freeze durable phases:

run_addressed -> quiescence_published -> reader_gate_exclusive -> effective_binding_probed ->
future_paths_bound -> dwh_cache_prepared -> dwh_snapshot_ready -> memory_ready ->
semantic_inventory_persisted -> semantic_sources_ready ->
semantic_replacements_published -> semantic_replacements_verified ->
layout_version_published -> legacy_delete_complete -> ready_published ->
terminal_durable -> quiescence_owner_cleared

Transitions are closed, monotonic, and artifact-digest bound. Resume re-verifies a phase before skipping it. It must resume after a crash during reader drain, partial legacy deletion, either atomic marker rename, terminal-state fsync, and before owner-qualified quiescence clear, including while core is unavailable.

Initial revision A plus effective binding X requires all of these before its READY publication:

  1. matching durable quiescence/run ownership, safe bounded session inventory, and an exclusive session-readers.lock lease proving zero active readers while new admission is blocked;
  2. P2 writer lock held continuously inside maintenance and active revision/descriptor/config revalidated;
  3. fixed read-only harness binding probe returns X, and every later child/result repeats X's binding SHA/cache key;
  4. matching schema-v1 DWH is migrated, or otherwise prepare-mode DWH builds, then strictly reverifies the trusted X cache and materializes the exact A/X binding-qualified snapshot;
  5. canonical Memory is migrated/reverified at the future global root;
  6. exact legacy semantic inventory and source-readiness bytes are persisted, then all A replacements are published and read back while the exclusive reader lease remains held;
  7. global layout-version.json is published atomically (or strictly reverified if present);
  8. only then, exact digest-confirmed legacy semantic deletion completes;
  9. exact revisions/A/readiness/X/READY.json is exclusively published last and strictly re-read.

The layout marker never names A or X. For later revision B, released workspace registry pull publishes B through P2's repository-first addressed callback and its complete changed-set capability set. With layout v1 already global, B/X has no READY: the fixed binding probe selects X and session admission fails migration_required, while prepare maintenance returns only trusted B/X paths. Activation reuses the verified X cache, creates a B/X snapshot and readiness generation without rewriting the marker; A/X and B/X coexist.

Also test a changed installation binding Y while commit B is already READY for X. The session probe selects Y, refuses B/X cache/snapshot/READY, and fails migration_required; activation securely finds the existing B/X READY and returns effective_config_mismatch before DWH prepare/migration, snapshot writes, semantic inventory/publish/delete, or marker/READY writes. B/X READY, snapshot, semantic points, and owner remain byte-identical. The fixture then makes and publishes content-only commit C and invokes released registry pull. C/Y is unready and isolated; activation now chooses the Task 5 prepare-mode DWH build (not legacy migration or X fallback), publishes/verifies the new schema-v2 Y cache and C/Y snapshot, publishes/read-backs C semantic replacements while quiesced, and exclusively creates revisions/C/readiness/Y/READY.json. With Y still selected, C/Y is admitted and B admission remains intentionally refused; B/X READY, snapshot, semantic points, and owner bytes remain unchanged but revision pinning alone does not restore binding X. The test then explicitly restores the installation binding to X, probes X and admits historical B/X; restores the installation binding to Y, probes Y and admits C/Y. Repeat the refusal -> newly published successor commit -> changed-binding activation for endpoint, transport, database, schema, and one included policy mutation, using a distinct fresh revision for every newly selected binding; no test may activate B/Y or any second READY binding at one revision.

Inventory every physical/LSH consumer and fail the test if it references .tht-dwh, artifacts/mschema/physical.yaml, or indexes/lsh outside dwh_snapshot.py. Explicitly exercise schema_cmd.physical_path, schema introspection/check/index, lsh_cmd build/read, preprocess DWH, dwh_pipeline, taskdoc/search-pack generation, and mschema readers through resolve_revision_dwh_snapshot. Exercise all Memory commands through memory_root/registry_path and Evidence preprocessing/search through explicit paths.corpus.

Add fault tests at every phase and prove throughout the publish/delete/READY interval that the durable marker blocks the new-admission race, the exclusive reader gate proves all shared readers drained, persistent inventory was accepted, and no session child remains. Replacement publish/readback must precede the first legacy delete. READY must follow the final delete. On failure/SIGKILL the OS locks release but durable owner state and quiescence remain, sessions stay refused, and only exact same-ID resume continues even with dead core. No failure clears quiescence or rolls back to P2 once the global marker exists; wrong owner/digest can neither resume nor clear.

Run:

(cd backend && npx vitest run \
  test/workspace-revision-layout.test.ts \
  test/workspace-runtime-renderer.test.ts \
  test/workspace-runtime-config-lease.test.ts \
  test/workspace-runtime-handoff.test.ts \
  test/workspace-effective-config-equivalence.test.ts \
  test/workspace-preprocessing-service.test.ts \
  test/workspace-maintenance.test.ts test/workspace-reader-lease.test.ts \
  test/routes-sessions.test.ts test/pi-process-manager.test.ts)
(cd harness && .venv/bin/pytest -q \
  tests/test_dwh_snapshot.py tests/test_dwh_owner_migration.py \
  tests/test_dwh_preprocess_job.py tests/test_lsh_job_resume.py tests/test_search_pack.py \
  tests/test_schema_fk_annotations.py tests/test_qdrant_cli_commands.py \
  tests/test_corpus_pipeline.py tests/test_memory_migration.py \
  tests/test_memory_promotion.py tests/test_memory_save_one.py tests/test_solved_search_cli.py)
(cd tools/thothctl && test "$(go env GOVERSION)" = "go1.26.5" && \
  go test ./internal/workspaceops ./cmd/thothctl -run 'Activate|RegistryPull|Resume|Quiesc|Reader|DedicatedJob' -v)

Expected: RED on activation, readiness gating, and remaining direct paths.

Step 2: Implement synchronous marker-gated session rendering and trusted prepare rendering.

Keep P2's exact synchronous signatures:

export class WorkspaceRuntimeConfigLeaseFactory {
  acquireSession(snapshotPath: string): RuntimeConfigLease;
  acquireMaintenance(input: MaintenanceRuntimeInput): RuntimeConfigLease;
}

Extend MaintenanceRuntimeInput with exact layoutIntent: "active" | "prepare-revision-layout-v1"; keep both factory signatures synchronous and do not add a TypeScript effective-DWH canonicalizer. In layout v1 the returned RuntimeConfigLease begins as a bounded binding-probe lease: its YAML has the exact descriptor/DWH inputs and trusted future base roots, may be used only for fixed tht config effective-dwh --json -c CONFIG, and cannot spawn Pi or any mutating child. The existing async ThtRunner startup/coordinator path runs that read-only probe with 65,536 stdout bytes, 16,384 stderr bytes and a 30-second timeout, validates the exact EffectiveDwhBinding and key, then calls a new synchronous lease method selectEffectiveDwh(binding): BoundRuntimeConfigLease. That method invokes readRevisionLayoutState(..., effectiveDwhCacheKey) and bindingQualifiedWorkspaceLayoutPaths; it never selects newest/only READY. This adds no alternate factory entrypoint and preserves P2's exact acquireSession(snapshotPath) and acquireMaintenance(input) signatures.

Session behavior after selection is exact:

  • no global marker: byte-identical P2 roots and normal P2 handoff;
  • valid marker + exact commit+binding READY: render its binding-qualified cache/snapshot and revision roots, reverify READY identity, then and only then spawn Pi/session work;
  • valid marker + absent exact READY: migration_required before Pi/session or semantic child spawn, even if another READY exists for that commit;
  • invalid marker/selected READY or binding mismatch: fail closed.

Active maintenance follows the same rule. Prepare maintenance is the only exception: before READY and only after the secure directory guard proves that revision has no READY for another binding, it uses the probed binding key with the trusted resolvers and renders only:

sessions = /data/sessions/<id>/sessions
memory = /data/sessions/<id>/memory
dwh_cache = /data/sessions/<id>/preprocessing/dwh-cache/<binding-key>
dwh_snapshot = /data/sessions/<id>/revisions/<revision>/dwh-snapshots/<binding-key>
ready = /data/sessions/<id>/revisions/<revision>/readiness/<binding-key>/READY.json
artifacts = /data/sessions/<id>/revisions/<revision>/artifacts
indexes = /data/sessions/<id>/revisions/<revision>/indexes
corpus = /data/sessions/<id>/revisions/<revision>/corpus

It never consults P2 memory, .tht-dwh, artifacts, indexes, corpus, another binding cache/snapshot, or a sibling READY as a destination. Tests prove the probe lease cannot escape to a session/mutating spawn and that session and maintenance selection use byte-identical binding-qualified YAML.

Step 3: Implement the quiesced activation transaction in the flat coordinator.

workspaceops.Run launches only Task 8's dedicated job. Inside that one-shot, WorkspacePreprocessingService.execute obtains CanonicalWorkspaceLockRootInput from the sole P2 factory, uses existing-root acquire, transfers the exact VerifiedWorkspaceLockRootLease, and enters one runUnderWorkspaceWriterLock(rootLease, async (writerCapability) => ...) action before ordinary job or quiescence. Inside that still-live writer callback, publish the durable owner marker, drain readers, then call Task 8's exact readerLeaseFactory.acquireForMaintenance(writerCapability, async (readerBorrow) => ...). The complete quiesced activation—including final recheck, all children/publication, terminal durability, and matching owner clear—settles inside that callback; readerBorrow never escapes. The writer action consumes and closes the root only after the adapter has returned and P2 has invalidated/closed the exclusive reader. Each fixed internal migration request runs only through writerCapability.spawnChild; thus the actual locked open file description is child FD 3 and the same retained root description is FD 4; neither is reopened. Revalidate target snapshot and inventory at every child boundary.

Activation is the sole semantic cutover and one continuous quiesced transaction. It binds the exact commit+effective key, then before DWH/Memory/semantic mutation securely enumerates the revision's readiness directory. An empty directory permits preparation; the selected READY may only byte-match for idempotent resume; one READY at another key returns effective_config_mismatch; multiple/unsafe entries fail closed. Only after this guard does activation migrate or prepare-build/reverify the cache and binding snapshot; reuse/recheck Memory and persisted semantic inventory/readiness; publish all replacement points and read back their exact identities/digests; atomically publish/reverify the global marker for initial enablement; delete only unchanged exact legacy inventory IDs; and exclusively publish the selected commit+binding READY last. For later revisions it strictly verifies the unchanged global marker and does not broaden deletion. All exclusive files use sibling write + file fsync + rename + parent fsync and synchronous no-follow reread. Different bytes at the same (commit,key) fail closed; a changed binding never writes a sibling generation for that commit and must use a newly published revision.

Only after READY and terminal run state are durably re-read does the same one-shot owner remove its byte-matching quiescence marker while still holding the exclusive reader gate, then release the gate. No HTTP/backend acknowledgment and no core lifecycle action exists. Failure before the global marker leaves P2 rendering but stays durably quiesced; failure after the marker leaves the revision unready and stays quiesced. Process death releases OS locks but exact same-ID resume is the only recovery path.

Step 4: Switch every consumer only through the shared resolvers.

Remove every direct path found by Step 1. P3 configs require a valid revision snapshot; legacy configs without dwh_snapshot retain their compatibility branch only when the global marker is absent. Schema/LSH reads never probe cache/old roots after layout enablement. Memory and corpus use explicit config paths. Schema/Evidence vector operations select strict revision scope only from a ready P3 config.

Step 5: Prove A to B transition and effective-binding equivalence.

For A/B with identical DWH inputs, run tht config effective-dwh --json against session and maintenance leases and assert byte-identical binding/cache key, distinct revision roots, and stable workspace://<id>. Use the released command, not raw Git or the backend route:

"$THOTHCTL" --installation "$INSTALLATION" workspace registry pull \
  --workspace "$WORKSPACE" --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate dwh-cache \
  --workspace "$WORKSPACE" --json

Assert B/X session admission is migration_required before B/X READY. The released dwh-cache command resolves only B/X, materializes/reverifies the binding-qualified B/X snapshot for pre-READY preparation/P5 validation, and proves no layout marker, READY, or semantic point changed; then activate and admit B/X. Then, without changing commit B, change effective binding to Y: admission fails and activation returns effective_config_mismatch before p3_prepare_dwh_cache, cache/snapshot/semantic/marker writes, or READY publication. Publish and pull content-only revision C, prove C/Y is unready, then run the full released activation path: p3_prepare_dwh_cache creates the new v2 Y owner/cache, C/Y snapshot, semantic verification, and immutable C/Y READY. With Y selected, admit C/Y and continue refusing B; prove B/X bytes unchanged. Explicitly restore installation binding X and admit B/X, then restore Y and admit C/Y. Repeat the complete refusal -> fresh successor revision -> new-binding activation path for endpoint, transport, database, schema, and one included policy, never reusing an already-READY revision for the next binding. Hold the registry addressed callback-scoped ordered set open to prove every changed-workspace writer capability contends without reacquisition; hold each ordinary child open for its selected writer lock; hold multiple session reader leases to prove drain, and rely on durable admission blocking plus the exclusive reader gate for semantic cutover while core may keep running.

Step 6: Run GREEN activation and full P2 regression gates.

(cd backend && npx vitest run \
  test/workspace-revision-layout.test.ts \
  test/workspace-runtime-renderer.test.ts \
  test/workspace-runtime-config-lease.test.ts \
  test/workspace-runtime-handoff.test.ts \
  test/workspace-effective-config-equivalence.test.ts \
  test/workspace-preprocessing-state.test.ts \
  test/workspace-preprocessing-service.test.ts \
  test/workspace-maintenance.test.ts test/workspace-reader-lease.test.ts \
  test/routes-sessions.test.ts test/pi-process-manager.test.ts test/tht-runner.test.ts && npx tsc --noEmit -p . && npm run build)
(cd harness && .venv/bin/pytest -q \
  tests/test_effective_dwh_binding.py tests/test_dwh_snapshot.py tests/test_dwh_owner_v2.py \
  tests/test_dwh_owner_migration.py tests/test_dwh_preprocess_job.py \
  tests/test_locked_child_stdin.py tests/test_layout_marker_commands.py tests/test_p3_internal_cli.py \
  tests/test_lsh_job_resume.py tests/test_search_pack.py tests/test_schema_fk_annotations.py \
  tests/test_qdrant_vector_store.py tests/test_semantic_kind_isolation.py \
  tests/test_qdrant_cli_commands.py tests/test_corpus_pipeline.py \
  tests/test_memory_migration.py tests/test_memory_promotion.py \
  tests/test_memory_save_one.py tests/test_solved_question.py tests/test_solved_search_cli.py)
(cd tools/thothctl && test "$(go env GOVERSION)" = "go1.26.5" && go test ./...)
./scripts/p2-acceptance.sh integration --keep

Expected: every focused gate passes; fresh P2 acceptance still passes before global enablement; A/X, B/X, same-revision Y refusal, C/Y READY, explicit X restore/B admission, and Y restore/C admission tests pass. New admission cannot cross quiescence publication; semantic delete and READY publication occur only under the exclusive reader gate, regardless of whether core is running.

Step 7: Commit.

git add backend/src/workspaces/revision-layout.ts \
  backend/src/workspaces/runtime-config-lease.ts backend/src/workspaces/runtime-renderer.ts \
  backend/src/tht/tht-runner.ts backend/src/workspaces/preprocessing-state.ts \
  backend/src/workspaces/preprocessing-service.ts backend/src/workspace-maintenance.ts \
  backend/src/pi/pi-process-manager.ts \
  backend/test/workspace-revision-layout.test.ts backend/test/workspace-runtime-renderer.test.ts \
  backend/test/workspace-runtime-config-lease.test.ts backend/test/workspace-runtime-handoff.test.ts \
  backend/test/workspace-effective-config-equivalence.test.ts \
  backend/test/workspace-preprocessing-service.test.ts backend/test/workspace-maintenance.test.ts \
  backend/test/workspace-reader-lease.test.ts backend/test/pi-process-manager.test.ts \
  harness/tht/cli/schema_cmd.py harness/tht/cli/lsh_cmd.py harness/tht/cli/preprocess_cmd.py \
  harness/tht/jobs/dwh_pipeline.py harness/tht/taskdoc.py harness/tht/search/evidence.py \
  harness/tht/cli/memory_cmd.py harness/tht/memory.py harness/tht/solved.py \
  harness/tht/layout_markers.py harness/tht/cli/config_cmd.py \
  harness/tests/test_layout_marker_commands.py harness/tests/test_p3_internal_cli.py \
  harness/tests/test_dwh_snapshot.py harness/tests/test_dwh_owner_migration.py \
  harness/tests/test_dwh_preprocess_job.py harness/tests/test_lsh_job_resume.py \
  harness/tests/test_search_pack.py harness/tests/test_schema_fk_annotations.py \
  harness/tests/test_qdrant_cli_commands.py harness/tests/test_corpus_pipeline.py \
  harness/tests/test_memory_migration.py harness/tests/test_memory_promotion.py \
  harness/tests/test_memory_save_one.py harness/tests/test_solved_search_cli.py \
  tools/thothctl/internal/workspaceops/operations.go \
  tools/thothctl/internal/workspaceops/operations_test.go \
  tools/thothctl/cmd/thothctl/main.go tools/thothctl/cmd/thothctl/main_test.go
git commit -m "feat: activate revision-isolated workspace layout"

Task 10: Document .tht-dwh, migration, recovery, and the P3 manual walkthrough

Files:

  • Modify: docs/install/local-workspace-registry.md
  • Modify: docs/install/server-workspace-registry.md
  • Modify: docs/testing/p2-p6-manual-verification.md
  • Create: docs/architecture/effective-dwh-cache.md
  • Modify: docs/index.md

Step 1: Write documentation contract tests first.

Create or extend scripts/test-verify-workspace-install-docs.sh assertions requiring both manuals and the architecture page to name:

  • effective config vs input fingerprint;
  • logical source identity and excluded revision/temp filename/secrets;
  • .tht-dwh, immutable generations/, OWNER.json v1/v2, ACTIVE;
  • cache key, revision snapshot roots, and why mismatch fails closed;
  • exact inspect/registry-pull/migrate/--resume/preprocess/reindex/recovery commands, including that explicit --resume is for pull/migration while empty-registry bootstrap recovery is automatic through inspect, lazy list, and status;
  • the repo-owned Node-API v8 workspace-fs-at seam for anchored missing-root provisioning, its closed typed writer/reader lock-name and flock surface, its Darwin/Linux durability behavior, and that exact fs-ext@2.1.1 is wrapper-internal and flock(2) only with no P3 raw FD/import/cast/path fallback;
  • client-generated addressed run IDs/digests, the pull create/resume identity/base fields derived inside the validated boundary, bounded exact-identity ensureBootstrapAddressed active-state/zero/one/multiple/corrupt rules (including the already_active no-network/no-state branch) and registry_bootstrap_recovery_conflict, no-follow durable owner quiescence, shared session reader leases, exclusive drain, dead-core same-ID resume, and owner-only clear;
  • global layout-version marker vs immutable commit+effective-binding READY, with at most one READY binding per revision and a required new content commit for changed binding;
  • Memory canonical JSONL vs Qdrant projection;
  • backup scope for cache, revisions, corpus, Memory, Qdrant, and registry;
  • warning not to edit OWNER/ACTIVE or copy an unverified generation manually.

Run:

./scripts/test-verify-workspace-install-docs.sh

Expected: RED on missing P3 content.

Step 2: Write operator-facing architecture and recovery content.

Explain safe recovery choices: inspect; inspect/lazy list/status always enter the continuously repository-locked selector, return its validated already_active snapshot without job scan/network/state when active state exists (including queued callers after another caller publishes), and only for validated absence automatically resume the sole exact-identity nonterminal bootstrap or create one, while incompatible active state or multiple, pull, mismatched, corrupt, churning, or over-bound job sets stop with registry_bootstrap_recovery_conflict and no operator-selected run ID; preserve legacy filesystem sources; run non-activating DWH/Memory preparation; use semantic-revision only to inventory legacy points and prove rebuild readiness, explicitly documenting that it writes no replacement Qdrant point and deletes nothing. workspace migrate dwh-cache must be documented as pre-READY materialization of the selected cache plus binding-qualified pulled-revision snapshot, usable before P5 acceptance but incapable of publishing layout/READY or admitting that revision. Perform replacement publish/readback, global reader-mode switch, exact legacy deletion, and immutable commit+binding READY only in one addressed one-shot run after durable admission blocking and exclusive reader-gate acquisition. The CLI never calls Fastify/HTTP or stops core; blocked admission plus zero shared readers makes publication safe while core may run. Use the client-generated run ID with exact --resume after interruption or ambiguous response, including dead-core recovery; SIGKILL releases OS locks but retains owner marker/run state. Prepare a changed binding only after publishing/pulling a new content commit; never activate a second binding READY for one revision. While Y is selected, B/X bytes remain immutable but B admission is refused; historical B/X is demonstrated only by restoring installation X, then Y is restored for C/Y. Rebuild Memory projection from canonical JSONL; never hand-clear quiescence or weaken digest checks. Evidence that cannot be rebuilt stays migration_required and its legacy points are not deleted. A global marker without the current exact commit+binding READY intentionally blocks sessions while prepare mode remains available only if that revision has no other READY binding. State that schema-v1 remains readable only to verify/migrate, not writable by P3. Explain that absent changed-set roots are provisioned only by P2's owned WorkspaceFsAtV1 Node-API seam using literal FD-relative syscalls, with Linux directory-fsync success and Darwin success-or-exact fail-closed behavior. For readers, P3 calls only VerifiedWorkspaceLockRootLease.acquireSessionReadersShared() and WorkspaceWriterLockCapability.runUnderSessionReadersExclusive(...) through the zero-filesystem adapter; it never opens/flocks directly or receives a wrapper, directory handle, path, numeric FD, lock name, flags, or mode, and exact fs-ext@2.1.1 remains P2-wrapper-internal and flock(2) only.

Step 3: Replace the P3 placeholder in the living manual with an independently runnable clean walkthrough.

Use variables INSTALLATION, THOTHCTL, WORKSPACE, and a new fixture/private Git remote. Include these exact operator calls:

"$THOTHCTL" --installation "$INSTALLATION" workspace inspect \
  --workspace "$WORKSPACE" --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate dwh-cache \
  --workspace "$WORKSPACE" --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate memory \
  --workspace "$WORKSPACE" --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate semantic-revision \
  --workspace "$WORKSPACE" --yes --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate activate-revision-layout \
  --workspace "$WORKSPACE" --yes --json
"$THOTHCTL" --installation "$INSTALLATION" workspace preprocess dwh \
  --workspace "$WORKSPACE" --json

# After committing/pushing content-only B, use the released product pull, never raw registry mutation:
"$THOTHCTL" --installation "$INSTALLATION" workspace registry pull \
  --workspace "$WORKSPACE" --json
# Pre-READY: materialize the pulled revision's selected cache/snapshot; do not activate READY.
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate dwh-cache \
  --workspace "$WORKSPACE" --json
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate activate-revision-layout \
  --workspace "$WORKSPACE" --yes --json

# Exact crash recovery pattern: export RUN_ID from the returned 32-hex runId first.
RUN_ID="${RUN_ID:?export RUN_ID as the returned 32-hex outer run ID}"
[[ "$RUN_ID" =~ ^[0-9a-f]{32}$ ]]
"$THOTHCTL" --installation "$INSTALLATION" workspace migrate activate-revision-layout \
  --workspace "$WORKSPACE" --yes --resume "$RUN_ID" --json

Then walk the reviewer through:

  1. during the initial empty-registry bootstrap, inject one preterminal death for each of inspect, lazy list, and status, then prove the next actual caller automatically resumes the same exact-identity run before network; prove multiple/mismatched/corrupt jobs stop with registry_bootstrap_recovery_conflict; after terminal recovery, record revision A and compare the safe operator/session effective binding;
  2. inspect migrated v2 OWNER, ACTIVE, binding-qualified immutable snapshot manifests/digests, the workspace-global layout-version.json, and exact A/binding readiness/<key>/READY.json without printing config/secrets;
  3. after each non-activating command, prove owner-clear permits a session that sees no mixed legacy/replacement state; specifically prove dwh-cache materializes the selected binding-qualified snapshot but no READY/layout/semantic bytes, and semantic-revision makes zero Qdrant upsert/delete calls;
  4. hold two live sessions with shared reader leases, start activation, prove the durable marker refuses a racing new admission, let both readers drain without killing them, and prove publication starts only after the exclusive reader lease establishes zero active readers; keep core running and show no Fastify/HTTP or stop/start command occurs;
  5. inject SIGKILL during partial deletion and after READY rename, make core unavailable, then use the returned exact --resume ID to reach success without re-embedding or broad deletion; prove the OS lock released, the durable owner marker remained, and wrong owner/digest could not clear it;
  6. commit/push content-only revision B, invoke released workspace registry pull, run workspace migrate dwh-cache, prove the B/X cache and binding-qualified snapshot exist pre-READY and are usable for later P5 validation while B admission still fails, then activate and prove the same cache generation but distinct revision roots and schema/Evidence points;
  7. prove A/X READY remains, and Memory/solved results remain visible at B with global identity;
  8. hold activation/mutation open and prove concurrent registry publication is refused; no existing reader is killed to reach exclusive ownership;
  9. while commit B is READY for X, change the fixture binding to Y; prove B admission and same-revision activation are refused with zero mutation while B/X bytes remain immutable. Publish/pull C, run pre-READY dwh-cache, activate C/Y, and admit C/Y. Then explicitly restore installation X and admit B/X; restore Y and admit C/Y. Repeat on distinct successor revisions for endpoint, transport, database, schema, and one included policy;
  10. create a conflicting legacy Memory registry, prove durable owner quiescence, remove only the conflict, and resume the same ID;
  11. scan captured output for fixture secret canaries and perform exact cleanup.

For every step explain component, state read, artifact produced, invariant, evidence to retain, and PASS/FAIL criterion. Set automated integration: PENDING until Task 12; keep manual acceptance: PENDING and Decision: PENDING until reviewer action. The manual environment must not reuse automated state.

Step 4: Run docs checks.

./scripts/test-verify-workspace-install-docs.sh

Expected: PASS.

Step 5: Commit.

git add docs/install/local-workspace-registry.md docs/install/server-workspace-registry.md \
  docs/testing/p2-p6-manual-verification.md docs/architecture/effective-dwh-cache.md \
  docs/index.md scripts/test-verify-workspace-install-docs.sh
git commit -m "docs: explain effective DWH cache ownership"

Task 11: Build the clean-state P3 acceptance process and self-tests

Files:

  • Create: scripts/p3-acceptance.sh
  • Create: scripts/test-p3-acceptance.sh
  • Create: backend/scripts/p3-acceptance.mjs
  • Modify: .gitignore only if .artifacts/p3-effective-config/ is not already covered

Step 1: Write RED acceptance-harness tests.

test-p3-acceptance.sh must test, without requiring the full expensive success run:

  • exact CLI grammar: integration [--keep] and cleanup --run <id>;
  • dirty tracked source refusal before build/start;
  • unique run/project IDs and exclusive ownership manifest creation;
  • no retry loop or recursive self-invocation;
  • trap cleanup on pre-service and post-service injected failures;
  • refusal to clean an unowned/mismatched project, volume, network, path, or symlink;
  • --keep retains owned resources and reports; cleanup removes owned resources only and is idempotent;
  • report schema and finalization on PASS and injected FAIL;
  • secret canaries absent from stdout/stderr/report/public retained files;
  • Qdrant/Ollama listener shutdown and Compose resource absence after normal cleanup.

Run:

./scripts/test-p3-acceptance.sh

Expected: RED because the runner does not exist.

Step 2: Implement the process runner.

Follow the hardened P1/P2 runner conventions: trusted fixed toolchain discovery, sanitized environment, no shell-evaluated fixture data, bounded subprocess capture, exact ownership labels, raw-Git safety, single attempt, atomic report writes, SHA-256 artifact manifest, and signal-safe cleanup. Reuse P2 fixture setup functions rather than copying them if they are already factored into sourceable non-executable helpers.

The full process assertions are:

  1. clean source commit/tree, exact go1.26.5, and no pre-existing owned resources;
  2. revision A P2 schema-v1 fixture and legacy Memory/schema/Evidence state detected as migration_required, with byte-identical P2 session/maintenance roots;
  3. the synchronous no-follow layout reader rejects symlinks/replacement/malformed state, and the trusted future resolver selects exact migration destinations without active fallback;
  4. every released operation pre-generates a 32-hex ID and launches only the immutable-image-pinned, profile-gated workspace-maintenance Compose job with canonical 4,096-byte-bounded request stdin and bounded exact JSON result; no Fastify/HTTP/internal route, compose exec core, frontend, host Python/Node/Pi/tht, or core stop/start is invoked. The four ordinary operations each use the selected-workspace P2 existing-root/writer-first lifecycle: shared session ownership is acquired only by acquireSessionReadersShared() and transferred into PiProcessManager through all child/stream teardown, while the full quiesced maintenance action is enclosed only by runUnderSessionReadersExclusive(...) via the zero-filesystem adapter. registry_pull uses only the repository-first P2 addressed callback; its actual create request passes validated installation/repository/remote identities plus expected base and its resume request passes all three identities, with compile/runtime mismatch-before-network/state proof. It owns the complete lexical changed set, provisions absent roots and opens/flocks writer/reader gates only through P2's typed repo-owned WorkspaceFsAtV1 seam (with native Linux/Darwin tests green, fs-ext wrapper-internal, and no P3 raw FD/import/cast/path factory), uses the exact addressed state artifact, and resumes the same ID by its durable pin phase while core is dead;
  5. every P3 request variant is exhaustively built by the opaque capability with fixed argv, the canonical exact-key base64 stdin wire, strict Python parser, bounded result and the actual writer FD 3/retained-root FD 4 pair; every argv target, including all four marker/readiness commands, exists in the built image; maximum 716,800-byte inventory round-trips through the 1,048,576-byte collector; DWH/Memory retain sources and reverify destinations; arbitrary spawn/argv/path, malformed stdin, missing/substituted/cross-root/post-callback/nested/concurrent capability misuse, and SIGKILL fail safely;
  6. after each successful non-activation command, terminal durability plus owner-only clear permits admission into one unmixed mode or expected migration_required; dwh-cache materializes the selected pre-READY cache and binding-qualified revision snapshot with zero READY/layout/semantic writes, and semantic-revision persists inventory/readiness with exactly zero replacement upserts/deletes;
  7. durable marker publication blocks a racing admission; multiple existing shared reader leases drain without being killed; the exclusive no-follow OS lease proves zero active readers and is held while activation publishes/verifies A/X replacements, publishes the global reader marker, deletes only unchanged exact legacy IDs, and publishes immutable A/X READY last while core may remain running;
  8. success durably records terminal state before the matching owner clears quiescence and releases exclusive ownership. SIGKILL releases OS locks but retains marker/run state; dead-core same-ID resume succeeds; wrong owner/digest and a second run cannot resume, mutate, or clear;
  9. every schema/LSH/search-pack/mschema reader resolves the selected binding snapshot, every Memory command uses the global root, and no direct old-root/sibling-binding probe remains after enablement;
  10. released registry pull alone receives writable registry/Git capability; repository lock precedes durable request_claimed, target_advertised, exact-OID target_fetched, and immutable exact A→B planned; complete changed IDs use P2 acquireOrProvision backed only by WorkspaceFsAtV1 and one callback-scoped root/writer/quiescence/readers set lexically; bootstrap and pull callbacks remain field-for-field P2; inspect/lazy-list/status use only bounded ensureBootstrapAddressed, whose locked active recheck returns the exact already_active snapshot without scan/network/state and whose absent branch performs automatic zero/create or one/resume selection. A three-caller initial-absence race publishes exactly once and all callers converge; incompatible active state or pull/multiple/mismatched/corrupt/churning/over-bound job sets fail closed before network/state; participants cannot reenter; active publication is all-or-nothing; and B/X sessions fail before B/X READY while ordinary prepare resolves only B/X;
  11. resumed B/X activation reuses X cache, publishes distinct revision state and immutable B/X READY, preserves A/X READY, and retains global Memory/solved retrieval;
  12. ordinary marker-before-exclusive ordering, new-admission race refusal, multi-reader drain, writer-before-reader order, typed shared/exclusive reader locking, publish-before-delete, partial-delete/after-READY SIGKILL resume, dead-core recovery, and owner-only clear are proved. Separately, dead-core registry SIGKILL spans before/after claim, advertisement, immutable-ref fetch, target_fetched, active rename/fsync, and terminal-before-clear. Same-ID pre-advertisement recovery may re-advertise; post-advertisement recovery keeps the recorded OID; at/after target_fetched it never uses the network. For bootstrap, every preterminal death is resumed through each actual inspect/lazy-list/status caller by the sole matching exact-identity run before any fresh advertisement; queued callers recheck and return the resulting exact already_active snapshot; and every incompatible-active/ambiguity/corruption/bound failure is stable and network/state-free. Exact mixed base/target reconciles, a third installation/repository/remote identity, create base mismatch, or missing/changed pinned target is rejected before network/state, and terminal replay succeeds even if a later independent pull moved tracking refs;
  13. after pull, migrate dwh-cache proves pre-READY cache plus binding-qualified snapshot materialization usable before P5 acceptance with no READY activation. At READY B/X, binding Y first refuses B admission and same-revision activation with zero mutation; then C is published/pulled, C/Y is prepared and activated, and C/Y is admitted while B/X bytes remain unchanged. Historical B/X is proved only by explicitly restoring installation X and admitting B/X, followed by restoring Y and admitting C/Y; transport, database, schema, and one root-affecting policy repeat on distinct successors with no second READY at one revision;
  14. corrupted owner, conflicting Memory registry, caller namespace conflict, unsafe marker/READY, partial migration, wrong --resume, and interrupted activation fail closed with stable codes and no secret leakage;
  15. safe reports pass secret scan and declared hashes; cleanup removes exactly owned Docker/filesystem state.

Step 3: Run the runner self-tests.

./scripts/test-p3-acceptance.sh

Expected: PASS.

Step 4: Commit.

git add scripts/p3-acceptance.sh scripts/test-p3-acceptance.sh backend/scripts/p3-acceptance.mjs .gitignore
git commit -m "test: add P3 clean-state process goal"

Task 12: Focused verification, full clean process run, retained report, and checkpoint

Files:

  • Create: docs/reports/2026-08-10-p3-effective-config-checkpoint.md
  • Modify: docs/testing/p2-p6-manual-verification.md
  • Modify: PROJECT_STATE.md
  • Create: scripts/lint-plan-shell-fences.py
  • Create: scripts/test-lint-plan-shell-fences.py

Step 1: Verify the source is clean before the evidence run.

git status --short
git rev-parse HEAD
git rev-parse 'HEAD^{tree}'

Expected: empty status and recorded commit/tree. If documentation/report changes are still uncommitted, commit them before running; the acceptance runner must not accept a dirty tracked tree.

Step 2: Run all P3-focused gates.

(cd harness && .venv/bin/pytest -q \
  tests/test_effective_dwh_binding.py tests/test_dwh_snapshot.py tests/test_dwh_owner_v2.py \
  tests/test_dwh_owner_migration.py tests/test_dwh_preprocess_job.py \
  tests/test_config_resources.py tests/test_portable_paths.py \
  tests/test_locked_child_stdin.py tests/test_layout_marker_commands.py tests/test_p3_internal_cli.py \
  tests/test_lsh_job_resume.py \
  tests/test_search_pack.py tests/test_schema_fk_annotations.py \
  tests/test_qdrant_vector_store.py tests/test_semantic_kind_isolation.py \
  tests/test_semantic_revision_migration.py tests/test_qdrant_cli_commands.py \
  tests/test_corpus_pipeline.py tests/test_memory_migration.py \
  tests/test_memory_promotion.py tests/test_memory_save_one.py \
  tests/test_solved_question.py tests/test_solved_search_cli.py \
  tests/test_repository_memory_sql_paths.py)
(cd harness && .venv/bin/ruff check \
  tht/effective_dwh.py tht/config.py tht/cli/config_cmd.py tht/jobs/dwh_pipeline.py \
  tht/paths.py tht/dwh_snapshot.py tht/dwh_owner.py tht/dwh_migration.py \
  tht/cli/preprocess_cmd.py tht/vectorstore/records.py tht/adapters/vector/qdrant.py \
  tht/ports/vector.py tht/adapters/factory.py tht/search/evidence.py \
  tht/semantic_migration.py tht/cli/vector_cmd.py tht/memory_migration.py \
  tht/cli/memory_cmd.py tht/memory.py tht/solved.py tht/cli/schema_cmd.py \
  tht/cli/lsh_cmd.py tht/taskdoc.py tht/locked_child_stdin.py tht/layout_markers.py \
  tests/test_dwh_preprocess_job.py tests/test_effective_dwh_binding.py \
  tests/test_config_resources.py tests/test_portable_paths.py tests/test_dwh_snapshot.py \
  tests/test_dwh_owner_v2.py tests/test_dwh_owner_migration.py tests/test_search_pack.py \
  tests/test_qdrant_vector_store.py tests/test_semantic_kind_isolation.py \
  tests/test_qdrant_cli_commands.py tests/test_corpus_pipeline.py \
  tests/test_memory_save_one.py tests/test_solved_question.py \
  tests/test_semantic_revision_migration.py tests/test_memory_migration.py \
  tests/test_memory_promotion.py tests/test_solved_search_cli.py \
  tests/test_repository_memory_sql_paths.py tests/test_locked_child_stdin.py \
  tests/test_layout_marker_commands.py tests/test_p3_internal_cli.py tests/test_lsh_job_resume.py \
  tests/test_schema_fk_annotations.py)
(cd backend && npx vitest run \
  test/workspace-revision-layout.test.ts test/workspace-runtime-renderer.test.ts \
  test/workspace-runtime-config-lease.test.ts test/workspace-runtime-handoff.test.ts \
  test/workspace-effective-config-equivalence.test.ts test/workspace-lock-root-lease.test.ts \
  test/workspace-preprocessing-state.test.ts test/workspace-preprocessing-service.test.ts \
  test/workspace-maintenance.test.ts test/workspace-registry.test.ts \
  test/workspace-registry-addressed-publication.test.ts \
  test/workspace-registry-addressed-process.test.ts test/workspace-reader-lease.test.ts \
  test/routes-workspaces.test.ts test/routes-sessions.test.ts \
  test/pi-process-manager.test.ts test/tht-runner.test.ts && \
  npx tsc --noEmit -p . && npm run build)
(cd backend && npx tsc --noEmit --target ES2022 --module ES2022 \
  --moduleResolution Bundler --strict --skipLibCheck \
  test/registry-pull-job-imports.compile.ts)
(cd tools/thothctl && test "$(go env GOVERSION)" = "go1.26.5" && go test ./...)
./scripts/test-verify-workspace-install-docs.sh
./scripts/test-p3-acceptance.sh
python3 scripts/test-lint-plan-shell-fences.py
python3 scripts/lint-plan-shell-fences.py \
  docs/superpowers/plans/2026-08-10-p2-host-workspace-preprocessing-cli.md \
  docs/superpowers/plans/2026-08-10-p3-effective-config-fingerprint.md

The plan-shell linter extracts every fenced bash/sh block byte-for-byte, rejects shell blocks containing angle-bracket placeholders, and runs bash -n on each block independently. Deliberate metavariables may remain only in text fences; runnable shell must use quoted environment checks such as ${RUN_ID:?message} and an explicit regex validation. Its self-test includes unterminated quotes, redirection-shaped <placeholder>, heredocs, and both P2/P3 documents.

Expected: every command exits 0. Record exact counts and versions, including Go 1.26.5, in the checkpoint report; no offline gate may download another Go toolchain. Do not run the full unrelated repository suites here; aggregate/full-stack verification belongs after P6 unless a touched-layer regression requires it.

Step 3: Run the complete process once, without retry.

./scripts/p3-acceptance.sh integration --keep

Expected: exit 0, one new .artifacts/p3-effective-config/<run-id>/report.json, matching report.md, ownership manifest, overall: PASS, automated integration: PASS, secret scan PASS, cleanup test PASS, and all fifteen assertions above PASS. Do not rerun a failure blindly: diagnose, add a regression test, fix, commit to regain clean state, then perform a new complete run with a new run ID.

Step 4: Independently validate retained evidence.

Use a small bounded script to parse report.json, verify every declared SHA-256, compare source commit/tree to current HEAD, confirm no undeclared public files, scan report/stdout/stderr/public manifests for every fixture canary, and query Docker by exact ownership labels. Then exercise retained cleanup:

RUN_ID="${RUN_ID:?export RUN_ID as the retained 32-hex acceptance run ID}"
[[ "$RUN_ID" =~ ^[0-9a-f]{32}$ ]]
./scripts/p3-acceptance.sh cleanup --run "$RUN_ID"
./scripts/p3-acceptance.sh cleanup --run "$RUN_ID"

Expected: first cleanup removes exactly declared containers/networks/volumes/temp roots and writes cleanup PASS; second reports idempotent already_clean; sanitized reports remain. Confirm no owned listener accepts connections.

Step 5: Write and commit the automated checkpoint report.

docs/reports/2026-08-10-p3-effective-config-checkpoint.md must record commit/tree, tool versions, focused command counts, retained report path and SHA-256, automated result, cleanup result, known unrelated debt (if any), and manual acceptance: PENDING. Update the P3 manual header and PROJECT_STATE.md with the same retained evidence and explicitly state that P4 is blocked pending reviewer approval.

git add docs/reports/2026-08-10-p3-effective-config-checkpoint.md \
  docs/testing/p2-p6-manual-verification.md PROJECT_STATE.md \
  scripts/lint-plan-shell-fences.py scripts/test-lint-plan-shell-fences.py
git commit -m "docs: record P3 automated verification"

Because that documentation commit is after the retained source commit, do not claim the report is bound to the doc commit; record both precisely. If policy requires a report bound to final docs too, run a fresh clean acceptance once and replace the retained reference rather than editing provenance.

Step 6: Stop for the human checkpoint.

Send the reviewer:

  • retained report path and hashes;
  • concise architecture summary;
  • focused verification results;
  • exact P3 manual walkthrough section;
  • explicit choices PASS, FAIL with notes, or DEFER.

Do not begin P4. After the reviewer runs the walkthrough in a new environment and explicitly approves, update Decision: PASS, manual acceptance: PASS, PROJECT_STATE.md, and the checkpoint report in one scoped documentation commit:

git add docs/testing/p2-p6-manual-verification.md \
  docs/reports/2026-08-10-p3-effective-config-checkpoint.md PROJECT_STATE.md
git commit -m "docs: record P3 manual acceptance"

Only that explicit decision completes P3 and unblocks P4.