# Changelog (0.8.8.7 and older) Releases 0.8.8.8 and later are in [CHANGELOG.md](CHANGELOG.md). ## [0.8.8.7] - 2026-09-19 ### Added - **Skills: `npx skills add … -a ziya` installer contract pinned, and the upstream registration PR prepared** (`tests/test_skill_installer_contract.py` (new, 4), `design/upstream/vercel-skills-ziya-agent.md` (new), `Docs/Skills.md`). The `vercel-labs/skills` CLI (`npx skills`) has no discovery protocol — each supported agent is a hardcoded `src/agents.ts` entry keyed on a detection probe, a project skills dir and a global skills dir. Ziya already satisfies all three (`~/.ziya` / `$ZIYA_HOME` created on first run; `.agents/skills` in `DISCOVERY_PATHS`; `~/.ziya/skills` via `discover_user_skills`), so registration is a small upstream PR; `design/upstream/vercel-skills-ziya-agent.md` holds the exact `agents.ts` / `types.ts` snippets, the README link, the PR text and the post-merge verification commands, following their `AGENTS.md` "Adding a New Agent" steps. `skillsDir` is the universal `.agents/skills` (not `.ziya/skills`) so a no-flag install and a skill already installed for Cursor/Codex in the same project are both visible to Ziya without duplication. The CLI's default install method is symlink; the new tests assert discovery follows a symlinked skill directory in both `.agents/skills` and `$ZIYA_HOME/skills`, still rejects a link whose name differs from the SKILL.md `name`, and that resolving `$ZIYA_HOME` creates the directory the installer probes. `Docs/Skills.md` now lists every discovery root with its precedence and documents the install commands, leading with `-g`: project-scope installs write into the current directory's `.agents/skills/` and are the right shape only for repo-specific skills; general-purpose skills belong in one global copy rather than a drifting copy per project. - **Skills: `~/.agents/skills` is scanned as a user-global root** (`app/services/skill_discovery.py` (`USER_HARNESS_PATHS`), `tests/test_skill_installer_contract.py` (+2)). `npx skills add -g` for every "universal" agent (Cursor, Codex, Copilot, Cline, Zed, …) places the canonical copy in `~/.agents/skills/` and creates no per-agent link (`installer.ts` short-circuits to the canonical dir), so a skill installed globally for any of those was invisible to Ziya even though the project-level `.agents/skills` was already read. Precedence is unchanged: `~/.ziya/skills` still wins over it by name, project roots still beat user roots. `~/.config/agents/skills` (Amp/OpenCode's listed global dir) is deliberately not added — the installer never writes there for those agents. - **Document export: typeset aesthetics — numbered figure/table captions, section numbering, a real title block, a document footer, and typeset-grade pagination** (`app/utils/document_print_decor.py` (new: injected document stylesheet, browser-side decoration pass, document footer, `format_document_date`, `page_content_box_px`), `app/utils/document_ir.py` (`subtitle`, `date`, `numbering`, `layout: titlepage`), `app/services/pdf_exporter.py` (`capture_pdf` gains `media` / `viewport` / `pre_capture_css` / `pre_capture_js`; `_KEEP_WITH_NEXT_JS` chains stacked headings and binds to tables, code, figures and display math; `export_document_pdf` drives the layer under print media), `frontend/src/components/PrintRenderPage.tsx` (subtitle, server-formatted date, `titlepage` class), `app/data/built_in_skills.py` (`document_authoring`: caption convention, layout guidance, pagination rules of thumb), `Docs/DocumentAuthoring.md`, `tests/test_document_print_decor.py` (new, 22: helpers, real-Chromium decoration and keep-with-next passes, PDF text-layer header-repeat check), `tests/test_document_export.py` (+2 seam)). A rendered report read like a chat screen dump: no figure or table labels, a `9/19/2026` locale date under a title no larger than the body `h1`, every diagram floating in the renderer's padded screen box (graphviz stretched to `width:1280px`, poster-sized glyphs), full-grid tables, a marketing tagline as the footer, and every table atomic — so a page-tall table jumped to a fresh page behind a blank band. Now: a `Figure:` paragraph under a diagram or a `Table:` paragraph over a table becomes a numbered caption (`Figure 2. …`; label alone when no line is given, `numbering.figures/tables: false` to opt out); `numbering.sections` prefixes `1 / 1.1 / 1.1.1` into headings AND the PDF bookmarks; the title block gets subtitle, `Author · September 19, 2026` byline and a rule (`layout: titlepage` gives it its own page); body headings step down from the title; diagrams are centred at natural size (viewBox-clamped, points→px for graphviz) with the renderer's reserved height, padding and hover toolbar collapsed; tables get a header rule, hairline rows, zebra fill and tabular digits, and one taller than 45 % of the page flows across pages with the `` repeated; the footer reads `Title · Author · Date … Page N of M` with the tool credit demoted to a lighter second line. Two driver findings underpin the pagination: Chromium repeats table headers ONLY under print media (under the pipeline's `emulate_media("screen")` the header appears once — verified in isolation and asserted on the PDF text layer), so documents now render under print media (the served bundle's only `@media print` rule drops Prism's text-shadow); and the in-page heuristics were measuring a 1224 px screen layout while the PDF paginates at ~673 px, so the document viewport is now sized to the printable box. The decoration pass runs from the driver before keep-with-next and the outline sentinels (one pagination for every capture), passes document text as an `evaluate` argument rather than splicing it into code, and is idempotent; where D3Renderer's resize observer rewrites an inline height after the box collapses, the stylesheet's `!important` — not the pass — owns the collapse. Transcript export is untouched apart from the shared keep-with-next improvement. - **Task cards: model-staged cards are conversation-only by default — they no longer accumulate in the permanent deck** (`app/mcp/tools/task_card_stage.py` (`persist` flag; creates with `draft` unless persisting; promotes rather than strands when no binding can be written), `app/mcp/tools/task_card_tools.py` (`task_card_list bound_to_current_chat` includes drafts; `conversation_only` on each row), `app/storage/task_cards.py` (`prune_stale_drafts` keeps any draft still referenced by a binding), `app/storage/task_bindings.py` (`all_bound_card_ids`), `app/data/built_in_skills.py` (task_cards skill text), `frontend/src/components/TaskCard/TaskCardInlineTile.tsx` (staged tile: `conversation-only` tag + **Save to deck**), `TaskCardsLibrary.tsx` (**Add to deck** for an unlisted card opened by id), `tests/test_task_card_stage_tool.py` (+4), `tests/test_task_card_drafts.py` (+3), `frontend/src/components/__tests__/stagedCardConversationOnly.test.ts` (new)). Models are now encouraged to use `task_card_stage` to hand sub-tasks off — to parallelize and to keep the parent conversation's context small — and every one of those cards was landing in the Task Cards library as a permanent artifact. The tool now files a card as an unlisted draft (the same lifecycle the fenced `task-card` proposal already used): its tile, run, bindings and the model's own `bound_to_current_chat` listing all resolve it by id, but the deck never shows it. `persist: true` files it in the deck up front; the staged tile's **Save to deck** (and the deck editor's **Add to deck**, reachable via a tile's Edit backlink) promote in place — an update, so block ids and any signature obtained against them survive. Two guards keep a card from becoming unreachable: with no conversation to bind to, or if writing the binding fails, the card is filed in the deck instead; and the week-old-draft GC now checks reachability, not just age, so a staged tile the user has not clicked yet keeps its card. `/goal` still files its synthesized card in the deck — unchanged here. - **Docs/self-description remediation: capability glossary, over-claim corrections, non-goals, a rewritten competitive analysis, and a conformance test that keeps the docs honest going forward** (`Docs/CapabilityGlossary.md` (new, generated from the ledger `aliases` field, ~30 hand-annotated terminology entries naming the competitor term where one exists), `Docs/Capabilities.md`, `Docs/FeatureInventory.md`, `Docs/DesignPhilosophy.md` (new "What Ziya deliberately does not do" section, 28 non-goals with rationale and cost), `Docs/competitive-analysis.md` (replaced; derived from the corpus with the absent/not_applicable/unknown split and a provenance line naming the source runs), `Docs/CONTRIBUTING.md` (new Documentation Conventions subsection), `tests/test_docs_ledger_conformance.py` (new), `scripts/complandscape_docdefects.py`, `.ziya/complandscape/60-synthesis/s1-20260910/DOC-DEFECTS.json`). Closes out the plan in `design/docs-self-description-remediation.md`: ~20 ledger over-claims and Critic-A's 12 inflated scores were qualified or removed before anything was added (WS1); 107 mature, competitively-relevant capabilities that were shipped but absent from the docs got one `FeatureInventory.md` line and one `Capabilities.md` paragraph each, at the critic-corrected ledger maturity, never above it (WS3); 28 deliberate non-goals — previously indistinguishable from ordinary gaps — are now stated as decisions with a one-line rationale and cost (WS4); the vocabulary layer readers were missing (searching by capability name or a competitor's term and finding nothing) is now a generated glossary plus inline "also known as" lines on the 24 confirmed terminology artifacts (WS2). `tests/test_docs_ledger_conformance.py` is the anti-recurrence check: every ledger capability at or above the critic-corrected mature threshold must be findable in `Docs/` by id, name or alias (391 checked, 0 missing — coverage residue is 0, no deliberate-exclusion exemptions were needed), and the four `over`-class rows WS1 closed with a `removed_sentence` are asserted absent from their target docs by exact (whitespace-normalised) text match, so a reintroduced over-claim fails the suite instead of drifting back in silently. One residual finding from this pass, not yet fixed: the generated region of `CapabilityGlossary.md` is stale against a fresh regeneration (~30 links point at `Capabilities.md` rather than the section anchors added to it since the glossary was last written) — tracked for a `scripts/complandscape_docdefects.py glossary --write` re-run rather than papered over in the test. - **Chat turns run as a server-owned relay, so a reload or a second window reattaches to the live stream** (`app/agents/chat_turn_relay.py` (new), `app/routes/misc_routes.py`, `app/server.py` (reattach/status endpoints), `frontend/src/components/App.tsx`, `frontend/src/components/ChatTurnReattachWatcher.tsx` (new), `frontend/src/utils/chatTurnReattach.ts` (new), `tests/test_chat_turn_relay.py` (new), `tests/test_chat_turn_endpoints.py` (new)). Each chat turn now executes as a server-owned task that buffers its output; the browser subscribes to that relay rather than owning the SSE stream directly, so reloading the page or opening the conversation in another window reattaches to the turn still in flight instead of dropping the in-progress response. Client stop and throttled-retry are routed through the relay's `cancel_turn` / `start_turn` so they behave identically to a first attempt. - **Plugin interface surface extended** (`app/plugins/interfaces.py`, `app/plugins/__init__.py`). Additional extension hooks on the plugin interface. ### Fixed - **MCP: a server installed from the registry now appears in the MCP status view immediately, without a manual reinitialize** (`app/mcp/manager.py` (`_restart_server_locked` registers `new_config` in `server_configs`, preserving a builtin's flag), `frontend/src/components/MCPStatusModal.tsx` (the `mcpStatusChanged` handler refetches at once, silently, instead of after a fixed 1 s settle), `tests/test_mcp_restart_registers_new_server.py` (new, 4)). The install path connected the new server via `restart_server(name, config)`, which created the client and started it — tools live, install reported as success — but only wrote `self.clients`; `get_server_status()` and the modal's server list both enumerate `self.server_configs`, which had never seen the new entry, so the server was absent from every listing until a full reinitialize re-read `mcp_config.json` from disk. The seam test starts a server from a config the manager does not hold and asserts it is present in `get_server_status()` (red before the fix, green after); a second asserts a builtin restarted with a new config keeps `builtin: True` so it does not migrate into the user-servers section. The frontend delay was compensating for nothing — every dispatcher of the event awaits its backend call first — and could catch a slow handshake mid-way and paint `Disconnected`. - **MCP registry: installed services are listed in the shape the UI reads, and hand-configured servers correlate to a registry entry deterministically** (`app/utils/mcp_installed_listing.py` (new: `installed_entry_to_api`, `correlate_server_to_registry`), `app/routes/mcp_routes.py` (`GET /registry/services/installed`), `tests/test_mcp_installed_listing.py` (new, 11 incl. 2 route seam tests)). The route appended the registry manager's snake_case records (`server_name`, `service_id`) to a list the frontend's camelCase `InstalledService` type consumes, and checked `svc.get('serviceId')` against them — so the accounted set was always empty, `ServiceCard` dropped every registry install (it returns null without `serviceId`), `isServiceInstalled` never matched one, and each install reappeared only through the substring-correlation fallback, tagged "Manually Configured" and bound to whichever of ~10k registry entries the aggregator happened to return first (`slack-mcp` resolved to `jtalk22/slack-mcp-server` on one call and the internal `slack-mcp` on the next; `quip_mcp` to `bug-breeder/quip-mcp`). Registry records are now converted to the API shape, servers whose config carries `registry_provider` are never re-derived by name, and correlation accepts only exact correspondences (normalised server name equal to the service id, its last path segment, or the service name, or an identical repository URL), ranked by match tier then id so the result is order-independent; substring matches are refused (`fetch` is not "Secure Fetch"). - **MCP registry: README-sized service descriptions are reduced to a one-line headline wherever a name or summary is displayed** (`app/mcp/description_summary.py` (new), `frontend/src/utils/mcpDescriptionSummary.ts` (new, mirror), `app/mcp/registry_manager.py` (`_add_to_config` summarises `description` before writing `mcp_config.json`; `_get_installed_from_config` summarises legacy blobs on read), `frontend/src/components/MCPStatusModal.tsx` (`getServerDisplayName`), `ServiceCard.tsx` (headline collapsed, markdown-rendered full text expanded), `MCPRegistryModal.tsx` (tool-search results, preview drawer), `tests/test_mcp_description_summary.py` (new, 16), `frontend/src/utils/__tests__/mcpDescriptionSummary.test.ts` (new, 9), `tests/fixtures/mcp_descriptions/` (real registry payloads)). Some registry providers return the server's entire README as `serviceDescription` — the Slack MCP entry is 20 KB of markdown, and the installed `quip_mcp` entry carried a 1.8 KB tool catalogue. Nothing in the pipeline reduced it: providers copied it verbatim into the config's `description`, the installed-services list used `description` as the service *name*, and the MCP status panel used it as the server header, so a panel row read `## Tools (37 total, 3 access tiers) …` where a name belonged. The summariser takes the first paragraph that reads as prose (skipping headings, `a | b | c` bylines, rules, lists, tables, code fences and awesome-list badge residue), keeps its lead sentence(s), and caps at 160 characters on a word boundary; run over all 10,256 entries the local registry currently serves it produced no empty results. Both seams are at shared chokepoints so closed-plugin providers are covered; the two seam tests fail against the unpatched manager. Existing configs that already hold a blob display correctly without editing (the read seam) and are rewritten on reinstall. - **Shell tool: globs follow the in-process `cd`, and `VAR=value` assignments store the expanded value** (`app/mcp_servers/shell_server.py` (`_expand_and_tokenize` gains `cwd`, globs via `glob.glob(pattern, root_dir=cwd)`; `_execute_pipeline` expands each peeled assignment RHS through `_expand_vars` + `expanduser` before recording it and passes `seg_cwd` to the tokenizer), `tests/test_shell_glob_cwd_and_assignment_expansion.py` (new, 9)). `cd /abs/pkg && cp src/*.ts /tmp/out/` failed with "No such file": the glob was resolved against the server process's cwd rather than the directory the executor had just switched to, so the literal pattern reached `cp`. Separately, `P=$HOME/pkg; cp $P/a b` handed `cp` a path still containing the literal `$HOME`, because `_peel_env_prefix` recorded the raw RHS and the later `$P` expansion is a single pass. Both now match shell semantics; unmatched globs are still passed through verbatim, absolute patterns are unaffected by cwd, and assignments on one segment resolve left to right (`A=x B=$A/y cmd`). - **Shell config: the "Temporary grant staged" banner now lists what it is staging** (`app/routes/mcp_routes.py` (`_session_pending_status` (new); GET `/shell-config` returns `sessionPendingDelta`; `request-session-grant` returns `pendingDelta`), `frontend/src/components/ShellConfigModal.tsx` (`sessionStagedDelta` state, hydrated from both responses and cleared on discard; banner renders one `field: values` line per entry, or says the request is floor-only), `tests/test_shell_config_session_pending_delta.py` (new, 7), `frontend/src/components/__tests__/shellConfigEscalationUX.test.ts` (+4)). The persistent "Unsigned privilege escalation" banner has always listed its beyond-floor delta; the staged temporary banner only said *that* something was staged, so a user about to run `sudo ziya-approve --session` could not see what they were about to sign for. The delta is computed server-side from `~/.ziya/pending_session_shell.json` with the same canonical code (`scope_canonical`) the persistent banner uses, so the two banners list like for like. A present-but-unreadable pending file still reports `sessionPending: true` with an empty delta so Apply/Discard stay reachable. Not addressed here: a stale pending file can coexist with an unsigned persistent escalation (Save clears the pending file, but other writers of `mcp_config.json` do not), in which case both banners still render. - **Task runs: a run stuck inside a block can now be force-stopped in place and resumed from the interrupted block** (`app/agents/live_runs.py` (new), `app/api/task_cards.py` (retain the run's `asyncio.Task`; `except asyncio.CancelledError`), `app/storage/task_runs.py` (`mark_aborted`), `app/api/task_runs.py` (`POST /cancel?force=true`), `frontend/src/components/TaskCard/runControls.ts` (`canForceAbort`), `liveActivity.ts` (`HUNG_AFTER_S`, `isHung`), `TaskCardInlineTile.tsx` (Force stop button), `holdChain.ts` / `RunRecoveryBanner.tsx` (`user_abort` wording), `services/taskRunApi.ts`, `tests/test_task_run_force_abort.py` (new, 13), `frontend/src/components/TaskCard/__tests__/forceAbort.test.ts` (new, 10), `Docs/Capabilities.md`, `design/task-cards.md`). Soft-cancel sets `cancel_requested` and relies on the executor reading it at the next block boundary; a run whose in-flight Task never returns — GFX Stage 2 run `b893cf07` streamed "starting build verification in frontend/" and then nothing for six hours, with cancel pressed two hours in — never reaches one, and four earlier runs of the same card show the same `npm run build`-then-silence signature. The launch path dropped the `asyncio.create_task` handle, so there was no in-process lever left: the cancel button was a no-op, and the run read `running` for as long as the server stayed up. The handle is now kept per run; `?force=true` cancels the coroutine, waits briefly for it to unwind, and the run is recorded as `held` / `user_abort` at the innermost running block — the same shape `mark_held` and the restart reconciler write, so the existing recovery banner offers resume-from-block and only the interrupted block is redone. The coroutine distinguishes a force-stop from an event-loop shutdown via recorded intent, so a server stop still leaves the record to the startup reconciler. The tile swaps Cancel for a labelled **Force stop** once a requested cancel has not landed, or after ten minutes of silence (`HUNG_AFTER_S`, deliberately well above the two-minute amber threshold so a legitimate long build is not offered for interruption). The end-to-end test launches a card against a model stream that hangs forever, asserts that plain cancel leaves it `running` with the lock held, then asserts force holds it at the leaf and releases the executor lock — a held record over a still-live executor would have been a zombie with a nicer label. Not addressed here: why the underlying `npm run build` tool call never returned despite its 1200 s shell grant. - **Task cards: a card the model launches with `task_card_launch` now shows its monitor tile in the launching conversation** (`app/mcp/tools/task_card_launch.py`, `frontend/src/apis/chatApi.ts`, `tests/test_task_card_launch.py` (+3), `frontend/src/apis/__tests__/taskCardStageSync.test.ts` (rewritten, 6)). The chat renders task monitors from `TaskBinding` records, not from runs; every human launch path (tile Run button, `/goal`, the bindings endpoint) writes one, but the tool called `_launch_run_for_card` directly and wrote none, so a model-launched run executed with no tile, no gear affordance and no inspector reachable from the chat — visible only under the project's run list. The tool now records an unanchored binding carrying the run id (the same shape `task_card_stage` writes, minus `run_id=None`) and returns `binding_id`; the stream handler fires `task-binding-created` for `task_card_launch` results as it already did for `task_card_stage`, so the tile appears mid-turn rather than after a reload. Headless launches (no conversation in context) still succeed and say where to find the run. The new seam test drives the tool against the real `TaskBindingStorage` in a tmp project dir and asserts the chat reads back exactly one bound binding; it fails against the unpatched tool. The frontend seam test had been red since its scoping anchor — a comment in the next handler — was reworded; it now brace-matches the `if` that names the tool and covers both tools. - **Prompt caching: the system-prompt prefix is now byte-identical from one turn to the next; per-turn volatile text (bead status, live memory counts and handles) moves to the current user message** (`app/utils/precision_prompt_system.py` (unconditional `## Message Timing` paragraph; `volatile_tail` collected during assembly and appended to the last user message after history is inserted), `app/memory/prompt.py` (`get_memory_prompt_sections()` returning `(stable, volatile)`; `get_memory_prompt_section()` kept as the byte-identical combined form), `tests/test_prompt_volatile_tail.py` (new, 6), `tests/test_message_timestamps.py` (assertion narrowed to `.lock` held for the life of the executor coroutine: a sibling's lock is held, a crashed owner's is not, and the reconciler skips the former. What it writes for the latter is `held` / `server_restart` with `held_at_block_id` = the innermost running block — the same shape `mark_held` writes for a credential fault — so the recovery banner, Retry / Continue and the banked-iteration prefix apply with no special case; reconciling to `failed` had left `block_states` at `running`, so `firstFailedBlock` found nothing and the only control was Restart. The cancel endpoint's `is_active` check was always false (per-request storage instance, process-local set), so every cancel force-wrote `cancelled` over a live run; it and the two resume guards now probe the lock. Windows has no `flock` and degrades to the previous behaviour. - **Task-card self-improvement: a block that keeps stopping for the same non-text reason is one finding in the Lessons panel, not N rows** (`app/utils/self_improve.py` (`stop_streaks`, `STOP_STREAK_MIN`; `summary_by_card` gains `stop_streak`), `app/api/task_cards.py` (`stop_streaks` on the card-lessons payload), `frontend/src/components/TaskCard/lessonStreaks.ts` (new), `LessonsPanel.tsx`, `TaskCardsLibrary.tsx`, `task-card-editor.css`, `frontend/src/services/taskCardApi.ts`, `tests/test_self_improve_stop_streaks.py` (new, 14), `frontend/src/components/TaskCard/__tests__/lessonStreaks.test.ts` (new, 6), `lessonsPanel.test.tsx` (+1)). A `stop` verdict is the judge saying the run failed for a reason editing the card cannot fix — the one ledger outcome that needs a *person*. GFX Stage 2's repair block stopped six runs in a row over two weeks (the headless render server does not pick up a rebuilt bundle), each rationale worded differently, and the panel showed six orange rows with nothing saying "this has been blocked for two weeks". The ledger now computes, per block, the trailing run of consecutive stops (a judge `error` is transparent — the judge failed, the run did not pass; any `accept`/`revise` resets) and returns streaks of two or more as an overlay on the card payload. The grouping is positional, not textual: no string compare groups those six wordings, and clustering ledger rows with a model call is not worth its cost or non-determinism — "stopped N runs in a row and nothing has passed since" is the finding regardless of wording. The panel folds a streak's members into one expandable row headed `stopped N runs in a row` with the newest rationale leading; every member row is still rendered inside it, so nothing readable or revertable is hidden. The deck badge turns orange with `⛔N` for a card in a streak. Tests pin the real GFX2 sequence (stop, accept, error, accept, six stops → a streak of six, not seven), that a single stop is a row not a streak, and that the overlay reaches the HTTP payload with all rows intact. - **Task-card self-improvement: a revise no longer has to rewrite whole fields, and the judge reply can no longer be truncated by its own patch** (`app/utils/self_improve.py` (`resolve_improve_patch`, `PATCH_OPS`, `JUDGE_MAX_TOKENS`), `app/agents/improve_evaluator.py`, `app/agents/block_executor.py`, `design/task-card-self-improvement.md`, `tests/test_self_improve_targeted_patch.py` (new, 14)). A `revise` verdict had to carry the FULL replacement text of every field it touched, and the judge's reply was capped at 2,000 tokens. Measured against the GFX Stage 1 card, the largest editable field is ~1,100 raw tokens before JSON escaping, so a reply that touched two fields plus a rationale and lesson could not fit — the replies most likely to arrive cut off and be recorded as a judge failure were exactly the ones that tried to revise, which is consistent with a ledger holding 20 verdicts, 4 judge failures and 0 revisions. A patched field's value may now be a list of targeted ops — `{"op":"replace","find":…,"with":…}` (`find` must occur exactly once in the current text; zero or many matches refuse the patch rather than guess) and `{"op":"append","text":…}` — which the executor resolves to full text against the current subtree BEFORE validation, so the field whitelist, the oscillation hash, pre-image capture, apply, persist and the ledger record all see one format: the same edit written as ops or as a string has the same `patch_hash`, and the revert path is untouched because it never sees an op. A patch with any unresolvable op is refused whole (`invalid_patch`). The prompt tells the judge to prefer ops, and the reply ceiling is now 8,000 tokens so a full replacement still fits when most of a field genuinely must change. 4 of the 14 tests fail against the unpatched executor/evaluator; the seam test drives an op-form patch from a stubbed judge through `_maybe_self_improve` and asserts the level restarts with the resolved text and the ledger carries full text, its pre-image and the matching hash. - **Task-card self-improvement: the judge now sees every stage of the level it grades, not the last iteration's self-report** (`app/models/task_card.py` (`Artifact.stages`), `app/agents/block_executor.py` (`_execute_sequence`, `_execute_parallel`, `_execute_repeat`, `_execute_until`, `_maybe_self_improve`), `app/agents/improve_evaluator.py`, `app/utils/self_improve.py` (`stage_evidence`, `stage_counts`, `render_stages_for_judge`, `render_outputs_for_judge`), `tests/test_self_improve_judge_evidence.py` (new, 16), `design/task-card-self-improvement.md`). Second finding of the GFX Stage 1 audit (the first was the judge-failure-as-accept fix in 0.8.8.6): a repeat block's artifact carries `summary` and `failed` from its LAST iteration only — last-wins is load-bearing for `{{previous}}` and `on_failure`, so it stays — and a group inherits the same single summary from its last child. The judge for the card's 20-engine sweep was therefore asked whether *every* engine met the criterion while being shown one engine's self-authored prose, `self_assessment: (none)` (the field is leaf-only) and no outputs at all; it accepted every time. Container artifacts now carry `stages`: one `{index, label, status, summary (capped 600), self_assessment, outputs}` entry per iteration (repeat/until), branch (parallel) or child (group), written at every site an iteration concludes — executed, banked, synthesized-failure, replayed — alongside the loop's own `iter_outcomes` so the two cannot disagree, and set explicitly on a group so a nested loop's iterations never leak upward through the last-wins `model_copy`. The judge prompt lists every non-passed stage in full (passed stages cap at 24 then count, so a wide fan-out cannot push its own failures out of the prompt), names the file parts in `outputs`, and is told that `failed` is last-stage-only, that a stage summary is the agent's claim rather than a measurement, and that an objective naming deliverables is unmet when `OUTPUTS` shows none. The ledger record carries the stage counts so a reader can tell an accept over 20/20 from one over 17/20. Tests: 11 of 16 fail against the unpatched executor/evaluator (a 3-iteration repeat whose middle iteration failed must reach the judge as "1 failed"; a group's stages must be its two children, not the nested repeat's three iterations; `on_failure=stop` marks the skipped children); the end-to-end seam test drives `_maybe_self_improve` and asserts the failed stage's summary appears in the judge's user message and the counts land in the ledger record. - **GFX Vega / Vega-Lite rendering fixes** (`frontend/src/plugins/d3/vegaPlugin.ts`, `vegaLitePlugin.ts`, `vegaRecovery.ts`, `vegaGraphSanitizer.ts`, `vegaLayerDefaults.ts`; `tests/gfx_corpus/vega`, `tests/gfx_corpus/vega-lite` + regression tests). Categorical palette reach, group/facet title layout, low-opacity scatter parity, long-label bar transpose, rangeless ordinal legend, side-axis title gap, band-axis colour, and sizing recovery (defects Ge2d5a2, Gb594ca, Ge0f221, Ge8d304, G546ae4, G5f6d4a, Ga1ef6a, Gebb648, G9ee008, G6d6c01, G67dceb, D258, D508, D511). - **GFX Mermaid rendering fixes** (`frontend/src/plugins/d3/mermaidPlugin.ts`, `mermaidEnhancer.ts`; `tests/gfx_corpus/mermaid` + tests). ER empty-fill markers, pie layout/scale, pie title+legend text, dark link stroke, autonumber/stress, border edge markers, quadrant & mindmap legibility, and sequence box title / subgraph end (G_c415f5, G40, G14a672, G961e2c, G4e869b, Gd34f80, D420, D421). - **GFX DrawIO rendering fixes** (`frontend/src/plugins/d3/drawioPlugin.ts`, `drawioEnhancer.ts`, `drawioShapes.ts`; `tests/gfx_corpus/drawio` + tests). Stranded canvas labels, deduped cell ids, default edge markers, viewport fit, marker floor, quoted-geometry waypoints, rotation labels, and route-fix repair (G5798aa, G3a3c03, G796ea6, Gb7a537, Gbb997d, G436a36, Ga59f77, D091). - **GFX JointJS rendering fixes** (`frontend/src/plugins/d3/jointPlugin.ts`, `jointLinkRouting.ts`, `jointShapeResolver.ts`; `tests/gfx_corpus/joint` + tests). Label contrast, label fit height, paper collision, self-loop and label handling, v1 body attrs, and shape resolution / link routing (G159927, G17, G814644, Gba30e7, Gec73fa, D130, D411, D419). - **GFX network-diagram rendering fixes** (`frontend/src/plugins/d3/networkDiagram.ts`; `tests/gfx_corpus/network` + tests). Layout, self-loop mesh handling, and regression coverage (G8b89de, G5e174d, G_bce22e). - **GFX force-directed rendering fixes** (`frontend/src/plugins/d3/forceDirectedPlugin.ts`; `tests/gfx_corpus/force-directed` + tests). Grouping, responsive SVG sizing, and regression coverage (G05, G181c6f, G497189, G2ca2a0). - **GFX chord rendering fixes** (`frontend/src/plugins/d3/chordPlugin.ts`; `tests/gfx_corpus/chord` + tests). Arc stroke and empty-matrix handling (G11, G0c278d). - **GFX D2 rendering fixes** (`frontend/src/plugins/d3/d2Plugin.ts`; `tests/gfx_corpus/d2` + tests). ELK flat node sizing, fence render path, flat-layout arrowheads, label-delimiter edge background, and card-marker rejection (G09b125, G94e293, G16470a, Ga05f90, Ge8868b, G7b82d2). - **GFX Graphviz rendering fixes** (`frontend/src/plugins/d3/graphvizPlugin.ts`; `tests/gfx_corpus/graphviz` + tests). Forced-undersize handling and cluster contrast (G48, D118, D402). - **GFX Plotly rendering fixes** (`frontend/src/plugins/d3/plotlyPlugin.ts`, `plotlyPreprocessor.ts`; `tests/gfx_corpus/plotly` + tests). Author text on fill, theme author colours, sankey density title, frames/animation, headless WebGL GPU-crash survival, and spatial-charts recipes (G29, Ge462ae, D457, D458, D459). - **GFX packet-diagram rendering fixes** (`frontend/src/plugins/d3/packetPlugin.ts`, `frontend/src/utils/d3Plugins/packetPlugin.ts`; `tests/gfx_corpus/packet` + tests, G6f8b51). - **GFX music-notation rendering fixes** (`frontend/src/plugins/d3/musicPlugin.ts`, `frontend/src/utils/d3Plugins/musicPlugin.ts`; `tests/gfx_corpus/music` + tests). Cross-staff beam dedupe, inline light theme, strict-JSON guard, and ledger / above-band handling (D428, D173, D174, G99d33f). - **GFX core rendering fixes** (`frontend/src/utils/d3EnvelopeUnwrap.ts` (new), `frontend/src/plugins/d3/basicChart.ts`, `chartTheme.ts`, `orthogonalRouter.ts`, `frontend/src/utils/colorUtils.ts`, `pluginDimensions.ts`, `frontend/src/components/D3Renderer.tsx`; `tests/gfx_corpus/basic-chart`, `tests/gfx_corpus/d3`, `tests/gfx_corpus/chat-message` + tests). Diagram-envelope unwrap, basic-chart margins / y-tick count / envelope dispatch, chart theme and colour contrast, orthogonal routing, plugin dimensions, and chat-message table / Prism-token / footnote legibility (G45, GChat, D320, D321, D373, D323, D328, D043). - **LaTeX / PDF rendering fixes** (`app/services/latex_renderer.py`, `latex_profiles.py`, `diagram_renderer.py`, `app/utils/latex_color.py`, `chemfig_lint.py`, `circuitikz_lint.py`, `chat_screenshot.py`; `tests/gfx_corpus/{tikz,tikz-cd,chemfig,circuitikz}` + `tests/test_latex_*.py`). Fill-aware contrast, light text-ink clamp, chemfig adjacent molecules, siunitx TS1 units, circuitikz bipole alias, mediabox objstm, tikz regressions, and diagram capture-extent units (G7127a1, Gbf5475, Geccfe9, Gf070d1, D033, D338, D348, D472, D125, D327). - **Chat rendering fixes** (`frontend/src/components/fenceScanner.ts`, `MarkdownRenderer.tsx`, `MUIChatHistory.tsx`; `frontend/src/components/__tests__/reabsorbLeakedTailContent.test.ts`). Fence scanning, markdown rendering, and reabsorption of tail content that had leaked out of the chat message. - **Diff view: a rejected diff is marked, and the diff-validation hook is hardened** (`frontend/src/utils/diffUtils.ts`, `app/utils/diff_validation_hook.py` + tests). - **Markdown-dialect handling fix** (`frontend/src/utils/markdownDialects.ts`; D017). - **Scroll-velocity handling, including programmatic scrolls** (`frontend/src/utils/scrollVelocity.ts` + tests). ### Changed - **Task-run records: every timestamp is epoch milliseconds (schema 2); old records upgrade on read** (`app/utils/run_time.py` (new: `EpochMs`, `now_ms`, `to_ms`, `normalize_run_record`, `normalize_artifact`, `RUN_RECORD_SCHEMA_VERSION`), `app/models/task_run.py`, `app/models/task_card.py` (`Artifact.created_at`), `app/storage/task_runs.py` (every stamp through `now_ms()`; reads normalize), `app/api/task_runs.py`, `app/agents/block_executor.py`, `app/agents/task_executor.py`, `frontend/src/components/TaskCard/liveActivity.ts` (`formatLastActivity` takes ms), `DeckRunList.tsx`, `TaskCardInlineTile.tsx`, `frontend/src/hooks/useTaskRunStream.ts` (wire seconds converted once at the boundary), `frontend/src/types/task_run.ts`, `design/task-cards.md` (§Timestamps, §Restarts and sibling servers), `tests/test_run_time_normalize.py` (new, 15), `liveActivity.test.ts`). `TaskRun.created_at`/`updated_at` were ms (the `BaseStorage` convention shared with cards, bindings and beads) while `started_at`, `completed_at`, `last_activity_at`, block/iteration states, progress notes, asks and `Artifact.created_at` were `time.time()` seconds — the reconciler itself wrote both units three lines apart. Nothing yet subtracted across the boundary, but a record was unreadable by eye (`fromtimestamp(started_at)` → year 58680, which is how the 2026-09-16 investigation started) and the hazard was one `completed_at - created_at` away. All fields are now `EpochMs`, an `int` whose validator upgrades any populated value below `1e11` from seconds, so a stray `time.time()` still lands correctly. There is deliberately no mass rewrite of the 279 encrypted records on disk: another live server may be mid-write on the same directory, so `TaskRunStorage.get`/`list`/`read_iteration_artifact` upgrade v1 records in memory, keyed on a missing `schema_version`, and a record becomes v2 the next time any writer persists it. The WebSocket stream still carries `ts`/`at` in seconds; `useTaskRunStream` converts once so the tile's REST-vs-live freshness comparison sees one unit. **Rollout: new code reads old records, old code cannot read new ones — restart every `ziya` process that shares the `~/.ziya` together.** - **MCP registry routes slimmed; the client, config validation and registry interface firmed up** (`app/routes/mcp_registry_routes.py`, `app/mcp/client.py`, `app/mcp/config_validation.py`, `app/mcp/registry/interface.py`, `frontend/src/components/__tests__/mcpRegistryInstallFailureUX.test.ts`). Registry route handlers were reduced (shared logic moved into helpers) and the client, config-validation and registry-interface paths were hardened, with an install-failure UX regression test. ## [0.8.8.6] - 2026-09-16 ### Added - **Golden tier for the graphics render suite: pixel-hash fast lane, tolerance-banded diff, drift report, and goldens that are only ever validated by the judge who saw them** (`scripts/gfx_ledger.py` (`golden_capture`, `golden_get`, `golden_set_trust`, `golden_trust_from_hash`, `golden_rebaseline`, `golden_status`, `pixel_hash`; `record --light-png/--dark-png`; `promote` carries validated hashes; new `golden` subcommand), `tests/gfx_render/golden.py` (new), `tests/gfx_render/test_render_golden.py` (new), `tests/gfx_render/conftest.py` (`--render-golden-tolerance`, `--render-golden-strict`, drift report + terminal summary), `tests/test_gfx_golden.py` (new, 22), `tests/test_render_diagram_save_path.py` (new, 12), `app/mcp/tools/diagram_render.py` (`save_path` on `render_diagram`; new `view_image` tool), `app/mcp/builtin_tools.py`, task cards GFX Stage 1 / Stage 2 (updated), "GFX drift judge" (new), `tests/gfx_corpus/README.md`). The smoke tier asserted only that a spec still *renders*; nothing asserted it still renders *the same*, so a layout regression that kept ink on the canvas passed. And the Stage 1 sweep, which judged 2,400 renders, kept verdicts and discarded every image, so there was no baseline to compare against and no way to say what "validated" pixels looked like. Now: a golden is the PNG a spec rendered to in one theme plus a sidecar naming its trust — `provisional` (the runner captured it because none existed; asserts only that later renders can be compared) or `validated` (a judge recorded `ok` on THOSE bytes via `record --light-png/--dark-png`, or the pixels hash-match a validated render committed in the corpus, or a human ran `golden rebaseline --reason`). Goldens are machine-local under `.ziya/gfx-golden/` (gitignored; Chromium-build-specific); what is committed is each validated golden's pixel hash in `expectations.json`, so a fresh clone can recognise the judged image and `golden trust-from-hash` accepts a local render that hashes identically. The runner compares cheapest-first — hash of the decoded RGBA buffer (PNG re-encoding cannot break it), then per-pixel diff at capture resolution with a 0.5 % tolerance — and reports `exact` / `noise` / `baselined` / `drift` (validated golden, beyond tolerance) / `changed` (provisional) / `error`. Drift is recorded to `.ziya/gfx-sweep/drift//` with the new render and a heatmap, not failed (`--render-golden-strict` to fail): the judge is a model, and wiring it into pytest would make the suite need credentials and be able to flip on identical code. Measured on the packet engine, two runs on one commit: 34/58 hash-identical, 24 differ by 4–554 px (worst 0.028 %, 18× under tolerance), 1 pre-existing intermittent screenshot timeout — so a CRC alone would have sent 41 % of a stable engine to the expensive path, and the tolerance is nowhere near a dropped element (6,400 px). Rebaselining is never automatic: forty "equivalent" verdicts in a row could walk a baseline somewhere nobody accepted in one step, so the judge card records and prints the `golden rebaseline` command for a person. `render_diagram(save_path=...)` writes the exact returned bytes (confined to `.ziya/` and `/tmp`, at the tool's common return point so all three render paths are covered) and `view_image` reads them back — the judge needs to see the golden, the drift and the heatmap, and nothing could show an on-disk PNG to a model before. Stage 1/2 cards now render with `save_path` and pass the files to `record`, so the 2,400 renders the sweep already pays for stop being discarded and a verified fix re-baselines by construction. The comparison's unit tests pass a 1-px glyph shift and fail a dropped 100×80 node — the two cases the tolerance exists to separate — and the ledger tests pin that a provisional capture can never replace a validated golden. - **A committed, self-maintaining render regression suite for the graphics engines** (`tests/gfx_corpus/` (new, 733 specs across 20 engines, 4.3 MB), `tests/gfx_render/` (new: `conftest.py`, `test_render_smoke.py`), `scripts/gfx_ledger.py` (`promote`, `promote_defect`, `promote_regression_sets`, `promote_verified`, `invariants_for_signature`; `reconcile` now promotes on the verified transition), `tests/test_gfx_ledger.py` (+6)). Every GFX fix so far added a unit test at the *transform* level — 279 jest + 65 pytest on fixup functions — and none at the *render* level, which is the level the Stage 1 sweep catches regressions at; the residue notes record the consequence repeatedly ("guard test green, render disagrees" on D-215/D-223/D-267/D-270). The only render-level regression check was the sweep itself: 2,400 model-judged renders, run when someone launches a card, whose evidence lives under the gitignored `.ziya/`. The corpus now lives in the repo and grows itself: when `gfx_ledger.py reconcile` flips a defect to `verified` — the moment "this used to be broken, now it renders" is known — its specs are copied into `tests/gfx_corpus//` with an `expectations.json` entry carrying the invariants derived from the defect's signature and the defect id as provenance. Spec bytes are frozen on first promotion (a later sweep rewriting the on-disk spec does not follow into the corpus; the spec *is* the thing under test). Seeded once from the Stage 1 regression sets (199 both-theme-passing specs) and the 235 verified defects (615 specs, deduped to 733). `tests/gfx_render` is opt-in (`--render`; skipped in 0.6 s otherwise) and renders each (spec, theme) through the SAME dispatch `render_diagram` uses — plugin types via the live `/render` harness, LaTeX types via `latex_renderer`, chat-message via the real chat UI — asserting the smoke-tier invariants: `renders` (terminal complete, no timeout/error), `no_console_errors` (warnings are the fixup layer working and are allowed), `ink_present` (≥ 0.2 % of pixels differ from the dominant colour — a lone 50 px artifact on a blank canvas, the D-151 shape, fails; a single short label passes). `--render-scope=smoke` (default) is the 199-spec regression set, ~400 renders at ~2 s each; `full` is the whole corpus. Session fails up front, with the reason, when no server answers on `--render-port`; the root conftest's `ZIYA_HOME` sandbox is overridden to a no-op here because the chat-message renderer must seed into the project the server actually reads, and chat-message cases skip with the remedy when the test process lacks the server's at-rest key material. `visual:*` tags (contrast / layout / min_text_size, mapped from the signature) are recorded for a later measured tier and ignored by this one, so a smoke run never fails on an invariant it cannot measure. A test pins that every invariant name the ledger writes is one the suite reads. Verified live: packet 20/20, chemfig 20/20 (LaTeX path), mermaid 24/24, chat-message 20 skipped with reason. GFX Stage 1 remains the exploratory, model-judged audit for the classes this tier cannot reduce to a number. - **Shadow line control: a `supervised` tier, and the control ceiling is now settable on a live session** (`app/shadow/lease.py`, `app/shadow/policy.py`, `app/shadow/sock_server.py`, `app/shadow/registry.py`, `app/shadow/pty_host.py`, `app/mcp/tools/shadow_tools.py`, `app/cli.py`, `tests/test_shadow_lease.py` (+2), `tests/test_shadow_host_e2e.py`). Trying to take control of a session started without `--allow-control` gave `control_disabled` with no way to fix it short of restarting the terminal; the only runtime knob was the `[x] exec` toggle, a Phase-2 flag nothing reads. `[x]` is gone; `[c]` cycles the session's control ceiling `none → supervised → gated → unrestricted → none`, persists it to the registry (the next `shadow_control` sees it, no restart), journals it, and dropping to `none` also revokes any live lease. `supervised` is a new restriction rung: every command — allowed or not — is confirmed by the human at the terminal (`y`/`n`), the per-command approval tier the original Phase-2 exec handshake described. Rank order for the min-wins clamp is `supervised < strict < gated < unrestricted`, so a chat asking for `strict` against a `supervised` ceiling gets `supervised`. `strict` (policy-only, no prompts) is dropped from the human-facing menu and stays as the headless/spawn tier and a chat-requestable restriction. `--allow-exec` / `allow_exec` are now inert and slated for removal in the audit pass. - **Competitive-landscape study: cli-surface under-claims documented (WS3)** (`Docs/Capabilities.md` ("In-Session Commands" section: new `Also called:` alias line describing the palette architecture), `Docs/FeatureInventory.md` (§11 Developer Experience (CLI-specific): new "Declarative slash-command palette" index row), `.ziya/docs-remediation/closed-cli-surface.json`). Closed the single `cli-slash-command-palette` under-claim row from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`. The in-session slash-command *surface* was already well documented (the `Docs/Capabilities.md` "In-Session Commands" table lists ~14 commands and the `DOC-DEFECTS` discrepancy text itself found "no material over/under-claim"), so the row was closed `already-present`: no command listing was duplicated. What was missing was any reader-vocabulary entry point and any statement of the underlying architecture, so a `` `Also called:` line was added to the Capabilities section — projected from the ledger `summary`/`depth_notes`/`limits` (maturity 4, not in Critic A's `inflated_scores`) — naming the ~20-command palette as a single `COMMAND_SPEC` source of truth that keeps completion, the inline `?` help and dispatch in sync, with the trailing-`?` next-level help and `//` literal-message escape, and stating the limit (not user/plugin-extensible at runtime — commands are added in source). A matching index row was added to `FeatureInventory.md` §11, which previously listed individual commands but never the palette as one capability. Aliases drawn from the ledger `aliases` field; each insertion tagged ``. - **Competitive-landscape study: ast-and-code-intelligence under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§3 Code Intelligence: new multi-language / 25+-language tree-sitter row after the AST reference-tracing row), `Docs/Capabilities.md` (new "Code Intelligence (AST)" section before "Code Application"), `.ziya/docs-remediation/closed-ast-and-code-intelligence.json`). Closed the single `ast-and-code-intelligence` under-claim row from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`: the docs described code intelligence as "AST-based" and mentioned only Python/TypeScript/HTML-CSS, leaving the 25-language tree-sitter parser entirely undocumented. Added one FeatureInventory index line and one Capabilities paragraph — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity (3), stating the limit — for the generic `TreeSitterParser`/`TreeSitterConverter` layer (`ast-treesitter-multilang`), which registers per-language parsers for C/C++, Rust, Go, Java, C#, Kotlin, Swift, Ruby, PHP, Scala, Lua, Perl, R, Elixir, Haskell, Dart, Zig, OCaml, Julia, Bash, HCL, SQL, TOML and YAML and maps their CSTs onto Ziya's unified node types. The maturity-3 limit is stated, not hidden: for tree-sitter languages the parser surfaces only definitions and imports (no call-graph or reference edges, so `ast_references` callers/importers are weak for them), attribute extraction is text-slice based rather than typed, depth is shallower than the native Python parser, and the whole layer depends on an optional package that degrades to disabled if absent. Not in Critic A's `inflated_scores`, so documented at ledger maturity. Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: plugins-governance-and-web-security under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§9 Enterprise & Internal Deployment: envelope encryption, passphrase encryption, directory-scan governance and prompt-extension rows plus an open-core plugin/provider-registry row; new §11b "Web & Transport Security" section: CSP + security headers and origin/CSRF guard rows), `Docs/Capabilities.md` (new "Web security, encryption & extensibility" section before "Skills"), `.ziya/docs-remediation/closed-plugins-governance-and-web-security.json`). Closed the 7 `plugins-governance-and-web-security` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — web-facing HTTP hardening, opt-in at-rest encryption, and the open-core plugin surface that the docs did not describe. The FeatureInventory index lines were already added by an earlier pass in this study; this pass added the matching `Capabilities.md` paragraphs — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for envelope encryption at rest (`application-level-encryption-at-rest`, maturity 4, stated off-by-default / whole-blob / needs `cryptography`), single-env-var passphrase encryption for community installs (`passphrase-encryption-community`), CSP + security response headers with relaxed/strict modes (`content-security-policy-and-headers`), the Origin/Referer CSRF guard (`origin-guard-csrf`), `.ziya/scan.yaml` directory-scan governance (`directory-scan-governance`), and the per-model prompt-extension framework (`prompt-extension-framework`). `plugin-provider-framework` is documented via the §9 open-core plugin/provider-registry row (which lists the previously-omitted `EncryptionProvider`/`DirectoryScanProvider`/`ToolResultFilterProvider`/`ExtractionPatternProvider` interfaces) and cross-referenced from Capabilities' Tool Enhancements alias; it was marked `already-present`. None of the 7 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`application-level-encryption-at-rest` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. (Duplicate §9 rows a concurrent pass produced for the same seven ids were removed so each capability has exactly one FeatureInventory index line and one Capabilities paragraph.) - **Competitive-landscape study: mcp-integration under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§8 MCP: five new rows — built-in in-process tools, config validation, registry marketplace, resources & prompts, startup diagnostics — plus the strengthened rug-pull-quarantine row, and a corrected §8 "Runtime MCP hot-reload" gap-table entry that had falsely claimed config changes require an app restart), `Docs/Capabilities.md` (new "Built-in (in-process) tools" subsection and MCP server-management paragraphs under "MCP Tools", plus a new "Rug-pull fingerprinting & quarantine" paragraph in the tool-result integrity section), `.ziya/docs-remediation/closed-mcp-integration.json`). Closed the 7 `mcp-integration` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the 13-category built-in tool registry and its per-category toggling (`mcp-builtin-direct-tools`), the line-anchored config validator/findings panel (`mcp-config-validation`), the multi-provider aggregation/dedup/ranking marketplace beneath the "registry browser" (`mcp-registry-marketplace`), MCP resources & prompts support beyond tools (`mcp-resources-and-prompts`), runtime server enable/disable/restart and manager reinitialize without an app restart (`mcp-server-lifecycle`, which also corrected the false FeatureInventory gap line), the SHA-256 rug-pull hard-quarantine and human re-authorization flow (`mcp-rugpull-quarantine`, previously under-mentioned as "trigger[s] security warnings"), and staged startup diagnostics with failure attribution (`mcp-startup-diagnostics`). None of the 7 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`mcp-builtin-direct-tools`, `mcp-config-validation`, `mcp-registry-marketplace`, `mcp-rugpull-quarantine`, `mcp-server-lifecycle` at 4; `mcp-resources-and-prompts`, `mcp-startup-diagnostics` at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: frontend-conversation-management under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§4 Context & Conversation Management: five new conversation-lifecycle rows + `` markers on the existing search and persistent-model-pin rows), `Docs/Capabilities.md` (new "Conversation management" subsection under "Context & Projects"), `.ziya/docs-remediation/closed-frontend-conversation-management.json`). Closed the 7 `frontend-conversation-management` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — conversation-lifecycle capabilities the docs did not describe or described only as a keyboard shortcut. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity (all 7 are maturity 3, none in Critic A's `inflated_scores`), stating the limit — for ephemeral (unsaved) conversations and promotion (`fcm-ephemeral-conversations`), move/copy conversations & folders across projects (`fcm-move-copy-project`), the read-only conversation info panel (`fcm-conversation-info-modal`), the durable per-conversation raw/pretty display mode (`fcm-display-mode-toggle`, previously documented only as the `Ctrl+Shift+U` shortcut), and the chat shadow-copy integrity scan/reconcile tooling (`chat-shadow-copy-integrity`). The two already-present capabilities — server-side conversation full-text search (`conversation-full-text-search`, already the "Conversation search with selectable ordering" row) and the durable multi-scope model pin (`per-conversation-model-pin`, already the model-pinning rows in §5) — were marked with `` and given `Also called:` alias lines plus (for parity) Capabilities paragraphs. Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: document-ingestion under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§6 Multimodal & File Formats: `PDF input` row expanded, four new rows — document upload endpoint, upload safety guards, fetched-PDF re-extraction, PCAP TCP health analysis), `Docs/Capabilities.md` (Large-PDF RAG section extended for the object-density routing heuristic and the light/full two-tier index, plus a new "Document Upload & Ingestion Pipeline" subsection), `.ziya/docs-remediation/closed-document-ingestion.json`). Closed the 7 `document-ingestion` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`. The upload path was materially under-documented: the only file-format mention was "PDF input — Native reading" and the PDF RAG section omitted the object-density heuristic and the two-tier index. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit — for the dual-backend text extractor (`pdf-text-extraction`), the multipart upload endpoint with its pluggable handler registry (`document-upload-route`), the oversize/zip-bomb guards (`document-safety-guards`), fetched-PDF re-extraction behind an SSRF guard (`fetched-pdf-http-reextraction`), and per-flow TCP health analysis for packet captures (`pcap-tcp-health`); the two already-present RAG capabilities were enhanced in place — the large-PDF routing ladder's object-density heuristic (`pdf-large-rag-routing`) and the content-addressed light/full index with lazy promotion (`pdf-rag-index`). None of the 7 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`pdf-large-rag-routing`, `pdf-rag-index` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: patch-diff-pipeline under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§3 Code Intelligence: seven new patch-pipeline rows + a corrected/``-marked regression-suite row), `Docs/Capabilities.md` (new "Inside the patch pipeline" subsection under "Code Application"), `.ziya/docs-remediation/closed-patch-diff-pipeline.json`). Closed the 8 `patch-diff-pipeline` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — the applier's robustness layer (the docs showed only the multi-strategy cascade and per-hunk status). Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for pre-apply repair (`diff-preprocessing-normalization`), idempotent re-apply (`diff-already-applied-detection`), fence-escape recovery (`diff-backtick-fence-recovery`), language-aware post-apply validation (`diff-per-language-validation`), env-tunable thresholds (`diff-config-tunables`), CWE-tagged security hardening (`diff-security-hardening`), and apply-time superseded-diff dedup (`diff-frontend-superseded-dedup`). `diff-test-harness-and-regression-suite` was already documented (the "Diff regression suite" FeatureInventory row); its stale "137 edge-case patch tests" count was corrected to the actual 159 test-case directories, it was marked with a ``, and a matching `Capabilities.md` paragraph was added for parity with the other seven. None of the 8 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`diff-already-applied-detection`, `diff-backtick-fence-recovery`, `diff-preprocessing-normalization`, `diff-security-hardening`, `diff-test-harness-and-regression-suite` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: task-cards-and-blocks under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§2a Task Cards: seven new block-grammar rows + a hierarchical-scope row, and `` markers on the existing self-assessment row), `Docs/Capabilities.md` (new "Task Cards & Blocks" section before "Scheduling & Run Lifecycle"), `.ziya/docs-remediation/closed-task-cards-and-blocks.json`). Closed the 9 `task-cards-and-blocks` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — the block grammar's breadth was materially under-stated (README and FeatureInventory headlined only "Task / Repeat / Parallel" for a grammar with eight block types). Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the grammar (`taskcard-block-tree-grammar`), the loop primitives (`taskcard-repeat-loop-block`, `taskcard-until-model-evaluator`), the givens/sequence primitives (`taskcard-state-block`, `taskcard-group-sequence-and-failure-policy`), data passing (`taskcard-template-substitution`), the additive permission tree (`taskcard-hierarchical-scope-model`), and the isolation invariant (`taskcard-conversation-isolation-sandbox`). `taskcard-self-assessment-capture` already had a FeatureInventory row (marked with a `` and given its alias line) and received a new Capabilities paragraph. None of the 9 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`taskcard-block-tree-grammar`, `taskcard-repeat-loop-block`, `taskcard-template-substitution`, `taskcard-hierarchical-scope-model`, `taskcard-conversation-isolation-sandbox` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: mcp-security-and-hallucination under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§8 MCP: nine new tool-result-security / hallucination-defense rows), `Docs/Capabilities.md` (new "MCP tool-result integrity & hallucination defenses" subsection under "Tools"), `.ziya/docs-remediation/closed-mcp-security-and-hallucination.json`). Closed the 10 `mcp-security-and-hallucination` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — a layered set of tool-result trust and anti-hallucination controls the docs did not describe. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for tool-result signing and validation (`mcp-tool-result-signing`, `mcp-response-schema-validation`, `mcp-input-constraint-validation`), tool-description defenses (`homoglyph-confusable-detection`; `mcp-tool-poisoning-scan` got the new Capabilities paragraph, its FeatureInventory row already added by WS1), tool-result hygiene and framing (`hidden-char-sanitization`, `encoded-payload-scanning`, `tool-result-trust-envelope`), and hallucinated-output detection (`shingle-parroting-detection`, `fake-tool-result-echo-detection`). None of the 10 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`hidden-char-sanitization`, `mcp-tool-poisoning-scan`, `mcp-tool-result-signing`, `shingle-parroting-detection` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: context-and-file-tree under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§3 Code Intelligence: expanded the File browser and Token-count rows + three new file-tree rows; §4 Context & Conversation Management: five new rows), `Docs/Capabilities.md` (two new subsections in "Context & Projects" — "File tree & context sources" and "Global, auto-included and reusable context"), `.ziya/docs-remediation/closed-context-and-file-tree.json`). Closed the 10 `context-and-file-tree` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the live file tree and its plumbing (`ctx-live-file-tree-websocket`, `ctx-filesystem-watcher`, `ctx-gitignore-ignore-engine`), context sources (`ctx-external-path-inclusion`, `ctx-auto-included-docs`, `ctx-named-context-sets`), model-side context curation (`ctx-model-self-curated-files`), the anti-hallucination file-state tracker (`ctx-file-state-line-tracking`), the per-file token *estimate* (`ctx-per-file-token-estimation`), and cross-project global chats/folders (`cross-project-global-items`). The pre-existing terse "File browser" and "Token count per file" rows were expanded in place (external-path persistence/`[external]` branch; the estimate-vs-exact clarification) rather than duplicated. None of the 10 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity (`ctx-file-state-line-tracking`, `ctx-gitignore-ignore-engine`, `ctx-model-self-curated-files` at 4; the rest at 3). Each insertion carries an `Also called:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: memory-beads-and-skills under-claims documented (WS3)** (`Docs/FeatureInventory.md` (new "Memory & beads — capability index" table in §2b, four new rows in §10 Skills), `Docs/Capabilities.md` (new bead-branching-fork paragraph in the "Memory" section), `.ziya/docs-remediation/closed-memory-beads-and-skills.json`). Closed the 11 `memory-beads-and-skills` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`: the persistent memory subsystem, the built-in/file-discovered skill library, and seam-based conversation branching. Added one FeatureInventory index line each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the memory store and lifecycle (`memory-flat-store`, `memory-post-conversation-extraction`, `memory-proposal-lifecycle`, `memory-browser-ui-api`, `memory-mindmap`, `memory-organizer`), conversation branching (`bead-branching-fork`), and the skills library (`skills-builtin-library`, `skills-catalog-injection-ondemand`, `skills-enhanced-dimensions`, `skills-file-discovery-agentskills`). The matching `Capabilities.md` "Memory" and "Skills" paragraphs for 10 of the 11 were already present from an earlier pass and were left in place; `bead-branching-fork` received a new Capabilities paragraph. None of the 11 appear in Critic A's `inflated_scores`, so all are documented at ledger maturity. Each insertion carries an `also called:` alias line drawn from the ledger `aliases` and is tagged ``; `skills-builtin-library`'s "few example skills" under-claim and `skills-enhanced-dimensions`'s "in future releases" over-caution are corrected to what ships today. - **Competitive-landscape study: visualization-renderers under-claims documented (WS3)** (`Docs/FeatureInventory.md` (§7 Visualizations: six new renderer rows + an internals note in the section intro), `Docs/Capabilities.md` ("Diagram Rendering": six new format rows, an "Also known as" alias block, and a new "Renderer coverage and internals" subsection), `.ziya/docs-remediation/closed-visualization-renderers.json`). Closed the 12 `visualization-renderers` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — registered renderers and shared rendering infrastructure the doc format tables did not list. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the format renderers (`viz-plotly-render`, `viz-jointjs-render`, `viz-chord-render`, `viz-force-directed-render`, `viz-network-diagram-render`, `viz-vega-render`) and the rendering internals (`viz-plugin-registry-and-selection`, `viz-mermaid-spec-repair` — the ~77-rule auto-repair pipeline, `viz-malformed-spec-recovery`, `viz-shared-layout-engine`). `viz-html-mockup-renderer` and `viz-music-render` were already documented and received only an `Also known as:` alias line and a `` marker. Each insertion carries an alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: scheduling-and-automation under-claims documented (WS3)** (`Docs/FeatureInventory.md` (new §2c "Scheduling & Run Lifecycle" + `` markers on the existing gear and history-replay rows), `Docs/Capabilities.md` (new "Scheduling & Run Lifecycle" section), `.ziya/docs-remediation/closed-scheduling-and-automation.json`). Closed the 14 `scheduling-and-automation` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json` — a fully-implemented, tested, UI-exposed scheduling engine and run-lifecycle machinery that the docs did not mention at all (`grep schedul|cron|recurring` returned zero hits). Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the scheduler (`sched-task-card-scheduler`, `sched-catch-up-coalesce`, `sched-max-runs-cap`, `sched-schedule-state-endpoint`, `sched-system-jobs`), the run lifecycle (`sched-run-lifecycle-status-model`, `sched-held-fault-classification`, `sched-zombie-reconciliation`), run observability (`sched-run-activity-heartbeat`, `sched-task-run-stream-relay`), resume/retention (`sched-resume-through-call`, `sched-iteration-artifact-retention`), and artifact serving (`sched-artifact-blob-serving`). `run-status-index` was already documented (the conversation-list gear + its cached projection) and received only an `Also called:` alias line and a `` marker. Each insertion carries an `also called:` alias line drawn from the ledger `aliases` and is tagged ``. Scheduler `cron`/timezone caveats and internal-only `system-jobs` status are stated, not hidden. - **Competitive-landscape study: execution-sandbox-and-approvals under-claims documented (WS3)** (`Docs/FeatureInventory.md` (new §11a "Execution Safety, Sandboxing & Approvals"), `Docs/Capabilities.md` (new "Execution Safety & Approvals" section), `.ziya/docs-remediation/closed-execution-sandbox-and-approvals.json`). Closed the 16 `execution-sandbox-and-approvals` under-claim rows from the synthesis run (`s1-20260910`) `DOC-DEFECTS.json`: shipped, competitively-relevant capabilities that were undocumented or under-described. Added one FeatureInventory index line and one Capabilities paragraph each — projected from the ledger `summary`/`limits`/`user_visible` at the ledger maturity, stating the limit where maturity is 3 — for the approval gate (`escalation-config-signature-gate`, `approval-keypair-provisioning`, `ephemeral-session-grant`, `approval-ttl-expiry`, `escalation-audit-ledger`), the shell command guards (`shell-command-allowlist-engine`, `shell-write-policy-enforcement`, `shell-credential-exfiltration-guard`, `shell-iac-deploy-guard`, `shell-interpreter-escape-guard`, `shell-execution-runtime-hardening`), and the write-path / provenance / diagnostics layer (`parent-authoritative-project-write-paths`, `task-run-permissions-snapshot`, `task-scope-permissions-editor`, `scope-clamp-diagnostics`). `approval-root-signer-cli` was documented at Critic-A's corrected maturity 4 (claimed 5) with a new Capabilities paragraph; its FeatureInventory presence (§2a) was already added by WS1 and left in place. Each insertion carries an `also known as:` alias line drawn from the ledger `aliases` and is tagged ``. - **Competitive-landscape study: "What Ziya deliberately does not do" (WS4)** (`Docs/DesignPhilosophy.md` (new section before "What I'd Do Differently"), `.ziya/docs-remediation/ws4-nongoals.json`, `.ziya/complandscape/60-synthesis/s1-20260910/DOC-DEFECTS.json`). Documented the 28 capabilities the synthesis run (`s1-20260910`) confirmed absent-by-mechanism and classified `DELIBERATE_NON_GOAL`, grouped under the five reason themes from the reintegration summary (local-first single-owner trust model; served web-SPA + CLI harness, not an editor; no infrastructure provisioning; BYO-credentials model-agnostic broker; streaming-text core, no generative media). Each entry states what the capability is (with the competitor whose term it is), why Ziya declines it as a decision, and what the decision costs — sourced from each `-disposition.json` `recommendation`/`architectural_conflicts`/`residual_quality_gap`, cross-checked against Critic B's `gap_queue_entries_to_discount`/`category_errors` and REPORT.md §9, and tagged ``. All 28 survived the critic cross-check (0 excluded); the 28 `non-goal` rows in `DOC-DEFECTS.json` are marked `closed_by=WS4`. - **Competitive-landscape study: competitor-vocabulary map for the 24 terminology artifacts (WS2)** (`Docs/CapabilityGlossary.md` ("Terms competitors use" table), `Docs/Capabilities.md` (11 inline alias lines), `.ziya/complandscape/60-synthesis/s1-20260910/DOC-DEFECTS.json`). Filled the hand-maintained "Terms competitors use" section of the glossary with a 24-row table mapping each competitor's term (and tool) to the Ziya mechanism (`ziya_internal_name`) and its **critic-corrected** status — FOUND at the ledger maturity, or PARTIAL with what is missing for the 6 rows Critic A downgraded (`automatic-mindmap-generation`, `custom-personas-assistants`, `diff-llm-resolver-stage`, `lint-test-fix-repair-loop`, `visual-agent-flow-builder`, `user-authored-project-rules`), with dead-code paths (`post-instruction-framework-dead`, the `app/shadow/journal.py` writer/reader, the `ContinuationMiddleware`/`ToolResultFilterProvider` stubs) marked _experimental, not wired at runtime_. Where the mechanism is already described in `Capabilities.md`, added an inline "Also known as: … ; competitors call this …" line beneath its first mention (11 of 24), each tagged ``; the other 13 have no `Capabilities.md` mention and are recorded for the WS3 under-claim pass. - **Competitive-landscape study: generated capability glossary (WS2)** (`scripts/complandscape_docdefects.py` (`glossary` subcommand, `--stdout`/`--write`), `Docs/CapabilityGlossary.md` (new), `tests/test_complandscape_docdefects.py` (+7)). A reader-vocabulary → Ziya-name cross reference generated from the ledger `aliases` and matrix domains: one entry per capability at maturity ≥2 (`ctx-live-file-tree-websocket`, `ctx-gitignore-ignore-engine`, …), grouped by matrix domain then alphabetically, carrying the capability name (linked to the `Capabilities.md` heading slug that names it, else to the file), the Ziya id in code font, the maturity, and an `also:` alias list deduplicated case-insensitively and capped at 8 with reader-facing phrases sorted ahead of code identifiers. Maturity ≤1 capabilities (`post-instruction-framework-dead`, the `app/shadow/journal.py` pieces, `sched-timezone-declared-not-implemented`, …) are parked, alias-free, in an "Experimental or vestigial" subsection. Output is deterministic (same corpus → identical bytes, no timestamp in the generated region); `--write` rewrites only the region between the `GENERATED` markers, preserving the hand-authored preamble and the "Terms competitors use" placeholder. The 430 capabilities with no matching `Capabilities.md` heading are reported to stderr as the WS3 under-claim backlog. 467 entries across 14 domains. - **Competitive-landscape study: documentation-defect projection** (`scripts/complandscape_docdefects.py` (new), `tests/test_complandscape_docdefects.py` (new, 10)). Extracts a `DOC-DEFECTS.json`/`.md` row set from the CL synthesis-run corpus (ledger, matrix, reintegration summary, per-capability Stage A/disposition, Critic A) classifying each doc discrepancy as terminology / non-goal / inflated / partial (explicit-id, `auto=false`) or under / over / other (keyword-matched free text, `auto=true`) without judging or rewriting the underlying findings. - **Competitive-landscape study: manual spot-check of `scripts/complandscape_docdefects.py`'s auto-classified rows** (`.ziya/complandscape/60-synthesis/s1-20260910/DOC-DEFECTS.json`). Reviewed every `class=other` row (41) plus a deterministic every-kth sample of `auto=true` rows in `under` (20/120) and `over` (10/19), reading `discrepancy_text` and correcting `class` where the keyword match was wrong; 24 of 71 rows were reclassified (`other`→mostly `under`, ~51%; `over`→`under` 2/10, 20%; `under`→`over` 1/20, 5%) and all 71 are now `reviewed=true`; `auto=false` rows were left untouched. - **Shadow sessions — the handoff's open items closed, plus a stale-registry bug the reboot exposed** (`app/shadow/registry.py`, `app/shadow/redaction.py`, `app/shadow/journal.py`, `app/shadow/pty_host.py`, `app/shadow/client.py`, `app/cli.py`, `app/main.py`, `app/cli_commands.py` (new), `tests/test_shadow_handoff_opens.py` (new, 20), `tests/test_shadow_security_control.py` (+6), `Docs/design/shadow-sessions.md` §3, §5, §7, §8, §10). **Registry liveness was `os.kill(pid, 0)`**, so after a reboot every entry a SIGKILLed or power-cut host left behind named a pid that may now be an unrelated process — and listed as a live session with a dead socket (the user's pre-reboot `06fb51` was this). A foreign pid must now also answer `ping` on the entry's socket with the matching `session_id`; missing/refused/wrong-id is stale and reaped, a timeout is indeterminate and left alone (reaping unlinks the journal, which must never happen to a busy host), an entry younger than 5 s with no socket is a host that has saved but not yet bound, and the current process's own entries are trivially live. Writing that test surfaced a second defect: `remove()` unlinked whatever socket/journal paths the entry named as long as they were inside a shadow directory, so reaping a corrupt entry pointing at a *sibling's* socket took the live session down — unlink is now confined to the entry's own `.sock`/`.journal`. **Registry at rest:** the entry (label, argv, cwd, meta — user- and model-authored text that carries hostnames and sometimes credentials) is now one ALE line under the journal's envelope when encryption is active; readers accept both forms and skip, rather than reap, an entry encrypted under a key they lack. **User redaction sets** for the lab-banner class of leak only a user can name: `~/.ziya/shadow/policies/redact/.json` (`{"patterns": [{"kind", "regex"}]}`, shareable like a policy), applied with `ziya shadow --redact ` (repeatable) or `--redact-regex RX`; a user regex runs on every output byte inside the terminal wrapper, so it passes the same ReDoS guard as a model `search` pattern, may not match the empty string, is capped at 64, and a missing or malformed set refuses to start the session (a seam test proves the flag reaches `ShadowCore` as compiled patterns — a flag parsed and dropped would pass every unit test). **`search` now matches meta records** — the event name and string values of its data, so an `ask` question, a chat `comment`, or a control verdict's command/reason is findable; keys and nested provenance are not searched. **The `[i]` instrument snippet** is delivered as a bracketed paste (`ESC[200~…ESC[201~`) when the child's line editor has DECSET 2004 on, tracked from its output: a bare burst of `;`, `{` and quotes tripped ZLE widgets under p10k and the snippet arrived mangled; a plain shell still gets the bare bytes, and the journaled `cmd` is the snippet either way since the markers are input escapes the segmenter strips. **Transcript cosmetics:** output blocks no longer open with the blank line left by the echoed command's newline, and tabs are expanded before the 5-column prefix shifts the tab stops (which collapsed `ls`/`ps` columns). **One subcommand set:** the `{'chat','ask','review','explain','task','shadow'}` literal lived in `app/main.py` twice and `app/cli.py` once; it is now `app.cli_commands.TOP_LEVEL_COMMANDS` (stdlib-only, importable before the heavy imports), with a test that the argparse subparsers equal it and neither module carries the literal any more. Observed once under load (avg ~14) and not reproduced in three subsequent runs: `test_socket_rejects_connections_from_another_uid` failed in a full-suite run — flagged, not chased. Not done: `$((6*7))` still classifies as a substitution (conservative, accepted); the enterprise policy-gate hook on `shadow_read`/`shadow_send` stays parked with the plugin work. - **Competitive-landscape study: comparative appendices and PDF build** (`scripts/complandscape_appendix.py` (new), `tests/test_complandscape_appendix.py` (new, 8)). The CL6 synthesis report keeps only rollups; the intermediate comparative data — 108 head-to-head depth tables, 27 per-tool scorecards, the 128-row gap register — was reachable only by reading JSON. One script now projects those back into print form as Appendices A–C of `REPORT.pdf` (A4) and a separate `MATRIX.pdf` companion (555 × 28, A3 landscape), adding no judgment: every number is copied from the corpus, and Critic A/B corrections are shown as `raw→**defensible**` beside the recorded value rather than replacing it (CL4 ledger corrections as `old→new`). Rendering is markdown-it-py → Chromium via Playwright, all inside the shell allowlist. One rendering defect fixed on the way: the report's prose quotes a literal `